From 6a8b69961d34aad3a0f446e2583ef6c0bb2446cb Mon Sep 17 00:00:00 2001 From: Jonathan Klee Date: Fri, 19 Dec 2025 18:40:27 +0100 Subject: [PATCH] chore: add some unit tests to increase code coverage --- app/build.gradle | 2 + .../utils/BinaryToDecimalSizeConverter.kt | 4 +- .../e/apps/FakeAppLoungePackageManager.kt | 4 +- .../e/apps/data/NetworkHandlerTest.kt | 109 +++++++++++++ .../e/apps/data/ResultSupremeTest.kt | 66 ++++++++ .../java/foundation/e/apps/data/ResultTest.kt | 36 +++++ .../java/foundation/e/apps/data/StoresTest.kt | 64 ++++++++ .../application/ApplicationDataManagerTest.kt | 145 +++++++++++++++++ .../data/application/data/ApplicationTest.kt | 77 +++++++++ .../data/application/home/HomeApiImplTest.kt | 99 ++++++++++++ ...kt => BinaryToDecimalSizeConverterTest.kt} | 28 ++-- .../application/utils/CategoryUtilsTest.kt | 66 ++++++++ .../blockedApps/BlockedAppRepositoryTest.kt | 82 ++++++++++ .../cleanapk/ApplicationDeserializerTest.kt | 54 +++++++ .../data/cleanapk/CleanApkSearchHelperTest.kt | 49 ++++++ .../repositories/HomeConverterTest.kt | 76 +++++++++ .../install/AppInstallConverterTest.kt | 53 ++++++ .../e/apps/data/enums/AppTagTest.kt | 14 ++ .../e/apps/data/enums/FilterLevelTest.kt | 23 +++ .../e/apps/data/enums/SourceTest.kt | 23 +++ .../e/apps/data/install/FileManagerTest.kt | 50 ++++++ .../data/install/models/AppInstallTest.kt | 50 ++++++ .../e/apps/data/login/LoginCommonTest.kt | 44 +++++ .../login/api/PlayStoreLoginWrapperTest.kt | 40 +++++ .../fdroid/FDroidAntiFeatureRepositoryTest.kt | 44 +++++ .../GPlayContentRatingRepositoryTest.kt | 56 +++++++ .../data/playstore/utils/AC2DMTaskTest.kt | 57 +++++++ .../data/preference/AppLoungeDataStoreTest.kt | 59 +++++++ .../preference/AppLoungePreferenceTest.kt | 70 ++++++++ .../domain/ValidateAppAgeLimitUseCaseTest.kt | 152 ++++++++++++++++++ .../download/data/DownloadProgressLDTest.kt | 66 ++++++++ .../model/CategoriesDiffUtilTest.kt | 35 ++++ .../e/apps/utils/ExodusUriGeneratorTest.kt | 45 ++++++ .../foundation/e/apps/utils/ExtensionsTest.kt | 18 +++ .../e/apps/utils/StorageComputerTest.kt | 40 +++++ .../e/apps/utils/SystemInfoProviderTest.kt | 22 +++ .../e/apps/utils/eventBus/AppEventTest.kt | 47 ++++++ gradle/libs.versions.toml | 2 + 38 files changed, 1953 insertions(+), 18 deletions(-) create mode 100644 app/src/test/java/foundation/e/apps/data/NetworkHandlerTest.kt create mode 100644 app/src/test/java/foundation/e/apps/data/ResultSupremeTest.kt create mode 100644 app/src/test/java/foundation/e/apps/data/ResultTest.kt create mode 100644 app/src/test/java/foundation/e/apps/data/StoresTest.kt create mode 100644 app/src/test/java/foundation/e/apps/data/application/ApplicationDataManagerTest.kt create mode 100644 app/src/test/java/foundation/e/apps/data/application/data/ApplicationTest.kt create mode 100644 app/src/test/java/foundation/e/apps/data/application/home/HomeApiImplTest.kt rename app/src/test/java/foundation/e/apps/data/application/utils/{BinaryToDecimalSizeConverterKtTest.kt => BinaryToDecimalSizeConverterTest.kt} (55%) create mode 100644 app/src/test/java/foundation/e/apps/data/application/utils/CategoryUtilsTest.kt create mode 100644 app/src/test/java/foundation/e/apps/data/blockedApps/BlockedAppRepositoryTest.kt create mode 100644 app/src/test/java/foundation/e/apps/data/cleanapk/ApplicationDeserializerTest.kt create mode 100644 app/src/test/java/foundation/e/apps/data/cleanapk/CleanApkSearchHelperTest.kt create mode 100644 app/src/test/java/foundation/e/apps/data/cleanapk/repositories/HomeConverterTest.kt create mode 100644 app/src/test/java/foundation/e/apps/data/database/install/AppInstallConverterTest.kt create mode 100644 app/src/test/java/foundation/e/apps/data/enums/AppTagTest.kt create mode 100644 app/src/test/java/foundation/e/apps/data/enums/FilterLevelTest.kt create mode 100644 app/src/test/java/foundation/e/apps/data/enums/SourceTest.kt create mode 100644 app/src/test/java/foundation/e/apps/data/install/FileManagerTest.kt create mode 100644 app/src/test/java/foundation/e/apps/data/install/models/AppInstallTest.kt create mode 100644 app/src/test/java/foundation/e/apps/data/login/LoginCommonTest.kt create mode 100644 app/src/test/java/foundation/e/apps/data/login/api/PlayStoreLoginWrapperTest.kt create mode 100644 app/src/test/java/foundation/e/apps/data/parentalcontrol/fdroid/FDroidAntiFeatureRepositoryTest.kt create mode 100644 app/src/test/java/foundation/e/apps/data/parentalcontrol/googleplay/GPlayContentRatingRepositoryTest.kt create mode 100644 app/src/test/java/foundation/e/apps/data/playstore/utils/AC2DMTaskTest.kt create mode 100644 app/src/test/java/foundation/e/apps/data/preference/AppLoungeDataStoreTest.kt create mode 100644 app/src/test/java/foundation/e/apps/data/preference/AppLoungePreferenceTest.kt create mode 100644 app/src/test/java/foundation/e/apps/domain/ValidateAppAgeLimitUseCaseTest.kt create mode 100644 app/src/test/java/foundation/e/apps/install/download/data/DownloadProgressLDTest.kt create mode 100644 app/src/test/java/foundation/e/apps/ui/categories/model/CategoriesDiffUtilTest.kt create mode 100644 app/src/test/java/foundation/e/apps/utils/ExodusUriGeneratorTest.kt create mode 100644 app/src/test/java/foundation/e/apps/utils/ExtensionsTest.kt create mode 100644 app/src/test/java/foundation/e/apps/utils/StorageComputerTest.kt create mode 100644 app/src/test/java/foundation/e/apps/utils/SystemInfoProviderTest.kt create mode 100644 app/src/test/java/foundation/e/apps/utils/eventBus/AppEventTest.kt diff --git a/app/build.gradle b/app/build.gradle index 3a186bd69..6d1d30632 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -53,6 +53,7 @@ def jacocoFileFilter = [ '**/BuildConfig.*', '**/Manifest*.*', '**/*Test*.*', + '**/ui/**', '**/*_ViewBinding*.*', '**/*Binding.class', '**/*BindingImpl.class', @@ -310,6 +311,7 @@ dependencies { testImplementation(libs.mockito.inline) testImplementation(libs.core.testing) testImplementation(libs.mockk) + testImplementation(libs.robolectric) // Coil and PhotoView implementation(libs.coil) diff --git a/app/src/main/java/foundation/e/apps/data/application/utils/BinaryToDecimalSizeConverter.kt b/app/src/main/java/foundation/e/apps/data/application/utils/BinaryToDecimalSizeConverter.kt index 5e24f0b0c..bcf265e58 100644 --- a/app/src/main/java/foundation/e/apps/data/application/utils/BinaryToDecimalSizeConverter.kt +++ b/app/src/main/java/foundation/e/apps/data/application/utils/BinaryToDecimalSizeConverter.kt @@ -21,7 +21,7 @@ package foundation.e.apps.data.application.utils import java.util.Locale @Suppress("MagicNumber") -fun convertBinaryToDecimal(input: String): String { +fun convertBinaryToDecimal(input: String, locale: Locale = Locale.getDefault()): String { val binaryToDecimalMap = mapOf( "KiB" to "KB", "MiB" to "MB", @@ -49,7 +49,7 @@ fun convertBinaryToDecimal(input: String): String { "GiB" -> value * 1.073741824 else -> value } - String.format(Locale.getDefault(), "%.2f %s", decimalValue, decimalUnit) + String.format(locale, "%.2f %s", decimalValue, decimalUnit) } else { "" // Unknown unit } diff --git a/app/src/test/java/foundation/e/apps/FakeAppLoungePackageManager.kt b/app/src/test/java/foundation/e/apps/FakeAppLoungePackageManager.kt index c27a0257c..9b2b69fd2 100644 --- a/app/src/test/java/foundation/e/apps/FakeAppLoungePackageManager.kt +++ b/app/src/test/java/foundation/e/apps/FakeAppLoungePackageManager.kt @@ -25,9 +25,9 @@ import foundation.e.apps.data.updates.UpdatesManagerImpl.Companion.PACKAGE_NAME_ import foundation.e.apps.data.updates.UpdatesManagerImpl.Companion.PACKAGE_NAME_F_DROID import foundation.e.apps.install.pkg.AppLoungePackageManager -class FakeAppLoungePackageManager( +open class FakeAppLoungePackageManager( context: Context, - val gplayApps: List, + private val gplayApps: List = emptyList(), ) : AppLoungePackageManager(context) { val applicationInfo = mutableListOf( diff --git a/app/src/test/java/foundation/e/apps/data/NetworkHandlerTest.kt b/app/src/test/java/foundation/e/apps/data/NetworkHandlerTest.kt new file mode 100644 index 000000000..8e0b9f0d0 --- /dev/null +++ b/app/src/test/java/foundation/e/apps/data/NetworkHandlerTest.kt @@ -0,0 +1,109 @@ +package foundation.e.apps.data + +import com.google.common.truth.Truth.assertThat +import foundation.e.apps.data.enums.ResultStatus +import foundation.e.apps.data.login.exceptions.GPlayException +import foundation.e.apps.data.playstore.utils.GPlayHttpClient +import foundation.e.apps.data.playstore.utils.GplayHttpRequestException +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import org.junit.Test +import java.net.SocketTimeoutException + +@OptIn(ExperimentalCoroutinesApi::class) +class NetworkHandlerTest { + + @Test + fun handleNetworkResult_returnsSuccess() = runTest { + val result = handleNetworkResult { "ok" } + + assertThat(result).isInstanceOf(ResultSupreme.Success::class.java) + assertThat(result.data).isEqualTo("ok") + assertThat(result.getResultStatus()).isEqualTo(ResultStatus.OK) + } + + @Test + fun handleNetworkResult_convertsSocketTimeout() = runTest { + val timeout = SocketTimeoutException("too slow") + + val result = handleNetworkResult { throw timeout } + + assertThat(result.isTimeout()).isTrue() + assertThat(result.message).isEqualTo("too slow Status: Timeout") + assertThat(result.exception).isSameInstanceAs(timeout) + } + + @Test + fun handleNetworkResult_mapsHttp429ToError() = runTest { + val exception = GplayHttpRequestException( + GPlayHttpClient.STATUS_CODE_TOO_MANY_REQUESTS, + "throttled" + ) + + val result = handleNetworkResult { throw exception } + + assertThat(result.isUnknownError()).isTrue() + assertThat(result.message).isEqualTo("throttled Status: 429") + assertThat(result.exception).isInstanceOf(GPlayException::class.java) + assertThat((result.exception as GPlayException).isTimeout).isFalse() + } + + @Test + fun handleNetworkResult_mapsHttp408ToTimeout() = runTest { + val exception = GplayHttpRequestException( + GPlayHttpClient.STATUS_CODE_TIMEOUT, + "deadline exceeded" + ) + + val result = handleNetworkResult { throw exception } + + assertThat(result.isTimeout()).isTrue() + assertThat(result.exception).isInstanceOf(GPlayException::class.java) + assertThat((result.exception as GPlayException).isTimeout).isTrue() + assertThat(result.exception?.message).isEqualTo("deadline exceeded Status: 408") + } + + @Test + fun handleNetworkResult_mapsGenericExceptions() = runTest { + val exception = IllegalStateException("broken") + + val result = handleNetworkResult { throw exception } + + assertThat(result.isUnknownError()).isTrue() + assertThat(result.message).isEqualTo("broken Status: Unknown") + assertThat(result.exception).isSameInstanceAs(exception) + } + + @Test + fun retryWithBackoff_retriesOnceWhenAllowed() = runTest { + var attempts = 0 + + val result: ResultSupreme? = retryWithBackoff { + attempts++ + if (attempts == 1) { + ResultSupreme.Error("", Exception("500 error")) + } else { + ResultSupreme.Success("done") + } + } + + assertThat(attempts).isEqualTo(2) + assertThat(testScheduler.currentTime).isEqualTo(1000L) + assertThat(result).isInstanceOf(ResultSupreme.Success::class.java) + assertThat(result?.data).isEqualTo("done") + } + + @Test + fun retryWithBackoff_doesNotRetryOnRateLimitCodes() = runTest { + var attempts = 0 + + val result: ResultSupreme? = retryWithBackoff { + attempts++ + ResultSupreme.Error("", Exception("429 rate limited")) + } + + assertThat(attempts).isEqualTo(1) + assertThat(testScheduler.currentTime).isEqualTo(0L) + assertThat(result).isInstanceOf(ResultSupreme.Error::class.java) + } +} diff --git a/app/src/test/java/foundation/e/apps/data/ResultSupremeTest.kt b/app/src/test/java/foundation/e/apps/data/ResultSupremeTest.kt new file mode 100644 index 000000000..00cab02b8 --- /dev/null +++ b/app/src/test/java/foundation/e/apps/data/ResultSupremeTest.kt @@ -0,0 +1,66 @@ +package foundation.e.apps.data + +import com.google.common.truth.Truth.assertThat +import foundation.e.apps.data.enums.ResultStatus +import org.junit.Test +import java.util.concurrent.TimeoutException + +class ResultSupremeTest { + + @Test + fun createSuccessStoresDataAndStatus() { + val result = ResultSupreme.create(ResultStatus.OK, data = "value") + + assertThat(result).isInstanceOf(ResultSupreme.Success::class.java) + assertThat(result.isSuccess()).isTrue() + assertThat(result.data).isEqualTo("value") + assertThat(result.getResultStatus()).isEqualTo(ResultStatus.OK) + } + + @Test + fun createTimeoutPreservesMessageAndException() { + val timeoutException = TimeoutException("slow network") + + val result = ResultSupreme.create( + ResultStatus.TIMEOUT, + data = listOf("retry later"), + message = "wait please", + exception = timeoutException + ) + + assertThat(result).isInstanceOf(ResultSupreme.Timeout::class.java) + assertThat(result.isTimeout()).isTrue() + assertThat(result.message).isEqualTo("wait please") + assertThat(result.exception).isSameInstanceAs(timeoutException) + } + + @Test + fun createUnknownErrorCarriesDataAndStatusMessage() { + val exception = IllegalStateException("boom") + + val result = ResultSupreme.create( + ResultStatus.UNKNOWN, + data = "partial", + exception = exception + ) + + assertThat(result).isInstanceOf(ResultSupreme.Error::class.java) + assertThat(result.data).isEqualTo("partial") + val status = result.getResultStatus() + assertThat(status).isEqualTo(ResultStatus.UNKNOWN) + assertThat(status.message).isEqualTo("boom") + assertThat(result.exception).isSameInstanceAs(exception) + } + + @Test + fun replicateReusesMessageAndException() { + val original = ResultSupreme.Error("message", IllegalArgumentException("fail")) + + val replica = ResultSupreme.replicate(original, newData = 5) + + assertThat(replica).isInstanceOf(ResultSupreme.Error::class.java) + assertThat(replica.data).isEqualTo(5) + assertThat(replica.message).isEqualTo("message") + assertThat(replica.exception).isInstanceOf(IllegalArgumentException::class.java) + } +} diff --git a/app/src/test/java/foundation/e/apps/data/ResultTest.kt b/app/src/test/java/foundation/e/apps/data/ResultTest.kt new file mode 100644 index 000000000..1510d5183 --- /dev/null +++ b/app/src/test/java/foundation/e/apps/data/ResultTest.kt @@ -0,0 +1,36 @@ +package foundation.e.apps.data + +import com.google.common.truth.Truth.assertThat +import org.junit.Test + +class ResultTest { + + @Test + fun successFactorySetsStatusAndData() { + val result = Result.success("value") + + assertThat(result.status).isEqualTo(Result.Status.SUCCESS) + assertThat(result.data).isEqualTo("value") + assertThat(result.message).isNull() + assertThat(result.isSuccess()).isTrue() + } + + @Test + fun errorFactoryKeepsMessage() { + val result = Result.error("oops", data = 5) + + assertThat(result.status).isEqualTo(Result.Status.ERROR) + assertThat(result.data).isEqualTo(5) + assertThat(result.message).isEqualTo("oops") + assertThat(result.isSuccess()).isFalse() + } + + @Test + fun loadingFactoryAllowsOptionalData() { + val result = Result.loading() + + assertThat(result.status).isEqualTo(Result.Status.LOADING) + assertThat(result.data).isNull() + assertThat(result.isSuccess()).isFalse() + } +} diff --git a/app/src/test/java/foundation/e/apps/data/StoresTest.kt b/app/src/test/java/foundation/e/apps/data/StoresTest.kt new file mode 100644 index 000000000..0a0902a4a --- /dev/null +++ b/app/src/test/java/foundation/e/apps/data/StoresTest.kt @@ -0,0 +1,64 @@ +package foundation.e.apps.data + +import com.google.common.truth.Truth.assertThat +import foundation.e.apps.data.cleanapk.repositories.CleanApkAppsRepository +import foundation.e.apps.data.cleanapk.repositories.CleanApkPwaRepository +import foundation.e.apps.data.enums.Source +import foundation.e.apps.data.playstore.PlayStoreRepository +import foundation.e.apps.data.preference.AppLoungePreference +import io.mockk.every +import io.mockk.mockk +import org.junit.Before +import org.junit.Test + +class StoresTest { + + private val playStoreRepository: PlayStoreRepository = mockk(relaxed = true) + private val cleanApkAppsRepository: CleanApkAppsRepository = mockk(relaxed = true) + private val cleanApkPwaRepository: CleanApkPwaRepository = mockk(relaxed = true) + private val preference: AppLoungePreference = mockk() + private lateinit var stores: Stores + + @Before + fun setUp() { + stores = Stores(playStoreRepository, cleanApkAppsRepository, cleanApkPwaRepository, preference) + } + + @Test + fun getStoresReturnsOnlyEnabledSources() { + every { preference.isPlayStoreSelected() } returns true + every { preference.isOpenSourceSelected() } returns false + every { preference.isPWASelected() } returns true + + val result = stores.getStores() + + assertThat(result.keys).containsExactly(Source.PLAY_STORE, Source.PWA) + assertThat(result[Source.PLAY_STORE]).isSameInstanceAs(playStoreRepository) + assertThat(result[Source.PWA]).isSameInstanceAs(cleanApkPwaRepository) + } + + @Test + fun enableAndDisableStoreProxiesPreference() { + every { preference.enableOpenSource() } returns Unit + every { preference.disableOpenSource() } returns Unit + + stores.enableStore(Source.OPEN_SOURCE) + stores.disableStore(Source.OPEN_SOURCE) + + io.mockk.verify { preference.enableOpenSource() } + io.mockk.verify { preference.disableOpenSource() } + } + + @Test + fun isStoreEnabledReflectsPreferenceFlags() { + every { preference.isPlayStoreSelected() } returns false + every { preference.isOpenSourceSelected() } returns false + every { preference.isPWASelected() } returns true + + val enabled = stores.isStoreEnabled(Source.PWA) + val disabled = stores.isStoreEnabled(Source.PLAY_STORE) + + assertThat(enabled).isTrue() + assertThat(disabled).isFalse() + } +} diff --git a/app/src/test/java/foundation/e/apps/data/application/ApplicationDataManagerTest.kt b/app/src/test/java/foundation/e/apps/data/application/ApplicationDataManagerTest.kt new file mode 100644 index 000000000..2b3b32831 --- /dev/null +++ b/app/src/test/java/foundation/e/apps/data/application/ApplicationDataManagerTest.kt @@ -0,0 +1,145 @@ +package foundation.e.apps.data.application + +import com.google.common.truth.Truth.assertThat +import foundation.e.apps.FakeAppLoungePackageManager +import foundation.e.apps.data.application.data.Application +import foundation.e.apps.data.enums.FilterLevel +import foundation.e.apps.data.enums.Source +import foundation.e.apps.data.enums.Status +import foundation.e.apps.install.pkg.PwaManager +import foundation.e.apps.data.application.utils.AppVisibilityResolver +import io.mockk.coEvery +import io.mockk.every +import io.mockk.mockk +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test + +@OptIn(ExperimentalCoroutinesApi::class) +class ApplicationDataManagerTest { + + private val visibilityResolver: AppVisibilityResolver = mockk() + private val pwaManager: PwaManager = mockk() + private lateinit var packageManager: FakeAppLoungePackageManager + private lateinit var manager: ApplicationDataManager + + @Before + fun setUp() { + packageManager = FakeAppLoungePackageManager(mockk(relaxed = true)) + manager = ApplicationDataManager(packageManager, pwaManager, visibilityResolver) + } + + @Test + fun getAppFilterLevel_returnsUnknownWhenPackageMissing() = runTest { + val application = Application(package_name = "") + + val level = manager.getAppFilterLevel(application) + + assertThat(level).isEqualTo(FilterLevel.UNKNOWN) + } + + @Test + fun getAppFilterLevel_returnsUiWhenPaidWithNoPrice() = runTest { + val application = Application(package_name = "pkg", isFree = false, price = "") + + val level = manager.getAppFilterLevel(application) + + assertThat(level).isEqualTo(FilterLevel.UI) + } + + @Test + fun getAppFilterLevel_returnsNoneForPwa() = runTest { + val application = Application(package_name = "pkg", source = Source.PWA) + + val level = manager.getAppFilterLevel(application) + + assertThat(level).isEqualTo(FilterLevel.NONE) + } + + @Test + fun getAppFilterLevel_returnsDataWhenInvisible() = runTest { + val restrictedValue = com.aurora.gplayapi.Constants.Restriction.values() + .first { it != com.aurora.gplayapi.Constants.Restriction.NOT_RESTRICTED } + val application = Application( + package_name = "pkg", + restriction = restrictedValue + ) + coEvery { visibilityResolver.isApplicationVisible(application) } returns false + + val level = manager.getAppFilterLevel(application) + + assertThat(level).isEqualTo(FilterLevel.DATA) + } + + @Test + fun getAppFilterLevel_returnsUiWhenSizeUnknown() = runTest { + val restrictedValue = com.aurora.gplayapi.Constants.Restriction.values() + .first { it != com.aurora.gplayapi.Constants.Restriction.NOT_RESTRICTED } + val application = Application( + package_name = "pkg", + restriction = restrictedValue, + originalSize = 0L + ) + coEvery { visibilityResolver.isApplicationVisible(application) } returns true + + val level = manager.getAppFilterLevel(application) + + assertThat(level).isEqualTo(FilterLevel.UI) + } + + @Test + fun updateStatusUsesPackageManagerForNative() { + val application = Application(package_name = "pkg", is_pwa = false) + val stubManager = object : FakeAppLoungePackageManager(mockk(relaxed = true)) { + override fun getPackageStatus(packageName: String, versionCode: Long): Status { + return Status.UPDATABLE + } + } + manager = ApplicationDataManager(stubManager, pwaManager, visibilityResolver) + + manager.updateStatus(application) + + assertThat(application.status).isEqualTo(Status.UPDATABLE) + } + + @Test + fun prepareAppsAddsHomeWhenListPopulated() = runTest { + val list = mutableListOf() + val apps = listOf(Application(package_name = "pkg")) + coEvery { visibilityResolver.isApplicationVisible(any()) } returns true + + manager.prepareApps(apps, list, "title") + + assertThat(list).hasSize(1) + assertThat(list.first().list).contains(apps.first()) + } + + @Test + fun prepareAppsSkipsWhenListEmpty() = runTest { + val list = mutableListOf() + + manager.prepareApps(emptyList(), list, "title") + + assertThat(list).isEmpty() + } + + @Test + fun updateStatusUsesPwaManagerForPwas() { + val application = Application(package_name = "pkg", is_pwa = true) + every { pwaManager.getPwaStatus(application) } returns Status.INSTALLED + + manager.updateStatus(application) + + assertThat(application.status).isEqualTo(Status.INSTALLED) + } + + @Test + fun getAppFilterLevelReturnsNoneForSystemApps() = runTest { + val application = Application(package_name = "pkg", source = Source.SYSTEM_APP) + + val level = manager.getAppFilterLevel(application) + + assertThat(level).isEqualTo(FilterLevel.NONE) + } +} diff --git a/app/src/test/java/foundation/e/apps/data/application/data/ApplicationTest.kt b/app/src/test/java/foundation/e/apps/data/application/data/ApplicationTest.kt new file mode 100644 index 000000000..63e03495c --- /dev/null +++ b/app/src/test/java/foundation/e/apps/data/application/data/ApplicationTest.kt @@ -0,0 +1,77 @@ +package foundation.e.apps.data.application.data + +import com.google.common.truth.Truth.assertThat +import foundation.e.apps.data.enums.Type +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.junit.Test + +@RunWith(RobolectricTestRunner::class) +class ApplicationTest { + + @Test + fun updateTypeSetsPwaWhenFlagged() { + val app = Application(is_pwa = true, type = Type.NATIVE) + + app.updateType() + + assertThat(app.type).isEqualTo(Type.PWA) + } + + @Test + fun updateTypeLeavesNativeWhenNotPwa() { + val app = Application(is_pwa = false, type = Type.PWA) + + app.updateType() + + assertThat(app.type).isEqualTo(Type.NATIVE) + } + + @Test + fun shareUriBuildsFdroidUrlWhenFlagged() { + val app = Application( + package_name = "org.fdroid.app", + isFDroidApp = true, + type = Type.NATIVE + ) + + val uri = app.shareUri + + assertThat(uri.toString()).isEqualTo("https://f-droid.org/packages/org.fdroid.app") + } + + @Test + fun shareUriReturnsShareUrlForNative() { + val app = Application( + package_name = "com.example.app", + shareUrl = "https://example.com/share", + type = Type.NATIVE + ) + + val uri = app.shareUri + + assertThat(uri.toString()).isEqualTo("https://example.com/share") + } + + @Test + fun shareUriReturnsAppUrlForPwa() { + val app = Application( + package_name = "com.pwa.app", + url = "https://pwa.app", + type = Type.PWA + ) + + val uri = app.shareUri + + assertThat(uri.toString()).isEqualTo("https://pwa.app") + } + + @Test + fun hasExodusPrivacyRatingReflectsReportId() { + val app = Application(reportId = 10) + val missing = Application(reportId = -1) + + assertThat(app.hasExodusPrivacyRating()).isTrue() + assertThat(missing.hasExodusPrivacyRating()).isFalse() + } +} diff --git a/app/src/test/java/foundation/e/apps/data/application/home/HomeApiImplTest.kt b/app/src/test/java/foundation/e/apps/data/application/home/HomeApiImplTest.kt new file mode 100644 index 000000000..0c5c05748 --- /dev/null +++ b/app/src/test/java/foundation/e/apps/data/application/home/HomeApiImplTest.kt @@ -0,0 +1,99 @@ +package foundation.e.apps.data.application.home + +import com.google.common.truth.Truth.assertThat +import foundation.e.apps.data.ResultSupreme +import foundation.e.apps.data.StoreRepository +import foundation.e.apps.data.Stores +import foundation.e.apps.data.application.ApplicationRepository +import foundation.e.apps.data.application.data.Application +import foundation.e.apps.data.application.data.Home +import foundation.e.apps.data.application.search.SearchSuggestion +import foundation.e.apps.data.enums.Source +import io.mockk.every +import io.mockk.mockk +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.take +import kotlinx.coroutines.flow.toList +import kotlinx.coroutines.test.runTest +import androidx.lifecycle.asFlow +import foundation.e.apps.util.MainCoroutineRule +import org.junit.Rule +import org.junit.Before +import org.junit.Test +import androidx.arch.core.executor.testing.InstantTaskExecutorRule + +@OptIn(ExperimentalCoroutinesApi::class) +class HomeApiImplTest { + + @get:Rule + val mainCoroutineRule = MainCoroutineRule() + + @get:Rule + val instantExecutorRule = InstantTaskExecutorRule() + + private val playStoreHome = Home(title = "Play", list = listOf(Application(name = "p1"))) + private val ossHome = Home( + title = "OSS", + list = listOf(Application(name = "o1", source = Source.OPEN_SOURCE)), + source = ApplicationRepository.APP_TYPE_OPEN + ) + + private lateinit var homeApi: HomeApiImpl + private val stores: Stores = mockk() + + @Before + fun setUp() { + homeApi = HomeApiImpl(stores) + } + + @Test + fun fetchHomeScreenDataEmitsSortedSources() = runTest { + val repositories = mapOf( + Source.PLAY_STORE to fakeStore(playStoreHome), + Source.OPEN_SOURCE to fakeStore(ossHome) + ) + every { stores.getStores() } returns repositories + every { stores.getStore(Source.PLAY_STORE) } returns repositories.getValue(Source.PLAY_STORE) + every { stores.getStore(Source.OPEN_SOURCE) } returns repositories.getValue(Source.OPEN_SOURCE) + + val emissions = homeApi.fetchHomeScreenData().asFlow().take(2).toList() + + assertThat(emissions).hasSize(2) + val finalList = emissions.last().data!! + assertThat(finalList.map { it.source }).containsExactly("", ApplicationRepository.APP_TYPE_OPEN).inOrder() + } + + @Test + fun fetchHomeScreenDataSetsErrorMessage() = runTest { + val failingStore = object : StoreRepository { + override suspend fun getHomeScreenData(list: MutableList): List { + throw IllegalStateException("boom") + } + override suspend fun getAppDetails(packageName: String) = Application() + override suspend fun getSearchResults(pattern: String) = emptyList() + override suspend fun getSearchSuggestions(pattern: String) = emptyList() + } + val repositories = mapOf(Source.PLAY_STORE to failingStore) + every { stores.getStores() } returns repositories + every { stores.getStore(Source.PLAY_STORE) } returns failingStore + + val emissions = homeApi.fetchHomeScreenData().asFlow().take(1).toList() + + val result = emissions.single() + assertThat(result.isUnknownError()).isTrue() + assertThat(result.message).contains("boom") + } + + private fun fakeStore(home: Home) = object : StoreRepository { + override suspend fun getHomeScreenData(list: MutableList): List { + list.add(home) + return list + } + + override suspend fun getAppDetails(packageName: String): Application = Application() + + override suspend fun getSearchResults(pattern: String): List = emptyList() + + override suspend fun getSearchSuggestions(pattern: String): List = emptyList() + } +} diff --git a/app/src/test/java/foundation/e/apps/data/application/utils/BinaryToDecimalSizeConverterKtTest.kt b/app/src/test/java/foundation/e/apps/data/application/utils/BinaryToDecimalSizeConverterTest.kt similarity index 55% rename from app/src/test/java/foundation/e/apps/data/application/utils/BinaryToDecimalSizeConverterKtTest.kt rename to app/src/test/java/foundation/e/apps/data/application/utils/BinaryToDecimalSizeConverterTest.kt index 02a287732..1d609f87a 100644 --- a/app/src/test/java/foundation/e/apps/data/application/utils/BinaryToDecimalSizeConverterKtTest.kt +++ b/app/src/test/java/foundation/e/apps/data/application/utils/BinaryToDecimalSizeConverterTest.kt @@ -20,41 +20,41 @@ package foundation.e.apps.data.application.utils import junit.framework.TestCase.assertEquals import org.junit.Test - +import java.util.Locale class BinaryToDecimalSizeConverterTest { @Test fun `test valid MiB to MB conversion`() { - assertEquals("1.05 MB", convertBinaryToDecimal("1 MiB")) - assertEquals("2.10 MB", convertBinaryToDecimal("2 MiB")) + assertEquals("1.05 MB", convertBinaryToDecimal("1 MiB", Locale.US)) + assertEquals("2.10 MB", convertBinaryToDecimal("2 MiB", Locale.US)) } @Test fun `test valid KiB to KB conversion`() { - assertEquals("1.02 KB", convertBinaryToDecimal("1 KiB")) - assertEquals("1048.58 KB", convertBinaryToDecimal("1024 KiB")) + assertEquals("1.02 KB", convertBinaryToDecimal("1 KiB", Locale.US)) + assertEquals("1048.58 KB", convertBinaryToDecimal("1024 KiB", Locale.US)) } @Test fun `test valid GiB to GB conversion`() { - assertEquals("1.07 GB", convertBinaryToDecimal("1 GiB")) - assertEquals("2.15 GB", convertBinaryToDecimal("2 GiB")) + assertEquals("1.07 GB", convertBinaryToDecimal("1 GiB", Locale.US)) + assertEquals("2.15 GB", convertBinaryToDecimal("2 GiB", Locale.US)) } @Test fun `test unknown unit`() { - assertEquals("", convertBinaryToDecimal("1 TiB")) // Tebibyte - assertEquals("", convertBinaryToDecimal("1 PiB")) // Pebibyte + assertEquals("", convertBinaryToDecimal("1 TiB", Locale.US)) // Tebibyte + assertEquals("", convertBinaryToDecimal("1 PiB", Locale.US)) // Pebibyte } @Test fun `test edge cases with zero and negative values`() { - assertEquals("0.00 MB", convertBinaryToDecimal("0 MiB")) - assertEquals("0.00 KB", convertBinaryToDecimal("0 KiB")) - assertEquals("0.00 GB", convertBinaryToDecimal("0 GiB")) + assertEquals("0.00 MB", convertBinaryToDecimal("0 MiB", Locale.US)) + assertEquals("0.00 KB", convertBinaryToDecimal("0 KiB", Locale.US)) + assertEquals("0.00 GB", convertBinaryToDecimal("0 GiB", Locale.US)) - assertEquals("", convertBinaryToDecimal("-1 MiB")) // Negative value - assertEquals("", convertBinaryToDecimal("-1024 KiB")) // Negative value + assertEquals("", convertBinaryToDecimal("-1 MiB", Locale.US)) // Negative value + assertEquals("", convertBinaryToDecimal("-1024 KiB", Locale.US)) // Negative value } } diff --git a/app/src/test/java/foundation/e/apps/data/application/utils/CategoryUtilsTest.kt b/app/src/test/java/foundation/e/apps/data/application/utils/CategoryUtilsTest.kt new file mode 100644 index 000000000..c41e966f8 --- /dev/null +++ b/app/src/test/java/foundation/e/apps/data/application/utils/CategoryUtilsTest.kt @@ -0,0 +1,66 @@ +package foundation.e.apps.data.application.utils + +import com.google.common.truth.Truth.assertThat +import foundation.e.apps.R +import foundation.e.apps.data.application.data.Category +import foundation.e.apps.data.cleanapk.data.categories.Categories +import foundation.e.apps.data.enums.AppTag +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner + +@RunWith(RobolectricTestRunner::class) +class CategoryUtilsTest { + + @Test + fun provideAppsCategoryIconResource_returnsDefaultForUnknown() { + val icon = CategoryUtils.provideAppsCategoryIconResource("unknown") + + assertThat(icon).isEqualTo(R.drawable.ic_cat_default) + } + + @Test + fun getCategories_usesTranslationsAndIcons() { + val categories = Categories( + translations = mapOf("comics" to "Comics", "game_open_games" to "ignored") + ) + + val result = CategoryUtils.getCategories( + categories, + listOf("comics", "game_open_games"), + AppTag.OpenSource("Open") + ) + + assertThat(result[0].title).isEqualTo("Comics") + assertThat(result[0].drawable).isEqualTo(R.drawable.ic_cat_comics) + assertThat(result[1].title).isEqualTo("Open games") + } + + @Test + fun getCategoryIconName_replacesConjunctionAndSpaces() { + val category = Category( + id = "food & drink", + title = "food & drink", + drawable = 0, + tag = AppTag.PWA("PWA") + ) + + val iconName = CategoryUtils.getCategoryIconName(category) + + assertThat(iconName).isEqualTo("food_and_drink") + } + + @Test + fun getCategoryIconName_usesIdForGPlayTags() { + val category = Category( + id = "Maps & Navigation", + title = "Fancy Title", + drawable = 0, + tag = AppTag.GPlay() + ) + + val iconName = CategoryUtils.getCategoryIconName(category) + + assertThat(iconName).isEqualTo("maps_and_navigation") + } +} diff --git a/app/src/test/java/foundation/e/apps/data/blockedApps/BlockedAppRepositoryTest.kt b/app/src/test/java/foundation/e/apps/data/blockedApps/BlockedAppRepositoryTest.kt new file mode 100644 index 000000000..985237808 --- /dev/null +++ b/app/src/test/java/foundation/e/apps/data/blockedApps/BlockedAppRepositoryTest.kt @@ -0,0 +1,82 @@ +package foundation.e.apps.data.blockedApps + +import com.google.common.truth.Truth.assertThat +import foundation.e.apps.data.DownloadManager +import io.mockk.every +import io.mockk.mockk +import java.io.File +import java.nio.file.Files +import kotlinx.coroutines.test.runTest +import kotlinx.serialization.json.Json +import org.junit.After +import org.junit.Test + +class BlockedAppRepositoryTest { + + private val tempDir = Files.createTempDirectory("blockedAppsTest").toFile() + private val warningFileName = "app-lounge-warning-list.json" + + @After + fun tearDown() { + tempDir.deleteRecursively() + } + + @Test + fun fetchUpdateOfAppWarningList_parsesDownloadedFile() = runTest { + val downloadManager = mockk(relaxed = true) + every { + downloadManager.downloadFileInCache(any(), any(), any(), any()) + } answers { + File(tempDir, warningFileName).writeText( + """ + { + "not_working_apps": ["bad.app"], + "zero_privacy_apps": ["zero.app"], + "third_party_store_apps": ["store.app"] + } + """.trimIndent() + ) + val callback = arg<((Boolean, String) -> Unit)?>(3) + callback?.invoke(true, "") + 1L + } + + val repository = BlockedAppRepository(downloadManager, Json, tempDir.absolutePath) + + repository.fetchUpdateOfAppWarningList() + + assertThat(repository.isBlockedApp("bad.app")).isTrue() + assertThat(repository.isPrivacyScoreZero("zero.app")).isTrue() + assertThat(repository.isThirdPartyStoreApp("store.app")).isTrue() + } + + @Test + fun fetchUpdateOfAppWarningList_handlesInvalidJsonGracefully() = runTest { + val downloadManager = mockk(relaxed = true) + every { + downloadManager.downloadFileInCache(any(), any(), any(), any()) + } answers { + File(tempDir, warningFileName).writeText("{invalid") + val callback = arg<((Boolean, String) -> Unit)?>(3) + callback?.invoke(true, "") + 1L + } + + val repository = BlockedAppRepository(downloadManager, Json, tempDir.absolutePath) + + repository.fetchUpdateOfAppWarningList() + + assertThat(repository.getBlockedAppPackages()).isEmpty() + assertThat(repository.isBlockedApp("anything")).isFalse() + } + + @Test + fun defaultState_hasNoBlockedApps() { + val repository = BlockedAppRepository(mockk(relaxed = true), Json, tempDir.absolutePath) + + assertThat(repository.getBlockedAppPackages()).isEmpty() + assertThat(repository.isBlockedApp("package")).isFalse() + assertThat(repository.isPrivacyScoreZero("package")).isFalse() + assertThat(repository.isThirdPartyStoreApp("package")).isFalse() + } +} diff --git a/app/src/test/java/foundation/e/apps/data/cleanapk/ApplicationDeserializerTest.kt b/app/src/test/java/foundation/e/apps/data/cleanapk/ApplicationDeserializerTest.kt new file mode 100644 index 000000000..cb18b8354 --- /dev/null +++ b/app/src/test/java/foundation/e/apps/data/cleanapk/ApplicationDeserializerTest.kt @@ -0,0 +1,54 @@ +package foundation.e.apps.data.cleanapk + +import com.google.common.truth.Truth.assertThat +import com.google.gson.JsonParser +import org.junit.Test + +class ApplicationDeserializerTest { + + private val deserializer = ApplicationDeserializer() + + @Test + fun deserializeUsesLatestVersionBlock() { + val json = """ + { + "success": true, + "app": { + "name": "Sample app", + "latest_downloaded_version": "1.2.3", + "latest_version_number": "1.2.3", + "1.2.3": { + "update_on": "2024-01-01", + "version_code": 42, + "apk_file_size": "2 MiB" + } + } + } + """.trimIndent() + + val cleanApkApp = deserializer.deserialize(JsonParser.parseString(json), null, null) + + assertThat(cleanApkApp.app.updatedOn).isEqualTo("2024-01-01") + assertThat(cleanApkApp.app.latest_version_code).isEqualTo(42) + assertThat(cleanApkApp.app.appSize).isEqualTo("2.10 MB") + assertThat(cleanApkApp.app.latest_downloaded_version).isEqualTo("1.2.3") + } + + @Test + fun deserializeHandlesMissingVersionData() { + val json = """ + { + "app": { + "name": "Minimal app", + "latest_downloaded_version": "0.0.1" + } + } + """.trimIndent() + + val cleanApkApp = deserializer.deserialize(JsonParser.parseString(json), null, null) + + assertThat(cleanApkApp.app.updatedOn).isEqualTo("") + assertThat(cleanApkApp.app.latest_version_code).isEqualTo(-1) + assertThat(cleanApkApp.app.appSize).isEqualTo("") + } +} diff --git a/app/src/test/java/foundation/e/apps/data/cleanapk/CleanApkSearchHelperTest.kt b/app/src/test/java/foundation/e/apps/data/cleanapk/CleanApkSearchHelperTest.kt new file mode 100644 index 000000000..4f232d879 --- /dev/null +++ b/app/src/test/java/foundation/e/apps/data/cleanapk/CleanApkSearchHelperTest.kt @@ -0,0 +1,49 @@ +package foundation.e.apps.data.cleanapk + +import com.google.common.truth.Truth.assertThat +import foundation.e.apps.data.application.data.Application +import foundation.e.apps.data.cleanapk.data.search.Search +import foundation.e.apps.data.enums.Source +import io.mockk.coEvery +import io.mockk.mockk +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test +import retrofit2.Response + +@OptIn(ExperimentalCoroutinesApi::class) +class CleanApkSearchHelperTest { + + private val retrofit: CleanApkRetrofit = mockk() + private lateinit var helper: CleanApkSearchHelper + + @Before + fun setUp() { + helper = CleanApkSearchHelper(retrofit) + } + + @Test + fun getSearchResultsMapsPwaAndOpenSource() = runTest { + val pwa = Application(package_name = "pwa.app", is_pwa = true) + val oss = Application(package_name = "oss.app", is_pwa = false) + val search = Search(apps = listOf(pwa, oss), success = true) + coEvery { retrofit.searchApps(any(), any(), any(), any(), any()) } returns Response.success(search) + + val results = helper.getSearchResults("k", CleanApkRetrofit.APP_SOURCE_ANY, CleanApkRetrofit.APP_TYPE_ANY) + + val mappedPwa = results.first { it.package_name == "pwa.app" } + val mappedOss = results.first { it.package_name == "oss.app" } + assertThat(mappedPwa.source).isEqualTo(Source.PWA) + assertThat(mappedOss.source).isEqualTo(Source.OPEN_SOURCE) + } + + @Test + fun getSearchResultsReturnsEmptyWhenBodyNull() = runTest { + coEvery { retrofit.searchApps(any(), any(), any(), any(), any()) } returns Response.success(null) + + val results = helper.getSearchResults("k", CleanApkRetrofit.APP_SOURCE_ANY, CleanApkRetrofit.APP_TYPE_ANY) + + assertThat(results).isEmpty() + } +} diff --git a/app/src/test/java/foundation/e/apps/data/cleanapk/repositories/HomeConverterTest.kt b/app/src/test/java/foundation/e/apps/data/cleanapk/repositories/HomeConverterTest.kt new file mode 100644 index 000000000..afd64fcd2 --- /dev/null +++ b/app/src/test/java/foundation/e/apps/data/cleanapk/repositories/HomeConverterTest.kt @@ -0,0 +1,76 @@ +package foundation.e.apps.data.cleanapk.repositories + +import com.google.common.truth.Truth.assertThat +import foundation.e.apps.data.application.ApplicationDataManager +import foundation.e.apps.data.application.data.Application +import foundation.e.apps.data.application.data.Home +import foundation.e.apps.data.cleanapk.data.home.CleanApkHome +import io.mockk.coJustRun +import io.mockk.coEvery +import io.mockk.every +import io.mockk.mockk +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test + +class HomeConverterTest { + + private lateinit var converter: HomeConverter + private val dataManager: ApplicationDataManager = mockk() + private val context = mockk(relaxed = true) + + @Before + fun setUp() { + // Return the key for readability + coEvery { dataManager.prepareApps(any(), any(), any()) } answers { + val apps = firstArg>() + val homes = secondArg>() + val title = thirdArg() + if (apps.isNotEmpty()) { + homes.add(Home(title, apps)) + } + } + every { context.getString(any()) } answers { "label_${firstArg()}" } + converter = HomeConverter(context, dataManager) + } + + @Test + fun toGenericHomeMapsTopUpdatedAndPopular() = runTestWithHome( + CleanApkHome( + top_updated_apps = listOf(Application(name = "updated app")), + popular_apps = listOf(Application(name = "popular app")) + ) + ) { result -> + assertThat(result).hasSize(2) + assertThat(result[0].list).isNotEmpty() + assertThat(result[1].list).isNotEmpty() + } + + @Test + fun toGenericHomeSetsSourceField() = runTestWithHome( + CleanApkHome( + popular_games = listOf(Application(name = "game")), + discover = emptyList() + ) + ) { result -> + assertThat(result.all { it.source == "oss" }).isTrue() + } + + @Test + fun toGenericHomeSkipsEmptyBuckets() = runTestWithHome( + CleanApkHome( + top_updated_apps = emptyList(), + discover = emptyList() + ) + ) { result -> + assertThat(result).isEmpty() + } + + private fun runTestWithHome( + home: CleanApkHome, + block: (List) -> Unit + ) = runTest { + val result = converter.toGenericHome(home, "oss") + block(result) + } +} diff --git a/app/src/test/java/foundation/e/apps/data/database/install/AppInstallConverterTest.kt b/app/src/test/java/foundation/e/apps/data/database/install/AppInstallConverterTest.kt new file mode 100644 index 000000000..acf4acaf0 --- /dev/null +++ b/app/src/test/java/foundation/e/apps/data/database/install/AppInstallConverterTest.kt @@ -0,0 +1,53 @@ +package foundation.e.apps.data.database.install + +import com.aurora.gplayapi.data.models.ContentRating +import com.aurora.gplayapi.data.models.PlayFile +import com.google.common.truth.Truth.assertThat +import foundation.e.apps.data.database.install.AppInstallConverter +import org.junit.Test + +class AppInstallConverterTest { + + private val converter = AppInstallConverter() + + @Test + fun stringListRoundTrip_isPreserved() { + val original = listOf("a", "b", "c") + + val json = converter.listToJsonString(original) + val restored = converter.jsonStringToList(json) + + assertThat(restored).containsExactlyElementsIn(original) + } + + @Test + fun mapRoundTrip_isPreserved() { + val original = mutableMapOf(1L to true, 2L to false) + + val json = converter.listToJsonLong(original) + val restored = converter.jsonLongToList(json) + + assertThat(restored).isEqualTo(original) + } + + @Test + fun playFileListRoundTrip_isPreserved() { + val files = listOf(PlayFile()) + + val json = converter.filesToJsonString(files) + val restored = converter.jsonStringToFiles(json) + + assertThat(restored).hasSize(1) + } + + @Test + fun contentRatingRoundTrip_isPreserved() { + val rating = ContentRating(id = "E", title = "Everyone") + + val json = converter.fromContentRating(rating) + val restored = converter.toContentRating(json) + + assertThat(restored.id).isEqualTo("E") + assertThat(restored.title).isEqualTo("Everyone") + } +} diff --git a/app/src/test/java/foundation/e/apps/data/enums/AppTagTest.kt b/app/src/test/java/foundation/e/apps/data/enums/AppTagTest.kt new file mode 100644 index 000000000..7d8f42ac9 --- /dev/null +++ b/app/src/test/java/foundation/e/apps/data/enums/AppTagTest.kt @@ -0,0 +1,14 @@ +package foundation.e.apps.data.enums + +import com.google.common.truth.Truth.assertThat +import org.junit.Test + +class AppTagTest { + + @Test + fun getOperationalTagMatchesLegacyStrings() { + assertThat(AppTag.OpenSource("Libre").getOperationalTag()).isEqualTo("Open Source") + assertThat(AppTag.PWA("Web").getOperationalTag()).isEqualTo("PWA") + assertThat(AppTag.GPlay().getOperationalTag()).isEqualTo("GPlay") + } +} diff --git a/app/src/test/java/foundation/e/apps/data/enums/FilterLevelTest.kt b/app/src/test/java/foundation/e/apps/data/enums/FilterLevelTest.kt new file mode 100644 index 000000000..97fe0b822 --- /dev/null +++ b/app/src/test/java/foundation/e/apps/data/enums/FilterLevelTest.kt @@ -0,0 +1,23 @@ +package foundation.e.apps.data.enums + +import com.google.common.truth.Truth.assertThat +import org.junit.Test + +class FilterLevelTest { + + @Test + fun isUnFilteredOnlyForNone() { + assertThat(FilterLevel.NONE.isUnFiltered()).isTrue() + assertThat(FilterLevel.UI.isUnFiltered()).isFalse() + assertThat(FilterLevel.DATA.isUnFiltered()).isFalse() + assertThat(FilterLevel.UNKNOWN.isUnFiltered()).isFalse() + } + + @Test + fun isInitializedExcludesUnknown() { + assertThat(FilterLevel.UNKNOWN.isInitialized()).isFalse() + assertThat(FilterLevel.NONE.isInitialized()).isTrue() + assertThat(FilterLevel.UI.isInitialized()).isTrue() + assertThat(FilterLevel.DATA.isInitialized()).isTrue() + } +} diff --git a/app/src/test/java/foundation/e/apps/data/enums/SourceTest.kt b/app/src/test/java/foundation/e/apps/data/enums/SourceTest.kt new file mode 100644 index 000000000..439a5c3b1 --- /dev/null +++ b/app/src/test/java/foundation/e/apps/data/enums/SourceTest.kt @@ -0,0 +1,23 @@ +package foundation.e.apps.data.enums + +import com.google.common.truth.Truth.assertThat +import org.junit.Test + +class SourceTest { + + @Test + fun toStringFormatsNicely() { + assertThat(Source.OPEN_SOURCE.toString()).isEqualTo("Open Source") + assertThat(Source.PWA.toString()).isEqualTo("Pwa") + assertThat(Source.SYSTEM_APP.toString()).isEqualTo("System App") + assertThat(Source.PLAY_STORE.toString()).isEqualTo("") + } + + @Test + fun fromStringParsesKnownValues() { + assertThat(Source.fromString("Open Source")).isEqualTo(Source.OPEN_SOURCE) + assertThat(Source.fromString("PWA")).isEqualTo(Source.PWA) + assertThat(Source.fromString("SYSTEM_APP")).isEqualTo(Source.SYSTEM_APP) + assertThat(Source.fromString("unknown")).isEqualTo(Source.PLAY_STORE) + } +} diff --git a/app/src/test/java/foundation/e/apps/data/install/FileManagerTest.kt b/app/src/test/java/foundation/e/apps/data/install/FileManagerTest.kt new file mode 100644 index 000000000..3087322a9 --- /dev/null +++ b/app/src/test/java/foundation/e/apps/data/install/FileManagerTest.kt @@ -0,0 +1,50 @@ +package foundation.e.apps.data.install + +import com.google.common.truth.Truth.assertThat +import java.io.File +import java.nio.file.Files +import org.junit.After +import org.junit.Test + +class FileManagerTest { + + private val tempDir = Files.createTempDirectory("fileManagerTest").toFile() + + @After + fun tearDown() { + tempDir.deleteRecursively() + } + + @Test + fun moveFile_movesFileAndDeletesOriginal() { + val inputDir = File(tempDir, "input").apply { mkdirs() } + val outputDir = File(tempDir, "output") + val fileName = "sample.txt" + val originalFile = File(inputDir, fileName) + originalFile.writeText("content") + + FileManager.moveFile( + inputDir.absolutePath + File.separator, + fileName, + outputDir.absolutePath + File.separator + ) + + val movedFile = File(outputDir, fileName) + assertThat(movedFile.exists()).isTrue() + assertThat(movedFile.readText()).isEqualTo("content") + assertThat(originalFile.exists()).isFalse() + } + + @Test + fun moveFile_ignoresMissingSourceFile() { + val outputDir = File(tempDir, "missingOutput") + + FileManager.moveFile( + File(tempDir, "missingInput").absolutePath + File.separator, + "absent.txt", + outputDir.absolutePath + File.separator + ) + + assertThat(File(outputDir, "absent.txt").exists()).isFalse() + } +} diff --git a/app/src/test/java/foundation/e/apps/data/install/models/AppInstallTest.kt b/app/src/test/java/foundation/e/apps/data/install/models/AppInstallTest.kt new file mode 100644 index 000000000..43384b792 --- /dev/null +++ b/app/src/test/java/foundation/e/apps/data/install/models/AppInstallTest.kt @@ -0,0 +1,50 @@ +package foundation.e.apps.data.install.models + +import com.google.common.truth.Truth.assertThat +import foundation.e.apps.data.cleanapk.CleanApkRetrofit +import foundation.e.apps.data.enums.Source +import foundation.e.apps.data.enums.Status +import org.junit.Test + +class AppInstallTest { + + @Test + fun isAppInstalling_matchesDownloadingStates() { + val installing = AppInstall(status = Status.DOWNLOADING) + val notInstalling = AppInstall(status = Status.BLOCKED) + + assertThat(installing.isAppInstalling()).isTrue() + assertThat(notInstalling.isAppInstalling()).isFalse() + } + + @Test + fun areFilesDownloaded_checksAllIdsComplete() { + val app = AppInstall( + downloadIdMap = mutableMapOf(1L to true, 2L to false) + ) + + assertThat(app.areFilesDownloaded()).isFalse() + + app.downloadIdMap[2L] = true + + assertThat(app.areFilesDownloaded()).isTrue() + } + + @Test + fun isAwaiting_returnsTrueForAwaitingStatus() { + val app = AppInstall(status = Status.AWAITING) + + assertThat(app.isAwaiting()).isTrue() + } + + @Test + fun getAppIconUrl_usesAssetUrlForPlayStoreAndPwa() { + val playStoreApp = AppInstall(source = Source.PLAY_STORE, iconImageUrl = "icon.png") + val pwaApp = AppInstall(source = Source.PWA, iconImageUrl = "icon2.png") + val fossApp = AppInstall(source = Source.OPEN_SOURCE, iconImageUrl = "https://example/icon") + + assertThat(playStoreApp.getAppIconUrl()).isEqualTo("${CleanApkRetrofit.ASSET_URL}icon.png") + assertThat(pwaApp.getAppIconUrl()).isEqualTo("${CleanApkRetrofit.ASSET_URL}icon2.png") + assertThat(fossApp.getAppIconUrl()).isEqualTo("https://example/icon") + } +} diff --git a/app/src/test/java/foundation/e/apps/data/login/LoginCommonTest.kt b/app/src/test/java/foundation/e/apps/data/login/LoginCommonTest.kt new file mode 100644 index 000000000..8b9050baf --- /dev/null +++ b/app/src/test/java/foundation/e/apps/data/login/LoginCommonTest.kt @@ -0,0 +1,44 @@ +package foundation.e.apps.data.login + +import foundation.e.apps.data.enums.User +import foundation.e.apps.data.preference.AppLoungeDataStore +import foundation.e.apps.data.preference.AppLoungePreference +import io.mockk.coVerify +import io.mockk.mockk +import kotlinx.coroutines.test.runTest +import org.junit.Test + +class LoginCommonTest { + + private val dataStore: AppLoungeDataStore = mockk(relaxed = true) + private val preference: AppLoungePreference = mockk(relaxed = true) + private val loginCommon = LoginCommon(dataStore, preference) + + @Test + fun saveUserType_delegatesToDataStore() = runTest { + loginCommon.saveUserType(User.ANONYMOUS) + + coVerify { dataStore.saveUserType(User.ANONYMOUS) } + } + + @Test + fun setNoGoogleMode_updatesPreferencesAndUser() = runTest { + loginCommon.setNoGoogleMode() + + coVerify { preference.disablePlayStore() } + coVerify { preference.enableOpenSource() } + coVerify { preference.enablePwa() } + coVerify { dataStore.saveUserType(User.NO_GOOGLE) } + } + + @Test + fun logout_clearsCredentialsAndResetsSources() = runTest { + loginCommon.logout() + + coVerify { dataStore.destroyCredentials() } + coVerify { dataStore.saveUserType(null) } + coVerify { preference.enableOpenSource() } + coVerify { preference.enablePwa() } + coVerify { preference.enablePlayStore() } + } +} diff --git a/app/src/test/java/foundation/e/apps/data/login/api/PlayStoreLoginWrapperTest.kt b/app/src/test/java/foundation/e/apps/data/login/api/PlayStoreLoginWrapperTest.kt new file mode 100644 index 000000000..8e58ba268 --- /dev/null +++ b/app/src/test/java/foundation/e/apps/data/login/api/PlayStoreLoginWrapperTest.kt @@ -0,0 +1,40 @@ +package foundation.e.apps.data.login.api + +import com.aurora.gplayapi.data.models.AuthData +import com.aurora.gplayapi.data.models.PlayResponse +import com.google.common.truth.Truth.assertThat +import foundation.e.apps.data.ResultSupreme +import foundation.e.apps.data.enums.User +import io.mockk.coEvery +import io.mockk.mockk +import java.util.Locale +import kotlinx.coroutines.test.runTest +import org.junit.Test + +class PlayStoreLoginWrapperTest { + + private val loginManager: PlayStoreLoginManager = mockk() + + @Test + fun login_setsLocaleOnSuccess() = runTest { + val authData = AuthData("", "") + coEvery { loginManager.login() } returns authData + val wrapper = PlayStoreLoginWrapper(loginManager, User.GOOGLE) + + val result = wrapper.login(Locale.FRANCE) + + assertThat(result).isInstanceOf(ResultSupreme.Success::class.java) + assertThat((result as ResultSupreme.Success).data?.locale).isEqualTo(Locale.FRANCE) + } + + @Test + fun validate_returnsErrorWhenStatusNotOk() = runTest { + val authData = AuthData("", "") + coEvery { loginManager.validate(authData) } throws Exception("Validation network code: 500") + val wrapper = PlayStoreLoginWrapper(loginManager, User.ANONYMOUS) + + val result = wrapper.validate(authData) + + assertThat(result).isInstanceOf(ResultSupreme.Error::class.java) + } +} diff --git a/app/src/test/java/foundation/e/apps/data/parentalcontrol/fdroid/FDroidAntiFeatureRepositoryTest.kt b/app/src/test/java/foundation/e/apps/data/parentalcontrol/fdroid/FDroidAntiFeatureRepositoryTest.kt new file mode 100644 index 000000000..871430fbd --- /dev/null +++ b/app/src/test/java/foundation/e/apps/data/parentalcontrol/fdroid/FDroidAntiFeatureRepositoryTest.kt @@ -0,0 +1,44 @@ +package foundation.e.apps.data.parentalcontrol.fdroid + +import com.google.common.truth.Truth.assertThat +import foundation.e.apps.data.parentalcontrol.ContentRatingDao +import foundation.e.apps.data.parentalcontrol.FDroidNsfwApp +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.mockk +import kotlinx.coroutines.test.runTest +import retrofit2.Response +import org.junit.Test + +class FDroidAntiFeatureRepositoryTest { + + private val monitorApi: FDroidMonitorApi = mockk() + private val contentRatingDao: ContentRatingDao = mockk(relaxed = true) + private val repository = FDroidAntiFeatureRepository(monitorApi, contentRatingDao) + + @Test + fun fetchNsfwApps_usesApiDataWhenAvailable() = runTest { + coEvery { monitorApi.getMonitorData() } returns Response.success( + FDroidMonitorData(antiFeatures = AntiFeatures(NSFW(listOf("nsfw.app")))) + ) + + repository.fetchNsfwApps() + + assertThat(repository.fDroidNsfwApps).containsExactly("nsfw.app") + coVerify { contentRatingDao.insertFDroidNsfwApp(listOf(FDroidNsfwApp("nsfw.app"))) } + } + + @Test + fun fetchNsfwApps_fallsBackToDatabaseWhenApiEmpty() = runTest { + coEvery { monitorApi.getMonitorData() } returns Response.success( + FDroidMonitorData(antiFeatures = AntiFeatures(NSFW(emptyList()))) + ) + coEvery { contentRatingDao.getAllFDroidNsfwApp() } returns listOf( + FDroidNsfwApp("cached.app") + ) + + repository.fetchNsfwApps() + + assertThat(repository.fDroidNsfwApps).containsExactly("cached.app") + } +} diff --git a/app/src/test/java/foundation/e/apps/data/parentalcontrol/googleplay/GPlayContentRatingRepositoryTest.kt b/app/src/test/java/foundation/e/apps/data/parentalcontrol/googleplay/GPlayContentRatingRepositoryTest.kt new file mode 100644 index 000000000..79a56a315 --- /dev/null +++ b/app/src/test/java/foundation/e/apps/data/parentalcontrol/googleplay/GPlayContentRatingRepositoryTest.kt @@ -0,0 +1,56 @@ +package foundation.e.apps.data.parentalcontrol.googleplay + +import com.aurora.gplayapi.data.models.ContentRating +import com.google.common.truth.Truth.assertThat +import foundation.e.apps.data.parentalcontrol.ContentRatingDao +import foundation.e.apps.data.playstore.PlayStoreRepository +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.mockk +import kotlinx.coroutines.test.runTest +import retrofit2.Response +import org.junit.Test + +class GPlayContentRatingRepositoryTest { + + private val ageGroupApi: AgeGroupApi = mockk() + private val playStoreRepository: PlayStoreRepository = mockk() + private val contentRatingDao: ContentRatingDao = mockk(relaxed = true) + private val repository = GPlayContentRatingRepository( + ageGroupApi, + playStoreRepository, + contentRatingDao + ) + + @Test + fun fetchContentRatingData_prefersApiResults() = runTest { + val groups = listOf(GPlayContentRatingGroup("SIX", "6", listOf("e"))) + coEvery { ageGroupApi.getDefinedAgeGroups() } returns Response.success(groups) + + repository.fetchContentRatingData() + + assertThat(repository.contentRatingGroups).containsExactlyElementsIn(groups) + coVerify { contentRatingDao.insertContentRatingGroups(groups) } + } + + @Test + fun fetchContentRatingData_fallsBackToDatabase() = runTest { + val cached = listOf(GPlayContentRatingGroup("TEN", "10", listOf("t"))) + coEvery { ageGroupApi.getDefinedAgeGroups() } returns Response.success(emptyList()) + coEvery { contentRatingDao.getAllContentRatingGroups() } returns cached + + repository.fetchContentRatingData() + + assertThat(repository.contentRatingGroups).containsExactlyElementsIn(cached) + } + + @Test + fun getEnglishContentRating_returnsPlayStoreValue() = runTest { + val contentRating = ContentRating(id = "E", title = "Everyone") + coEvery { playStoreRepository.getEnglishContentRating("pkg") } returns contentRating + + val result = repository.getEnglishContentRating("pkg") + + assertThat(result?.id).isEqualTo("E") + } +} diff --git a/app/src/test/java/foundation/e/apps/data/playstore/utils/AC2DMTaskTest.kt b/app/src/test/java/foundation/e/apps/data/playstore/utils/AC2DMTaskTest.kt new file mode 100644 index 000000000..c9a4b8272 --- /dev/null +++ b/app/src/test/java/foundation/e/apps/data/playstore/utils/AC2DMTaskTest.kt @@ -0,0 +1,57 @@ +package foundation.e.apps.data.playstore.utils + +import com.aurora.gplayapi.data.models.PlayResponse +import com.google.common.truth.Truth.assertThat +import io.mockk.confirmVerified +import io.mockk.every +import io.mockk.mockk +import io.mockk.slot +import io.mockk.verify +import okhttp3.RequestBody +import okio.Buffer +import org.junit.Test + +class AC2DMTaskTest { + + private val client: GPlayHttpClient = mockk() + private val task = AC2DMTask(client) + + @Test + fun returnsEmptyResponseWhenMissingCredentials() { + val response = task.getAC2DMResponse(null, "token") + + assertThat(response).isInstanceOf(PlayResponse::class.java) + verify(exactly = 0) { client.post(any(), any>(), any()) } + } + + @Test + fun returnsEmptyResponseWhenTokenMissing() { + val response = task.getAC2DMResponse("user@e.email", null) + + assertThat(response).isInstanceOf(PlayResponse::class.java) + verify(exactly = 0) { client.post(any(), any>(), any()) } + } + + @Test + fun postIsInvokedWithExpectedBody() { + val bodySlot = slot() + every { client.post(any(), any>(), capture(bodySlot)) } returns PlayResponse() + + task.getAC2DMResponse("user@e.email", "token123") + + verify(exactly = 1) { + client.post( + "https://android.clients.google.com/auth", + match> { headers -> headers["Content-Type"] == "application/x-www-form-urlencoded" }, + any() + ) + } + val buffer = Buffer() + bodySlot.captured.writeTo(buffer) + val encoded = buffer.readUtf8() + assertThat(encoded).contains("Email=user@e.email") + assertThat(encoded).contains("Token=token123") + assertThat(encoded).contains("service=ac2dm") + confirmVerified(client) + } +} diff --git a/app/src/test/java/foundation/e/apps/data/preference/AppLoungeDataStoreTest.kt b/app/src/test/java/foundation/e/apps/data/preference/AppLoungeDataStoreTest.kt new file mode 100644 index 000000000..7ffd0738a --- /dev/null +++ b/app/src/test/java/foundation/e/apps/data/preference/AppLoungeDataStoreTest.kt @@ -0,0 +1,59 @@ +package foundation.e.apps.data.preference + +import android.content.Context +import com.google.common.truth.Truth.assertThat +import foundation.e.apps.data.enums.User +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.test.runTest +import kotlinx.serialization.json.Json +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.RuntimeEnvironment + +@RunWith(RobolectricTestRunner::class) +class AppLoungeDataStoreTest { + + private lateinit var dataStore: AppLoungeDataStore + + @Before + fun setUp() { + val context: Context = RuntimeEnvironment.getApplication() + dataStore = AppLoungeDataStore(context, Json { ignoreUnknownKeys = true }) + runBlocking { + dataStore.destroyCredentials() + dataStore.saveUserType(null) + } + } + + @Test + fun getUserDefaultsToUnavailable() { + assertThat(dataStore.getUser()).isEqualTo(User.UNAVAILABLE) + } + + @Test + fun saveUserTypePersistsValue() = runTest { + dataStore.saveUserType(User.ANONYMOUS) + + assertThat(dataStore.getUser()).isEqualTo(User.ANONYMOUS) + } + + @Test + fun saveAasTokenStoresInFlow() = runTest { + dataStore.saveAasToken("token-123") + + assertThat(dataStore.aasToken.first()).isEqualTo("token-123") + dataStore.destroyCredentials() + assertThat(dataStore.aasToken.first()).isEqualTo("") + } + + @Test + fun saveTOCStatusStoresVersion() = runTest { + dataStore.saveTOCStatus(true, "v2") + + assertThat(dataStore.tocStatus.first()).isTrue() + assertThat(dataStore.tosVersion.first()).isEqualTo("v2") + } +} diff --git a/app/src/test/java/foundation/e/apps/data/preference/AppLoungePreferenceTest.kt b/app/src/test/java/foundation/e/apps/data/preference/AppLoungePreferenceTest.kt new file mode 100644 index 000000000..a179a69f6 --- /dev/null +++ b/app/src/test/java/foundation/e/apps/data/preference/AppLoungePreferenceTest.kt @@ -0,0 +1,70 @@ +package foundation.e.apps.data.preference + +import android.content.SharedPreferences +import androidx.preference.PreferenceManager +import androidx.test.core.app.ApplicationProvider +import com.google.common.truth.Truth.assertThat +import foundation.e.apps.R +import foundation.e.apps.data.Constants.PREFERENCE_SHOW_FOSS +import foundation.e.apps.data.Constants.PREFERENCE_SHOW_GPLAY +import foundation.e.apps.data.Constants.PREFERENCE_SHOW_PWA +import foundation.e.apps.data.enums.User +import io.mockk.every +import io.mockk.mockk +import org.junit.After +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner + +@RunWith(RobolectricTestRunner::class) +class AppLoungePreferenceTest { + + private val dataStore: AppLoungeDataStore = mockk(relaxed = true) + private val context = ApplicationProvider.getApplicationContext() + private lateinit var sharedPreferences: SharedPreferences + private lateinit var preference: AppLoungePreference + + @Before + fun setUp() { + every { dataStore.getUser() } returns User.NO_GOOGLE + sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context) + sharedPreferences.edit().clear().commit() + preference = AppLoungePreference(context, dataStore) + } + + @After + fun tearDown() { + sharedPreferences.edit().clear().commit() + } + + @Test + fun preferredApplicationType_defaultsToAny() { + assertThat(preference.preferredApplicationType()).isEqualTo("any") + } + + @Test + fun preferredApplicationType_returnsOpenWhenFossSelected() { + sharedPreferences.edit().putBoolean(PREFERENCE_SHOW_FOSS, true).commit() + + assertThat(preference.preferredApplicationType()).isEqualTo("open") + } + + @Test + fun preferredApplicationType_returnsPwaWhenPwaSelected() { + sharedPreferences.edit().putBoolean(PREFERENCE_SHOW_PWA, true).commit() + + assertThat(preference.preferredApplicationType()).isEqualTo("pwa") + } + + @Test + fun disableAndEnablePlayStoreTogglesFlag() { + preference.disablePlayStore() + + assertThat(sharedPreferences.getBoolean(PREFERENCE_SHOW_GPLAY, true)).isFalse() + + preference.enablePlayStore() + + assertThat(sharedPreferences.getBoolean(PREFERENCE_SHOW_GPLAY, false)).isTrue() + } +} diff --git a/app/src/test/java/foundation/e/apps/domain/ValidateAppAgeLimitUseCaseTest.kt b/app/src/test/java/foundation/e/apps/domain/ValidateAppAgeLimitUseCaseTest.kt new file mode 100644 index 000000000..6a3906ca7 --- /dev/null +++ b/app/src/test/java/foundation/e/apps/domain/ValidateAppAgeLimitUseCaseTest.kt @@ -0,0 +1,152 @@ +package foundation.e.apps.domain + +import com.aurora.gplayapi.data.models.ContentRating +import com.google.common.truth.Truth.assertThat +import foundation.e.apps.data.ResultSupreme +import foundation.e.apps.data.application.apps.AppsApi +import foundation.e.apps.data.application.data.Application +import foundation.e.apps.data.blockedApps.BlockedAppRepository +import foundation.e.apps.data.enums.ResultStatus +import foundation.e.apps.data.enums.Source +import foundation.e.apps.data.enums.Status +import foundation.e.apps.data.enums.Type +import foundation.e.apps.data.install.models.AppInstall +import foundation.e.apps.data.parentalcontrol.Age +import foundation.e.apps.data.parentalcontrol.ContentRatingDao +import foundation.e.apps.data.parentalcontrol.ParentalControlRepository +import foundation.e.apps.data.parentalcontrol.fdroid.FDroidAntiFeatureRepository +import foundation.e.apps.data.parentalcontrol.googleplay.GPlayContentRatingGroup +import foundation.e.apps.data.parentalcontrol.googleplay.GPlayContentRatingRepository +import foundation.e.apps.domain.ValidateAppAgeLimitUseCase.Companion.KEY_ANTI_FEATURES_NSFW +import foundation.e.apps.domain.model.ContentRatingValidity +import io.mockk.coEvery +import io.mockk.every +import io.mockk.mockk +import kotlinx.coroutines.test.runTest +import org.e.parentalcontrol.data.model.TypeAppManagement +import org.junit.Before +import org.junit.Test + +class ValidateAppAgeLimitUseCaseTest { + + private val gPlayContentRatingRepository: GPlayContentRatingRepository = mockk(relaxed = true) + private val fDroidAntiFeatureRepository: FDroidAntiFeatureRepository = mockk(relaxed = true) + private val parentalControlRepository: ParentalControlRepository = mockk(relaxed = true) + private val blockedAppRepository: BlockedAppRepository = mockk(relaxed = true) + private val appsApi: AppsApi = mockk(relaxed = true) + private val contentRatingDao: ContentRatingDao = mockk(relaxed = true) + + private lateinit var useCase: ValidateAppAgeLimitUseCase + + @Before + fun setUp() { + useCase = ValidateAppAgeLimitUseCase( + gPlayContentRatingRepository, + fDroidAntiFeatureRepository, + parentalControlRepository, + blockedAppRepository, + appsApi, + contentRatingDao + ) + + every { blockedAppRepository.isThirdPartyStoreApp(any()) } returns false + every { fDroidAntiFeatureRepository.fDroidNsfwApps } returns emptyList() + every { parentalControlRepository.getSelectedTypeAppManagement() } returns TypeAppManagement.CONTENT_RATING + every { parentalControlRepository.getSelectedAgeGroup() } returns Age.SIX + } + + @Test + fun returnsValidWhenParentalControlDisabled() = runTest { + every { parentalControlRepository.getSelectedAgeGroup() } returns Age.PARENTAL_CONTROL_DISABLED + + val result = useCase(appInstall()) + + assertThat(result).isInstanceOf(ResultSupreme.Success::class.java) + val data = requireNotNull((result as ResultSupreme.Success).data) + assertThat(data.isValid).isTrue() + } + + @Test + fun blocksThirdPartyStoreApps() = runTest { + every { blockedAppRepository.isThirdPartyStoreApp("pkg") } returns true + + val result = useCase(appInstall()) + + val data = requireNotNull((result as ResultSupreme.Success).data) + assertThat(data.isValid).isFalse() + } + + @Test + fun cleanApkNsfwAppsAreRejected() = runTest { + every { parentalControlRepository.getSelectedAgeGroup() } returns Age.SIX + val nsfwApplication = Application( + antiFeatures = listOf(mapOf(KEY_ANTI_FEATURES_NSFW to "1")) + ) + coEvery { appsApi.getCleanapkAppDetails(any()) } returns + Pair(nsfwApplication, ResultStatus.OK) + + val app = appInstall(source = Source.PWA).copy(id = "cleanApkId") + + val result = useCase(app) + + val data = requireNotNull((result as ResultSupreme.Success).data) + assertThat(data.isValid).isFalse() + } + + @Test + fun validatesAgainstAllowedContentRating() = runTest { + every { parentalControlRepository.getSelectedTypeAppManagement() } returns + TypeAppManagement.CONTENT_RATING + every { parentalControlRepository.getSelectedAgeGroup() } returns Age.SIX + every { blockedAppRepository.isThirdPartyStoreApp(any()) } returns false + every { gPlayContentRatingRepository.contentRatingGroups } returns listOf( + GPlayContentRatingGroup(id = Age.SIX.toString(), ageGroup = "6", ratings = listOf("e")) + ) + + assertThat(gPlayContentRatingRepository.contentRatingGroups).isNotEmpty() + + val result = useCase(appInstall(contentRating = ContentRating(id = "e", title = "Everyone"))) + + val data = requireNotNull((result as ResultSupreme.Success).data) + assertThat(data.isValid).isTrue() + assertThat(data.contentRating?.id).isEqualTo("e") + } + + @Test + fun parentalGuidanceRequestsPin() = runTest { + coEvery { gPlayContentRatingRepository.getEnglishContentRating(any()) } returns + ContentRating(id = ParentalControlRepository.KEY_PARENTAL_GUIDANCE, title = "PG") + + val result = useCase(appInstall()) + + val data = requireNotNull((result as ResultSupreme.Success).data) + assertThat(data.isValid).isFalse() + assertThat(data.requestPin).isTrue() + } + + @Test + fun returnsErrorWhenContentRatingMissingForPlayStore() = runTest { + coEvery { gPlayContentRatingRepository.getEnglishContentRating(any()) } returns null + coEvery { contentRatingDao.getContentRating(any()) } returns null + + val result = useCase( + appInstall( + source = Source.PLAY_STORE, + contentRating = ContentRating(id = "", title = "") + ) + ) + + assertThat(result).isInstanceOf(ResultSupreme.Error::class.java) + } + + private fun appInstall( + source: Source = Source.PLAY_STORE, + contentRating: ContentRating = ContentRating(id = "PG", title = "Parental Guidance") + ) = AppInstall( + packageName = "pkg", + source = source, + status = Status.UNAVAILABLE, + type = Type.NATIVE, + contentRating = contentRating + ) +} diff --git a/app/src/test/java/foundation/e/apps/install/download/data/DownloadProgressLDTest.kt b/app/src/test/java/foundation/e/apps/install/download/data/DownloadProgressLDTest.kt new file mode 100644 index 000000000..5ae4d24c2 --- /dev/null +++ b/app/src/test/java/foundation/e/apps/install/download/data/DownloadProgressLDTest.kt @@ -0,0 +1,66 @@ +package foundation.e.apps.install.download.data + +import android.app.DownloadManager +import android.database.Cursor +import androidx.arch.core.executor.testing.InstantTaskExecutorRule +import com.google.common.truth.Truth.assertThat +import foundation.e.apps.data.install.AppManagerWrapper +import java.lang.reflect.Method +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.mockito.kotlin.mock +import org.mockito.kotlin.whenever + +@RunWith(RobolectricTestRunner::class) +class DownloadProgressLDTest { + + @get:Rule + val instantTaskExecutorRule = InstantTaskExecutorRule() + + private val downloadManager: DownloadManager = mock() + private val query: DownloadManager.Query = mock() + private val appManagerWrapper: AppManagerWrapper = mock() + private lateinit var downloadProgressLD: DownloadProgressLD + private lateinit var processCursor: Method + + @Before + fun setUp() { + downloadProgressLD = DownloadProgressLD(downloadManager, query, appManagerWrapper) + processCursor = + DownloadProgressLD::class.java.getDeclaredMethod( + "processCursor", + Cursor::class.java, + List::class.java + ).apply { isAccessible = true } + } + + @Test + fun processCursor_updatesProgressForSuccessfulDownload() { + val cursor: Cursor = mock() + whenever(cursor.moveToFirst()).thenReturn(true) + whenever(cursor.moveToNext()).thenReturn(false) + whenever(cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_ID)).thenReturn(0) + whenever(cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_STATUS)).thenReturn(1) + whenever(cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_TOTAL_SIZE_BYTES)).thenReturn(2) + whenever(cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_BYTES_DOWNLOADED_SO_FAR)).thenReturn( + 3 + ) + whenever(cursor.getLong(0)).thenReturn(10L) + whenever(cursor.getInt(1)).thenReturn(DownloadManager.STATUS_SUCCESSFUL) + whenever(cursor.getLong(2)).thenReturn(200L) + whenever(cursor.getLong(3)).thenReturn(50L) + + var emitted: DownloadProgress? = null + downloadProgressLD.observeForever { emitted = it } + + processCursor.invoke(downloadProgressLD, cursor, listOf(10L)) + + assertThat(emitted).isNotNull() + assertThat(emitted?.bytesDownloadedSoFar?.get(10L)).isEqualTo(50L) + assertThat(emitted?.totalSizeBytes?.get(10L)).isEqualTo(200L) + assertThat(emitted?.status?.get(10L)).isTrue() + } +} diff --git a/app/src/test/java/foundation/e/apps/ui/categories/model/CategoriesDiffUtilTest.kt b/app/src/test/java/foundation/e/apps/ui/categories/model/CategoriesDiffUtilTest.kt new file mode 100644 index 000000000..90a8820bc --- /dev/null +++ b/app/src/test/java/foundation/e/apps/ui/categories/model/CategoriesDiffUtilTest.kt @@ -0,0 +1,35 @@ +package foundation.e.apps.ui.categories.model + +import com.google.common.truth.Truth.assertThat +import foundation.e.apps.data.application.data.Category +import foundation.e.apps.data.enums.AppTag +import org.junit.Test + +class CategoriesDiffUtilTest { + + private val original = listOf( + Category(id = "1", title = "One", drawable = 1, tag = AppTag.GPlay()), + Category(id = "2", title = "Two", drawable = 2, tag = AppTag.GPlay()) + ) + private val updated = listOf( + Category(id = "1", title = "One", drawable = 1, tag = AppTag.GPlay()), + Category(id = "2", title = "Deux", drawable = 2, tag = AppTag.GPlay()) + ) + + @Test + fun itemsAreMatchedById() { + val diff = CategoriesDiffUtil(original, updated) + + assertThat(diff.getOldListSize()).isEqualTo(2) + assertThat(diff.getNewListSize()).isEqualTo(2) + assertThat(diff.areItemsTheSame(0, 0)).isTrue() + } + + @Test + fun contentChangeIsDetected() { + val diff = CategoriesDiffUtil(original, updated) + + assertThat(diff.areContentsTheSame(0, 0)).isTrue() + assertThat(diff.areContentsTheSame(1, 1)).isFalse() + } +} diff --git a/app/src/test/java/foundation/e/apps/utils/ExodusUriGeneratorTest.kt b/app/src/test/java/foundation/e/apps/utils/ExodusUriGeneratorTest.kt new file mode 100644 index 000000000..74351f1cf --- /dev/null +++ b/app/src/test/java/foundation/e/apps/utils/ExodusUriGeneratorTest.kt @@ -0,0 +1,45 @@ +package foundation.e.apps.utils + +import com.google.common.truth.Truth.assertThat +import java.util.Locale +import org.junit.After +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner + +@RunWith(RobolectricTestRunner::class) +class ExodusUriGeneratorTest { + + private lateinit var defaultLocale: Locale + + @Before + fun setUp() { + defaultLocale = Locale.getDefault() + } + + @After + fun tearDown() { + Locale.setDefault(defaultLocale) + } + + @Test + fun buildReportUriUsesPreferredLanguage() { + Locale.setDefault(Locale.FRENCH) + + val uri = ExodusUriGenerator.buildReportUri("com.example.app") + + assertThat(uri.toString()) + .isEqualTo("https://reports.exodus-privacy.eu.org/fr/reports/com.example.app/latest") + } + + @Test + fun buildRequestUriFallsBackToEnglishWhenLocaleUnsupported() { + Locale.setDefault(Locale("pt")) + + val uri = ExodusUriGenerator.buildRequestReportUri("com.example.app") + + assertThat(uri.toString()) + .isEqualTo("https://reports.exodus-privacy.eu.org/en/analysis/submit#com.example.app") + } +} diff --git a/app/src/test/java/foundation/e/apps/utils/ExtensionsTest.kt b/app/src/test/java/foundation/e/apps/utils/ExtensionsTest.kt new file mode 100644 index 000000000..2e94492eb --- /dev/null +++ b/app/src/test/java/foundation/e/apps/utils/ExtensionsTest.kt @@ -0,0 +1,18 @@ +package foundation.e.apps.utils + +import com.google.common.truth.Truth.assertThat +import java.util.Date +import java.util.Locale +import org.junit.Test + +class ExtensionsTest { + + @Test + fun dateFormatsWithProvidedPattern() { + val date = Date(0) // Jan 1 1970 UTC + + val formatted = date.getFormattedString("yyyy-MM-dd", Locale.US) + + assertThat(formatted).isEqualTo("1970-01-01") + } +} diff --git a/app/src/test/java/foundation/e/apps/utils/StorageComputerTest.kt b/app/src/test/java/foundation/e/apps/utils/StorageComputerTest.kt new file mode 100644 index 000000000..849b3517e --- /dev/null +++ b/app/src/test/java/foundation/e/apps/utils/StorageComputerTest.kt @@ -0,0 +1,40 @@ +package foundation.e.apps.utils + +import com.google.common.truth.Truth.assertThat +import java.util.Locale +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner + +@RunWith(RobolectricTestRunner::class) +class StorageComputerTest { + + @Test + fun humanReadableByteCountSI_returnsBytesForSmallNumbers() { + val formatted = StorageComputer.humanReadableByteCountSI(500) + + assertThat(formatted).isEqualTo("500 B") + } + + @Test + fun humanReadableByteCountSI_formatsLargerNumbers() { + val defaultLocale = Locale.getDefault() + Locale.setDefault(Locale.US) + + val formatted = StorageComputer.humanReadableByteCountSI(1_500_000) + + Locale.setDefault(defaultLocale) + + assertThat(formatted).isEqualTo("1.5 MB") + } + + @Test + fun spaceMissing_matchesCalculatedDifference() { + val appInstall = foundation.e.apps.data.install.models.AppInstall(appSize = 1234) + + val expected = + appInstall.appSize + (500 * (1000 * 1000)) - StorageComputer.calculateAvailableDiskSpace() + + assertThat(StorageComputer.spaceMissing(appInstall)).isEqualTo(expected) + } +} diff --git a/app/src/test/java/foundation/e/apps/utils/SystemInfoProviderTest.kt b/app/src/test/java/foundation/e/apps/utils/SystemInfoProviderTest.kt new file mode 100644 index 000000000..7a260ce28 --- /dev/null +++ b/app/src/test/java/foundation/e/apps/utils/SystemInfoProviderTest.kt @@ -0,0 +1,22 @@ +package foundation.e.apps.utils + +import com.google.common.truth.Truth.assertThat +import org.json.JSONObject +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner + +@RunWith(RobolectricTestRunner::class) +class SystemInfoProviderTest { + + @Test + fun getAppBuildInfo_containsExpectedKeys() { + val json = JSONObject(SystemInfoProvider.getAppBuildInfo()) + + assertThat(json.has("package")).isTrue() + assertThat(json.has("version")).isTrue() + assertThat(json.has("device")).isTrue() + assertThat(json.has("api")).isTrue() + assertThat(json.has("build_id")).isTrue() + } +} diff --git a/app/src/test/java/foundation/e/apps/utils/eventBus/AppEventTest.kt b/app/src/test/java/foundation/e/apps/utils/eventBus/AppEventTest.kt new file mode 100644 index 000000000..eaf1529b4 --- /dev/null +++ b/app/src/test/java/foundation/e/apps/utils/eventBus/AppEventTest.kt @@ -0,0 +1,47 @@ +package foundation.e.apps.utils.eventBus + +import com.google.common.truth.Truth.assertThat +import foundation.e.apps.data.enums.ResultStatus +import foundation.e.apps.data.enums.User +import foundation.e.apps.data.install.models.AppInstall +import foundation.e.apps.data.ResultSupreme +import kotlinx.coroutines.CompletableDeferred +import org.junit.Test + +class AppEventTest { + + @Test + fun ageLimitRestrictionEvent_carriesDeferred() { + val deferred = CompletableDeferred() + + val event = AppEvent.AgeLimitRestrictionEvent("PG", deferred) + + assertThat(event.data).isEqualTo("PG") + assertThat(event.onClose).isSameInstanceAs(deferred) + } + + @Test + fun successfulLogin_wrapsUser() { + val event = AppEvent.SuccessfulLogin(User.ANONYMOUS) + + assertThat(event.data).isEqualTo(User.ANONYMOUS) + } + + @Test + fun updateEvent_wrapsResult() { + val result = ResultSupreme.WorkError(ResultStatus.UNKNOWN) + + val event = AppEvent.UpdateEvent(result) + + assertThat(event.data).isEqualTo(result) + } + + @Test + fun appPurchaseEvent_containsAppInstall() { + val appInstall = AppInstall(packageName = "pkg") + + val event = AppEvent.AppPurchaseEvent(appInstall) + + assertThat(event.data).isEqualTo(appInstall) + } +} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 1a0a6e09c..48ab23260 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -44,6 +44,7 @@ okhttp = "4.12.0" photoview = "2.3.0" preferenceKtx = "1.2.1" protobufJavalite = "4.28.2" +robolectric = "4.12.1" room = "2.6.1" shimmer = "0.5.0" telemetry = "1.0.1-release" @@ -105,6 +106,7 @@ preference-ktx = { module = "androidx.preference:preference-ktx", version.ref = protobuf-javalite = { module = "com.google.protobuf:protobuf-javalite", version.ref = "protobufJavalite" } recyclerview = { module = "androidx.recyclerview:recyclerview", version.ref = "recyclerview" } retrofit = { module = "com.squareup.retrofit2:retrofit", version.ref = "gson" } +robolectric = { module = "org.robolectric:robolectric", version.ref = "robolectric" } room-compiler = { module = "androidx.room:room-compiler", version.ref = "room" } room-ktx = { module = "androidx.room:room-ktx", version.ref = "room" } room-runtime = { module = "androidx.room:room-runtime", version.ref = "room" } -- GitLab