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

Verified Commit ee74223e authored by Fahim M. Choudhury's avatar Fahim M. Choudhury
Browse files

test(updates): cover batch-checking of manual updates

parent 024d8b52
Loading
Loading
Loading
Loading
+165 −0
Original line number Diff line number Diff line
package foundation.e.apps.data.install.updates

import android.content.Context
import androidx.test.core.app.ApplicationProvider
import com.google.gson.Gson
import com.google.gson.GsonBuilder
import foundation.e.apps.data.application.data.Application
import foundation.e.apps.data.enums.Source
import foundation.e.apps.data.enums.Type
import foundation.e.apps.data.installation.model.SharedLib
import foundation.e.apps.domain.model.install.Status
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest
import org.junit.After
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertNull
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner

@OptIn(ExperimentalCoroutinesApi::class)
@RunWith(RobolectricTestRunner::class)
class ManualUpdateChainStoreTest {
    private lateinit var context: Context
    private lateinit var store: ManualUpdateChainStore

    @Before
    fun setUp() {
        context = ApplicationProvider.getApplicationContext()
        store = ManualUpdateChainStore(context, testGson())
    }

    @After
    fun tearDown() = runTest {
        store.clearSnapshot(CHAIN_ID)
        store.clearSnapshot(OTHER_CHAIN_ID)
    }

    @Test
    fun writeSnapshot_readsBackMatchingSnapshot() = runTest {
        val snapshot = createSnapshot(chainId = CHAIN_ID, cursor = 1)

        store.writeSnapshot(snapshot)

        val storedSnapshot = store.readSnapshot(CHAIN_ID)
        assertEquals(snapshot.chainId, storedSnapshot?.chainId)
        assertEquals(snapshot.cursor, storedSnapshot?.cursor)
        assertEquals(snapshot.packages.map { it.package_name }, storedSnapshot?.packages?.map { it.package_name })
        assertNull(store.readSnapshot(OTHER_CHAIN_ID))
    }

    @Test
    fun writeSnapshot_preservesApplicationFieldsUsedByManualUpdateChain() = runTest {
        val snapshot = createSnapshot(chainId = CHAIN_ID, cursor = 1)

        store.writeSnapshot(snapshot)

        val storedSnapshot = store.readSnapshot(CHAIN_ID)
        assertNotNull(storedSnapshot)
        assertEquals(snapshot.createdAtMillis, storedSnapshot?.createdAtMillis)
        assertApplicationFields(snapshot.packages[0], storedSnapshot!!.packages[0])
        assertApplicationFields(snapshot.packages[1], storedSnapshot.packages[1])
        assertApplicationFields(snapshot.packages[2], storedSnapshot.packages[2])
    }

    @Test
    fun advanceSnapshot_updatesCursor_andCapsAtPackageCount() = runTest {
        val snapshot = createSnapshot(chainId = CHAIN_ID, cursor = 1)
        store.writeSnapshot(snapshot)

        val advancedSnapshot = store.advanceSnapshot(CHAIN_ID, consumedCount = 10)

        assertNotNull(advancedSnapshot)
        assertEquals(3, advancedSnapshot?.cursor)
        assertEquals(3, store.readSnapshot(CHAIN_ID)?.cursor)
        assertApplicationFields(snapshot.packages[0], advancedSnapshot!!.packages[0])
    }

    @Test
    fun clearSnapshot_removesOnlyMatchingChain() = runTest {
        store.writeSnapshot(createSnapshot(chainId = CHAIN_ID))

        store.clearSnapshot(OTHER_CHAIN_ID)
        assertNotNull(store.readSnapshot(CHAIN_ID))

        store.clearSnapshot(CHAIN_ID)
        assertNull(store.readSnapshot(CHAIN_ID))
    }

    private fun createSnapshot(
        chainId: String,
        cursor: Int = 0,
    ): ManualUpdateChainSnapshot {
        return ManualUpdateChainSnapshot(
            chainId = chainId,
            cursor = cursor,
            createdAtMillis = 1234L,
            packages = listOf(
                createApplication("one"),
                createApplication("two"),
                createApplication("three"),
            ),
        )
    }

    private fun createApplication(packageSuffix: String): Application {
        return Application(
            _id = "app-$packageSuffix",
            name = "App $packageSuffix",
            package_name = "foundation.e.apps.$packageSuffix",
            source = Source.PLAY_STORE,
            status = Status.UPDATABLE,
            type = Type.NATIVE,
            icon_image_path = "icons/$packageSuffix",
            latest_version_code = 42L,
            offer_type = 1,
            isFree = true,
            originalSize = 4096L,
            url = "https://example.com/$packageSuffix.apk",
            isSystemApp = false,
            dependentLibraries = listOf(
                SharedLib(
                    packageName = "foundation.e.apps.lib.$packageSuffix",
                    versionCode = 7L,
                    offerType = 2,
                    downloadUrls = listOf("https://example.com/lib-$packageSuffix.apk"),
                    downloadIds = mapOf(123L to true),
                )
            ),
        )
    }

    private fun assertApplicationFields(
        expected: Application,
        actual: Application,
    ) {
        assertEquals(expected._id, actual._id)
        assertEquals(expected.name, actual.name)
        assertEquals(expected.package_name, actual.package_name)
        assertEquals(expected.source, actual.source)
        assertEquals(expected.status, actual.status)
        assertEquals(expected.type, actual.type)
        assertEquals(expected.icon_image_path, actual.icon_image_path)
        assertEquals(expected.latest_version_code, actual.latest_version_code)
        assertEquals(expected.offer_type, actual.offer_type)
        assertEquals(expected.isFree, actual.isFree)
        assertEquals(expected.originalSize, actual.originalSize)
        assertEquals(expected.url, actual.url)
        assertEquals(expected.isSystemApp, actual.isSystemApp)
        assertEquals(expected.dependentLibraries, actual.dependentLibraries)
    }

    private fun testGson(): Gson {
        return GsonBuilder()
            .enableComplexMapKeySerialization()
            .create()
    }

    companion object {
        private const val CHAIN_ID = "chain-1"
        private const val OTHER_CHAIN_ID = "chain-2"
    }
}
+29 −3
Original line number Diff line number Diff line
@@ -62,13 +62,17 @@ class UpdatesWorkManagerTest {

    @Test
    fun startUpdateAllWork_buildsExpectedOneTimeRequest() {
        UpdatesWorkManager.startUpdateAllWork(context)
        UpdatesWorkManager.startUpdateAllWork(context, CHAIN_ID)

        val workInfo = getActiveUniqueWorkInfo("updates_work_user")
        val workSpec = getWorkSpec(workInfo.id)

        assertThat(workInfo.tags).contains(UpdatesWorkManager.TAG_WORK_USER_INITIATED_UPDATE)
        assertThat(workSpec.input.getBoolean(UpdatesWorker.IS_AUTO_UPDATE, true)).isFalse()
        assertThat(workSpec.input.getString(UpdatesWorkManager.INPUT_KEY_CHAIN_ID)).isEqualTo(CHAIN_ID)
        assertThat(
            workSpec.input.getBoolean(UpdatesWorkManager.INPUT_KEY_IS_MANUAL_CHAIN_CONTINUATION, true)
        ).isFalse()
        assertThat(workSpec.constraints.requiredNetworkType).isEqualTo(NetworkType.CONNECTED)
        assertThat(workSpec.expedited).isTrue()
        assertThat(workSpec.outOfQuotaPolicy)
@@ -77,10 +81,10 @@ class UpdatesWorkManagerTest {

    @Test
    fun startUpdateAllWork_replacesExistingUniqueWork() {
        UpdatesWorkManager.startUpdateAllWork(context)
        UpdatesWorkManager.startUpdateAllWork(context, CHAIN_ID)
        val firstWorkId = getActiveUniqueWorkInfo("updates_work_user").id

        UpdatesWorkManager.startUpdateAllWork(context)
        UpdatesWorkManager.startUpdateAllWork(context, "$CHAIN_ID-next")

        val allWorkInfos = workManager.getWorkInfosForUniqueWork("updates_work_user").get()
        val activeWorkInfos = allWorkInfos.filter { !it.state.isFinished }
@@ -89,6 +93,24 @@ class UpdatesWorkManagerTest {
        assertThat(activeWorkInfos.single().id).isNotEqualTo(firstWorkId)
    }

    @Test
    fun appendUpdateAllWork_appendsContinuationRequestWithManualTag() {
        UpdatesWorkManager.startUpdateAllWork(context, CHAIN_ID)

        UpdatesWorkManager.appendUpdateAllWork(context, CHAIN_ID)

        val workInfos = workManager.getWorkInfosForUniqueWork("updates_work_user").get()
        val continuationWorkInfo = workInfos.single { it.state == WorkInfo.State.BLOCKED }
        val continuationWorkSpec = getWorkSpec(continuationWorkInfo.id)

        assertThat(continuationWorkInfo.tags).contains(UpdatesWorkManager.TAG_WORK_USER_INITIATED_UPDATE)
        assertThat(continuationWorkSpec.input.getString(UpdatesWorkManager.INPUT_KEY_CHAIN_ID))
            .isEqualTo(CHAIN_ID)
        assertThat(
            continuationWorkSpec.input.getBoolean(UpdatesWorkManager.INPUT_KEY_IS_MANUAL_CHAIN_CONTINUATION, false)
        ).isTrue()
    }

    @Test
    fun enqueueWork_buildsExpectedPeriodicRequest() {
        UpdatesWorkManager.enqueueWork(context, interval = 6, ExistingPeriodicWorkPolicy.REPLACE)
@@ -132,4 +154,8 @@ class UpdatesWorkManagerTest {
        val workManagerImpl = WorkManagerImpl.getInstance(context)
        return requireNotNull(workManagerImpl.workDatabase.workSpecDao().getWorkSpec(workId.toString()))
    }

    companion object {
        private const val CHAIN_ID = "chain-1"
    }
}
+391 −3

File changed.

Preview size limit exceeded, changes collapsed.

+93 −0
Original line number Diff line number Diff line
package foundation.e.apps.ui.updates

import android.content.Context
import android.view.LayoutInflater
import android.view.ContextThemeWrapper
import androidx.test.core.app.ApplicationProvider
import androidx.work.WorkInfo
import foundation.e.apps.R
import foundation.e.apps.data.application.data.Application
import foundation.e.apps.databinding.FragmentUpdatesBinding
import foundation.e.apps.domain.model.install.Status
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.kotlin.mock
import org.mockito.kotlin.whenever
import org.robolectric.RobolectricTestRunner

@RunWith(RobolectricTestRunner::class)
class UpdatesFragmentTest {
    @Test
    fun updateButtonAvailability_disablesButton_whenManualChainWorkIsActive() {
        val fragment = createFragment()

        setPrivateField(fragment, "displayedUpdates", listOf(Application(status = Status.UPDATABLE)))
        setPrivateField(fragment, "userUpdateWorkInfos", listOf(createWorkInfo(WorkInfo.State.RUNNING)))

        invokePrivateMethod(fragment, "updateButtonAvailability")

        assertFalse(readBinding(fragment).button.isEnabled)
    }

    @Test
    fun updateAllClick_doesNothing_whenManualChainWorkIsAlreadyActive() {
        val fragment = createFragment()
        val binding = readBinding(fragment)

        setPrivateField(fragment, "displayedUpdates", listOf(Application(status = Status.UPDATABLE)))
        setPrivateField(fragment, "userUpdateWorkInfos", listOf(createWorkInfo(WorkInfo.State.ENQUEUED)))
        binding.button.isEnabled = true

        invokePrivateMethod(fragment, "initUpdateAllButton")
        binding.button.performClick()

        assertTrue(binding.button.isEnabled)
    }

    private fun createFragment(): UpdatesFragment {
        val fragment = UpdatesFragment()
        val context = ApplicationProvider.getApplicationContext<Context>()
        val themedContext = ContextThemeWrapper(context, R.style.Theme_Apps)
        val binding = FragmentUpdatesBinding.inflate(LayoutInflater.from(themedContext))
        setPrivateField(fragment, "_binding", binding)
        setPrivateField(fragment, "displayedUpdates", emptyList<Application>())
        setPrivateField(fragment, "periodicUpdateWorkInfos", emptyList<WorkInfo>())
        setPrivateField(fragment, "userUpdateWorkInfos", emptyList<WorkInfo>())
        setPrivateField(fragment, "taggedInstallWorkInfos", emptyList<WorkInfo>())
        setPrivateField(fragment, "legacyInstallWorkInfos", emptyList<WorkInfo>())
        return fragment
    }

    private fun createWorkInfo(state: WorkInfo.State): WorkInfo {
        return mock<WorkInfo>().also { workInfo ->
            whenever(workInfo.state).thenReturn(state)
        }
    }

    private fun readBinding(fragment: UpdatesFragment): FragmentUpdatesBinding {
        return readPrivateField(fragment, "_binding")
    }

    private fun invokePrivateMethod(target: Any, methodName: String) {
        target.javaClass.getDeclaredMethod(methodName).apply {
            isAccessible = true
            invoke(target)
        }
    }

    private fun setPrivateField(target: Any, fieldName: String, value: Any?) {
        target.javaClass.getDeclaredField(fieldName).apply {
            isAccessible = true
            set(target, value)
        }
    }

    @Suppress("UNCHECKED_CAST")
    private fun <T> readPrivateField(target: Any, fieldName: String): T {
        return target.javaClass.getDeclaredField(fieldName).apply {
            isAccessible = true
        }.get(target) as T
    }
}
+0 −3
Original line number Diff line number Diff line
@@ -2,7 +2,6 @@ package foundation.e.apps.ui.updates

import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import foundation.e.apps.data.Stores
import foundation.e.apps.data.application.ApplicationRepository
import foundation.e.apps.data.application.UpdatesDao
import foundation.e.apps.data.application.data.Application
import foundation.e.apps.data.enums.ResultStatus
@@ -60,7 +59,6 @@ class UpdatesViewModelTest {
    @get:Rule
    val instantTaskExecutorRule = InstantTaskExecutorRule()

    private val applicationRepository by lazy { mock<ApplicationRepository>() }
    private val updatesManagerImpl by lazy { mock<UpdatesManagerImpl>() }
    private val sessionRepository by lazy { mock<SessionRepository>() }
    private val stores by lazy { mock<Stores>() }
@@ -99,7 +97,6 @@ class UpdatesViewModelTest {

        updatesViewModel = UpdatesViewModel(
            updatesManagerRepository = updatesManagerRepository,
            applicationRepository = applicationRepository,
            sessionRepository = sessionRepository,
            stores = stores,
            appPreferencesRepository = appPreferencesRepository