Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 55146b70 authored by Jonathan Klee's avatar Jonathan Klee
Browse files

tests: add more data & domain layers unit tests

parent 6854bcb3
Loading
Loading
Loading
Loading
Loading
+282 −7
Original line number Diff line number Diff line
/*
 * Copyright (C) MURENA SAS 2026
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
 *
 */

package foundation.e.apps.data.application

import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import androidx.lifecycle.asFlow
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.apps.AppsApi
import foundation.e.apps.data.application.category.CategoriesResponse
import foundation.e.apps.data.application.category.CategoryApi
import foundation.e.apps.data.application.data.Application
import foundation.e.apps.data.application.data.Category
import foundation.e.apps.data.application.data.Home
import foundation.e.apps.data.application.downloadInfo.DownloadInfoApi
import foundation.e.apps.data.application.search.SearchSuggestion
import foundation.e.apps.data.application.utils.CategoryType
import foundation.e.apps.data.cleanapk.data.download.Download
import foundation.e.apps.data.enums.FilterLevel
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.install.models.AppInstall
import foundation.e.apps.util.MainCoroutineRule
import io.mockk.coEvery
import io.mockk.coVerify
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.Rule
import org.junit.Test
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import retrofit2.Response

@OptIn(ExperimentalCoroutinesApi::class)
class ApplicationRepositoryHomeTest {
@@ -34,7 +62,11 @@ class ApplicationRepositoryHomeTest {
    @get:Rule
    val instantExecutorRule = InstantTaskExecutorRule()

    private val playStoreHome = Home(title = "Play", list = listOf(Application(name = "p1")))
    private val playStoreHome = Home(
        title = "Play",
        list = listOf(Application(name = "p1")),
        source = ApplicationRepository.APP_TYPE_ANY
    )
    private val ossHome = Home(
        title = "OSS",
        list = listOf(Application(name = "o1", source = Source.OPEN_SOURCE)),
@@ -67,7 +99,27 @@ class ApplicationRepositoryHomeTest {

        assertThat(emissions).hasSize(2)
        val finalList = emissions.last().data!!
        assertThat(finalList.map { it.source }).containsExactly("", ApplicationRepository.APP_TYPE_OPEN).inOrder()
        assertThat(finalList.map { it.source })
            .containsExactly(ApplicationRepository.APP_TYPE_ANY, ApplicationRepository.APP_TYPE_OPEN)
            .inOrder()
    }

    @Test
    fun fetchHomeScreenDataEmitsPlayStoreFirst() = 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 = applicationRepository.getHomeScreenData().asFlow().take(2).toList()

        val firstList = emissions.first().data!!
        assertThat(firstList.map { it.source })
            .containsExactly(ApplicationRepository.APP_TYPE_ANY)
            .inOrder()
    }

    @Test
@@ -91,6 +143,226 @@ class ApplicationRepositoryHomeTest {
        assertThat(result.message).contains("boom")
    }

    @Test
    fun fetchHomeScreenDataSetsOpenSourceErrorMessage() = runTest {
        val failingStore = object : StoreRepository {
            override suspend fun getHomeScreenData(list: MutableList<Home>): List<Home> {
                throw IllegalStateException("oss down")
            }
            override suspend fun getAppDetails(packageName: String) = Application()
            override suspend fun getSearchResults(pattern: String) = emptyList<Application>()
            override suspend fun getSearchSuggestions(pattern: String) = emptyList<SearchSuggestion>()
        }
        val repositories = linkedMapOf(Source.OPEN_SOURCE to failingStore)
        every { stores.getStores() } returns repositories
        every { stores.getStore(Source.OPEN_SOURCE) } returns failingStore

        val emissions = applicationRepository.getHomeScreenData().asFlow().take(1).toList()

        val result = emissions.single()
        assertThat(result.isUnknownError()).isTrue()
        assertThat(result.message).contains("oss down")
    }

    @Test
    fun fetchHomeScreenDataEmitsMergedListWhenOneSourceFails() = runTest {
        val failingStore = object : StoreRepository {
            override suspend fun getHomeScreenData(list: MutableList<Home>): List<Home> {
                throw IllegalStateException("oss down")
            }
            override suspend fun getAppDetails(packageName: String) = Application()
            override suspend fun getSearchResults(pattern: String) = emptyList<Application>()
            override suspend fun getSearchSuggestions(pattern: String) = emptyList<SearchSuggestion>()
        }
        val repositories = linkedMapOf(
            Source.PLAY_STORE to fakeStore(playStoreHome),
            Source.OPEN_SOURCE to failingStore,
        )
        every { stores.getStores() } returns repositories
        every { stores.getStore(Source.PLAY_STORE) } returns repositories.getValue(Source.PLAY_STORE)
        every { stores.getStore(Source.OPEN_SOURCE) } returns failingStore

        val emissions = applicationRepository.getHomeScreenData().asFlow().take(2).toList()

        val result = emissions.last()
        assertThat(result.isUnknownError()).isTrue()
        assertThat(result.message).contains("oss down")
        assertThat(result.data?.map { it.source })
            .containsExactly(ApplicationRepository.APP_TYPE_ANY)
    }

    @Test
    fun getSelectedAppTypesReturnsEnabledStores() {
        every { stores.isStoreEnabled(Source.PLAY_STORE) } returns true
        every { stores.isStoreEnabled(Source.OPEN_SOURCE) } returns true
        every { stores.isStoreEnabled(Source.PWA) } returns false

        val result = applicationRepository.getSelectedAppTypes()

        assertThat(result)
            .containsExactly(ApplicationRepository.APP_TYPE_ANY, ApplicationRepository.APP_TYPE_OPEN)
            .inOrder()
    }

    @Test
    fun getApplicationDetailsDelegatesToAppsApiForList() = runTest {
        val expected = Pair(listOf(Application(name = "n1")), ResultStatus.OK)
        val packages = listOf("pkg.one")

        coEvery { appsApi.getApplicationDetails(packages, Source.PLAY_STORE) } returns expected

        val result = applicationRepository.getApplicationDetails(packages, Source.PLAY_STORE)

        assertThat(result).isEqualTo(expected)
    }

    @Test
    fun getAppFilterLevelDelegatesToAppsApi() = runTest {
        val app = Application(name = "app")
        val expected = FilterLevel.UI

        coEvery { appsApi.getAppFilterLevel(app) } returns expected

        val result = applicationRepository.getAppFilterLevel(app)

        assertThat(result).isEqualTo(expected)
    }

    @Test
    fun getApplicationDetailsDelegatesToAppsApiForSingle() = runTest {
        val expected = Pair(Application(name = "n1"), ResultStatus.OK)

        coEvery { appsApi.getApplicationDetails("id", "pkg", Source.PWA) } returns expected

        val result = applicationRepository.getApplicationDetails("id", "pkg", Source.PWA)

        assertThat(result).isEqualTo(expected)
    }

    @Test
    fun updateFusedDownloadWithDownloadingInfoDelegatesToDownloadInfoApi() = runTest {
        val appInstall = AppInstall(id = "id", packageName = "pkg")

        coEvery { downloadInfoApi.updateFusedDownloadWithDownloadingInfo(Source.PLAY_STORE, appInstall) } returns Unit

        applicationRepository.updateFusedDownloadWithDownloadingInfo(Source.PLAY_STORE, appInstall)

        coVerify(exactly = 1) { downloadInfoApi.updateFusedDownloadWithDownloadingInfo(Source.PLAY_STORE, appInstall) }
    }

    @Test
    fun getOSSDownloadInfoDelegatesToDownloadInfoApi() = runTest {
        val expected = mockk<Response<Download>>()

        coEvery { downloadInfoApi.getOSSDownloadInfo("id", "v1") } returns expected

        val result = applicationRepository.getOSSDownloadInfo("id", "v1")

        assertThat(result).isEqualTo(expected)
    }

    @Test
    fun getCategoriesListDelegatesToCategoryApi() = runTest {
        val categories = listOf(Category(title = "t1"))
        val expected = listOf(
            CategoriesResponse(categories, CategoryType.APPLICATION, Source.OPEN_SOURCE, ResultStatus.OK)
        )

        coEvery { categoryApi.getCategoriesList(CategoryType.APPLICATION) } returns expected

        val result = applicationRepository.getCategoriesList(CategoryType.APPLICATION)

        assertThat(result).isEqualTo(expected)
    }

    @Test
    fun getAppsListBasedOnCategoryUsesOpenSourceApi() = runTest {
        val expected = ResultSupreme.Success(Pair(listOf(Application(name = "app")), ""))
        val repository = repositoryWithCategoryApi(object : CategoryApi {
            override suspend fun getCategoriesList(type: CategoryType) = emptyList<CategoriesResponse>()
            override suspend fun getGplayAppsByCategory(category: String, pageUrl: String?) =
                ResultSupreme.Error<Pair<List<Application>, String>>()
            override suspend fun getCleanApkAppsByCategory(category: String, source: Source) = expected
        })

        val result = repository.getAppsListBasedOnCategory("cat", null, Source.OPEN_SOURCE)

        assertThat(result).isEqualTo(expected)
    }

    @Test
    fun getAppsListBasedOnCategoryUsesPwaApi() = runTest {
        val expected = ResultSupreme.Success(Pair(listOf(Application(name = "app")), ""))
        val repository = repositoryWithCategoryApi(object : CategoryApi {
            override suspend fun getCategoriesList(type: CategoryType) = emptyList<CategoriesResponse>()
            override suspend fun getGplayAppsByCategory(category: String, pageUrl: String?) =
                ResultSupreme.Error<Pair<List<Application>, String>>()
            override suspend fun getCleanApkAppsByCategory(category: String, source: Source) = expected
        })

        val result = repository.getAppsListBasedOnCategory("cat", null, Source.PWA)

        assertThat(result).isEqualTo(expected)
    }

    @Test
    fun getAppsListBasedOnCategoryUsesGplayApi() = runTest {
        val expected = ResultSupreme.Success(Pair(listOf(Application(name = "app")), "next"))
        val repository = repositoryWithCategoryApi(object : CategoryApi {
            override suspend fun getCategoriesList(type: CategoryType) = emptyList<CategoriesResponse>()
            override suspend fun getGplayAppsByCategory(category: String, pageUrl: String?) = expected
            override suspend fun getCleanApkAppsByCategory(category: String, source: Source) =
                ResultSupreme.Error<Pair<List<Application>, String>>()
        })

        val result = repository.getAppsListBasedOnCategory("cat", "next", Source.PLAY_STORE)

        assertThat(result).isEqualTo(expected)
    }

    @Test
    fun getFusedAppInstallationStatusDelegatesToAppsApi() {
        val app = Application(name = "app")

        every { appsApi.getFusedAppInstallationStatus(app) } returns Status.INSTALLED

        val result = applicationRepository.getFusedAppInstallationStatus(app)

        assertThat(result).isEqualTo(Status.INSTALLED)
    }

    @Test
    fun isAnyFusedAppUpdatedDelegatesToAppsApi() {
        val newApps = listOf(Application(name = "n1"))
        val oldApps = listOf(Application(name = "o1"))

        every { appsApi.isAnyFusedAppUpdated(newApps, oldApps) } returns true

        val result = applicationRepository.isAnyFusedAppUpdated(newApps, oldApps)

        assertThat(result).isTrue()
    }

    @Test
    fun isAnyAppInstallStatusChangedDelegatesToAppsApi() {
        val apps = listOf(Application(name = "n1"))

        every { appsApi.isAnyAppInstallStatusChanged(apps) } returns true

        val result = applicationRepository.isAnyAppInstallStatusChanged(apps)

        assertThat(result).isTrue()
    }

    @Test
    fun isOpenSourceSelectedDelegatesToAppsApi() {
        every { appsApi.isOpenSourceSelected() } returns false

        val result = applicationRepository.isOpenSourceSelected()

        assertThat(result).isFalse()
    }

    private fun fakeStore(home: Home) = object : StoreRepository {
        override suspend fun getHomeScreenData(list: MutableList<Home>): List<Home> {
            list.add(home)
@@ -103,4 +375,7 @@ class ApplicationRepositoryHomeTest {

        override suspend fun getSearchSuggestions(pattern: String): List<SearchSuggestion> = emptyList()
    }

    private fun repositoryWithCategoryApi(categoryApi: CategoryApi) =
        ApplicationRepository(categoryApi, appsApi, downloadInfoApi, stores)
}
+102 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) MURENA SAS 2026
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
 *
 */

package foundation.e.apps.data.cleanapk.repositories

import android.content.Context
import com.google.common.truth.Truth.assertThat
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.cleanapk.CleanApkRetrofit
import foundation.e.apps.data.cleanapk.CleanApkSearchHelper
import foundation.e.apps.data.cleanapk.data.home.CleanApkHome
import foundation.e.apps.data.cleanapk.data.home.HomeScreenResponse
import foundation.e.apps.data.enums.Source
import io.mockk.coEvery
import io.mockk.coVerify
import io.mockk.mockk
import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Test
import retrofit2.Response

class CleanApkPwaRepositoryTest {

    private val cleanApkRetrofit = mockk<CleanApkRetrofit>(relaxed = true)
    private val homeConverter = mockk<HomeConverter>(relaxed = true)
    private val context = mockk<Context>(relaxed = true)
    private val searchHelper = mockk<CleanApkSearchHelper>(relaxed = true)

    private lateinit var repository: CleanApkPwaRepository

    @Before
    fun setUp() {
        repository = CleanApkPwaRepository(cleanApkRetrofit, homeConverter, context, searchHelper)
    }

    @Test
    fun `getHomeScreenData populates list and sets sources`() = runTest {
        val appOne = Application(name = "One")
        val appTwo = Application(name = "Two")
        val expectedHomes = listOf(
            Home("Top", listOf(appOne)),
            Home("Popular", listOf(appTwo))
        )

        coEvery {
            cleanApkRetrofit.getHomeScreenData(
                CleanApkRetrofit.APP_TYPE_PWA,
                CleanApkRetrofit.APP_SOURCE_ANY
            )
        } returns Response.success(HomeScreenResponse(home = CleanApkHome()))

        coEvery { homeConverter.toGenericHome(any(), CleanApkRetrofit.APP_TYPE_PWA) } returns expectedHomes

        val inputList = mutableListOf<Home>()
        val result = repository.getHomeScreenData(inputList)

        coVerify {
            cleanApkRetrofit.getHomeScreenData(
                CleanApkRetrofit.APP_TYPE_PWA,
                CleanApkRetrofit.APP_SOURCE_ANY
            )
        }

        assertThat(result).isSameInstanceAs(inputList)
        assertThat(result).hasSize(2)
        assertThat(result.map { it.source }).containsExactly(
            ApplicationRepository.APP_TYPE_PWA,
            ApplicationRepository.APP_TYPE_PWA
        )
        assertThat(appOne.source).isEqualTo(Source.PWA)
        assertThat(appTwo.source).isEqualTo(Source.PWA)
    }

    @Test(expected = IllegalStateException::class)
    fun `getHomeScreenData throws when home payload missing`() = runTest {
        coEvery {
            cleanApkRetrofit.getHomeScreenData(
                CleanApkRetrofit.APP_TYPE_PWA,
                CleanApkRetrofit.APP_SOURCE_ANY
            )
        } returns Response.success(null)

        repository.getHomeScreenData(mutableListOf())
    }
}
+0 −2
Original line number Diff line number Diff line
@@ -35,7 +35,6 @@ import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertThrows
import org.junit.Assert.assertTrue
import org.junit.Ignore
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.kotlin.any
@@ -54,7 +53,6 @@ class MicrogLoginManagerTest {
    private val context = RuntimeEnvironment.getApplication()

    @Test
    @Ignore("Flaky under mock AccountManager; covered by error-path tests")
    fun `returns success when token available`() = runBlocking {
        val account = Account("user@gmail.com", MicrogCertUtil.GOOGLE_ACCOUNT_TYPE)
        val accountManager = mock<AccountManager>()
+103 −36

File changed.

Preview size limit exceeded, changes collapsed.

+102 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2026 e Foundation
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */

package foundation.e.apps.domain.home

import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.asFlow
import com.google.common.truth.Truth.assertThat
import foundation.e.apps.data.ResultSupreme
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.util.MainCoroutineRule
import io.mockk.coEvery
import io.mockk.mockk
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.test.runTest
import org.junit.Rule
import org.junit.Test
import java.lang.IllegalStateException

@OptIn(ExperimentalCoroutinesApi::class)
class FetchHomeScreenDataUseCaseTest {

    @get:Rule
    val instantExecutorRule = InstantTaskExecutorRule()

    @get:Rule
    val mainCoroutineRule = MainCoroutineRule()

    private val applicationRepository = mockk<ApplicationRepository>()

    @Test
    fun invoke_maps_result_to_domain() = runTest(mainCoroutineRule.testDispatcher) {
        val app = Application(_id = "app-1", package_name = "pkg.one", name = "App One")
        val home = Home(title = "Play", list = listOf(app), source = ApplicationRepository.APP_TYPE_ANY)
        val liveData = MutableLiveData<ResultSupreme<List<Home>>>()
        liveData.value = ResultSupreme.Success(listOf(home))

        coEvery { applicationRepository.getHomeScreenData() } returns liveData

        val useCase = FetchHomeScreenDataUseCase(applicationRepository)
        val result = useCase().asFlow().first()

        assertThat(result).isInstanceOf(HomeScreenResult.Success::class.java)
        assertThat(result.data).hasSize(1)
        assertThat(result.data.first().apps.first().id).isEqualTo("app-1")
        assertThat(result.data.first().apps.first().packageName).isEqualTo("pkg.one")
        assertThat(result.data.first().apps.first().name).isEqualTo("App One")
    }

    @Test
    fun invoke_maps_timeout_to_domain() = runTest(mainCoroutineRule.testDispatcher) {
        val app = Application(_id = "app-2", package_name = "pkg.two", name = "App Two")
        val home = Home(title = "Play", list = listOf(app), source = ApplicationRepository.APP_TYPE_ANY)
        val liveData = MutableLiveData<ResultSupreme<List<Home>>>()
        liveData.value = ResultSupreme.Timeout(listOf(home))

        coEvery { applicationRepository.getHomeScreenData() } returns liveData

        val useCase = FetchHomeScreenDataUseCase(applicationRepository)
        val result = useCase().asFlow().first()

        assertThat(result).isInstanceOf(HomeScreenResult.Timeout::class.java)
        assertThat(result.data).hasSize(1)
        assertThat(result.data.first().apps.first().id).isEqualTo("app-2")
    }

    @Test
    fun invoke_maps_error_to_domain() = runTest(mainCoroutineRule.testDispatcher) {
        val liveData = MutableLiveData<ResultSupreme<List<Home>>>()
        val exception = IllegalStateException("boom")
        liveData.value = ResultSupreme.Error("failed", exception)

        coEvery { applicationRepository.getHomeScreenData() } returns liveData

        val useCase = FetchHomeScreenDataUseCase(applicationRepository)
        val result = useCase().asFlow().first()

        assertThat(result).isInstanceOf(HomeScreenResult.Error::class.java)
        val error = result as HomeScreenResult.Error
        assertThat(error.data).isEmpty()
        assertThat(error.message).isEqualTo("failed")
        assertThat(error.exception).isEqualTo(exception)
    }
}
Loading