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

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

test: capture AppInstallProcessor baseline tests for existing behaviour

Lock down the current enqueue, worker, and update-completion quirks before extraction so later class splits can be checked against explicit behavior instead of assumptions.
parent 42c5b794
Loading
Loading
Loading
Loading
+244 −8
Original line number Diff line number Diff line
@@ -20,10 +20,14 @@ package foundation.e.apps.installProcessor

import android.content.Context
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import com.aurora.gplayapi.data.models.AuthData
import foundation.e.apps.data.ResultSupreme
import foundation.e.apps.R
import foundation.e.apps.data.application.ApplicationRepository
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.event.AppEvent
import foundation.e.apps.data.fdroid.FDroidRepository
import foundation.e.apps.data.install.AppInstallComponents
import foundation.e.apps.data.install.AppInstallRepository
@@ -31,22 +35,29 @@ import foundation.e.apps.data.install.AppManager
import foundation.e.apps.data.install.AppManagerWrapper
import foundation.e.apps.data.install.models.AppInstall
import foundation.e.apps.data.install.notification.StorageNotificationManager
import foundation.e.apps.data.install.workmanager.AppInstallProcessor
import foundation.e.apps.data.preference.PlayStoreAuthStore
import foundation.e.apps.data.install.wrapper.AppEventDispatcher
import foundation.e.apps.data.install.wrapper.NetworkStatusChecker
import foundation.e.apps.data.install.wrapper.ParentalControlAuthGateway
import foundation.e.apps.data.install.wrapper.StorageSpaceChecker
import foundation.e.apps.data.install.wrapper.UpdatesNotificationSender
import foundation.e.apps.data.install.wrapper.UpdatesTracker
import foundation.e.apps.data.install.workmanager.AppInstallProcessor
import foundation.e.apps.data.install.workmanager.InstallWorkManager
import foundation.e.apps.data.playstore.utils.GplayHttpRequestException
import foundation.e.apps.data.preference.PlayStoreAuthStore
import foundation.e.apps.domain.ValidateAppAgeLimitUseCase
import foundation.e.apps.domain.model.ContentRatingValidity
import foundation.e.apps.domain.model.User
import foundation.e.apps.domain.preferences.SessionRepository
import foundation.e.apps.util.MainCoroutineRule
import io.mockk.coEvery
import io.mockk.coVerify
import io.mockk.every
import io.mockk.justRun
import io.mockk.mockk
import io.mockk.mockkObject
import io.mockk.unmockkObject
import io.mockk.verify
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest
import org.junit.Assert.assertEquals
@@ -57,6 +68,7 @@ import org.junit.Test
import org.mockito.Mock
import org.mockito.Mockito
import org.mockito.MockitoAnnotations
import java.util.Locale

@OptIn(ExperimentalCoroutinesApi::class)
class AppInstallProcessorTest {
@@ -80,16 +92,12 @@ class AppInstallProcessorTest {
    @Mock
    private lateinit var fakeFDroidRepository: FDroidRepository

    @Mock
    private lateinit var context: Context

    @Mock
    private lateinit var sessionRepository: SessionRepository

    @Mock
    private lateinit var playStoreAuthStore: PlayStoreAuthStore

    @Mock
    private lateinit var applicationRepository: ApplicationRepository

    private lateinit var appInstallProcessor: AppInstallProcessor
@@ -100,7 +108,7 @@ class AppInstallProcessorTest {
    @Mock
    private lateinit var storageNotificationManager: StorageNotificationManager

    private lateinit var appEventDispatcher: AppEventDispatcher
    private lateinit var appEventDispatcher: RecordingAppEventDispatcher
    private lateinit var storageSpaceChecker: StorageSpaceChecker
    private lateinit var parentalControlAuthGateway: ParentalControlAuthGateway
    private lateinit var updatesTracker: UpdatesTracker
@@ -110,7 +118,13 @@ class AppInstallProcessorTest {
    @Before
    fun setup() {
        MockitoAnnotations.openMocks(this)
        appEventDispatcher = mockk(relaxed = true)
        context = mockk(relaxed = true)
        sessionRepository = mockk(relaxed = true)
        playStoreAuthStore = mockk(relaxed = true)
        applicationRepository = mockk(relaxed = true)
        coEvery { sessionRepository.awaitUser() } returns User.NO_GOOGLE
        coEvery { playStoreAuthStore.awaitAuthData() } returns null
        appEventDispatcher = RecordingAppEventDispatcher()
        storageSpaceChecker = mockk(relaxed = true)
        parentalControlAuthGateway = mockk(relaxed = true)
        updatesTracker = mockk(relaxed = true)
@@ -242,6 +256,185 @@ class AppInstallProcessorTest {
        assertEquals("processInstall", finalFusedDownload, null)
    }

    @Test
    fun `enqueueFusedDownload warns anonymous paid users without aborting`() = runTest {
        val appInstall = AppInstall(
            type = foundation.e.apps.data.enums.Type.PWA,
            id = "123",
            status = Status.AWAITING,
            downloadURLList = mutableListOf("apk"),
            packageName = "com.example.paid",
            isFree = false
        )
        val appManagerWrapper = mockk<AppManagerWrapper>(relaxed = true)
        val processor = createProcessorForCanEnqueue(appManagerWrapper)

        mockkObject(InstallWorkManager)
        try {
            coEvery { sessionRepository.awaitUser() } returns User.ANONYMOUS
            coEvery {
                playStoreAuthStore.awaitAuthData()
            } returns AuthData(email = "anon@example.com", isAnonymous = true)
            coEvery { appManagerWrapper.addDownload(appInstall) } returns true
            Mockito.`when`(validateAppAgeRatingUseCase(appInstall))
                .thenReturn(ResultSupreme.create(ResultStatus.OK, ContentRatingValidity(true)))
            every { networkStatusChecker.isNetworkAvailable() } returns true
            every { storageSpaceChecker.spaceMissing(appInstall) } returns 0L
            justRun { InstallWorkManager.enqueueWork(any(), any(), any()) }

            val result = processor.enqueueFusedDownload(appInstall)

            assertTrue(result)
            assertTrue(appEventDispatcher.events.any {
                it is AppEvent.ErrorMessageEvent && it.data == R.string.paid_app_anonymous_message
            })
            coVerify { appManagerWrapper.updateAwaiting(appInstall) }
            verify { InstallWorkManager.enqueueWork(context, appInstall, false) }
        } finally {
            unmockkObject(InstallWorkManager)
        }
    }

    @Test
    fun `canEnqueue refreshes download urls for non-PWA installs`() = runTest {
        val appInstall = AppInstall(
            type = foundation.e.apps.data.enums.Type.NATIVE,
            source = Source.PLAY_STORE,
            id = "123",
            status = Status.AWAITING,
            packageName = "com.example.app"
        )

        val appManagerWrapper = mockk<AppManagerWrapper>(relaxed = true)
        val processor = createProcessorForCanEnqueue(appManagerWrapper)

        coEvery { appManagerWrapper.addDownload(appInstall) } returns true
        Mockito.`when`(validateAppAgeRatingUseCase(appInstall))
            .thenReturn(ResultSupreme.create(ResultStatus.OK, ContentRatingValidity(true)))
        every { networkStatusChecker.isNetworkAvailable() } returns true
        every { storageSpaceChecker.spaceMissing(appInstall) } returns 0L

        val result = processor.canEnqueue(appInstall)

        assertTrue(result)
        coVerify { applicationRepository.updateFusedDownloadWithDownloadingInfo(Source.PLAY_STORE, appInstall) }
        coVerify { appManagerWrapper.addDownload(appInstall) }
    }

    @Test
    fun `canEnqueue returns false when download url refresh throws http error`() = runTest {
        val appInstall = AppInstall(
            type = foundation.e.apps.data.enums.Type.NATIVE,
            source = Source.PLAY_STORE,
            id = "123",
            status = Status.AWAITING,
            packageName = "com.example.app"
        )

        val appManagerWrapper = mockk<AppManagerWrapper>(relaxed = true)
        val processor = createProcessorForCanEnqueue(appManagerWrapper)

        coEvery {
            applicationRepository.updateFusedDownloadWithDownloadingInfo(Source.PLAY_STORE, appInstall)
        } throws GplayHttpRequestException(403, "forbidden")

        val result = processor.canEnqueue(appInstall)

        assertEquals(false, result)
        assertTrue(appEventDispatcher.events.any { it is AppEvent.UpdateEvent })
        coVerify(exactly = 0) { appManagerWrapper.addDownload(appInstall) }
    }

    @Test
    fun `canEnqueue keeps going when download url refresh throws illegal state`() = runTest {
        val appInstall = AppInstall(
            type = foundation.e.apps.data.enums.Type.NATIVE,
            source = Source.PLAY_STORE,
            id = "123",
            status = Status.AWAITING,
            packageName = "com.example.app"
        )

        val appManagerWrapper = mockk<AppManagerWrapper>(relaxed = true)
        val processor = createProcessorForCanEnqueue(appManagerWrapper)

        coEvery {
            applicationRepository.updateFusedDownloadWithDownloadingInfo(Source.PLAY_STORE, appInstall)
        } throws IllegalStateException("boom")
        coEvery { appManagerWrapper.addDownload(appInstall) } returns true
        Mockito.`when`(validateAppAgeRatingUseCase(appInstall))
            .thenReturn(ResultSupreme.create(ResultStatus.OK, ContentRatingValidity(true)))
        every { networkStatusChecker.isNetworkAvailable() } returns true
        every { storageSpaceChecker.spaceMissing(appInstall) } returns 0L

        val result = processor.canEnqueue(appInstall)

        assertTrue(result)
        coVerify { appManagerWrapper.addDownload(appInstall) }
    }

    @Test
    fun `processInstall returns success when internal exception occurs`() = runTest {
        val fusedDownload = initTest()
        fakeFusedManagerRepository.forceCrash = true

        val result = appInstallProcessor.processInstall(fusedDownload.id, false) {
            // _ignored_
        }

        assertTrue(result.isSuccess)
        assertEquals(ResultStatus.OK, result.getOrNull())
    }

    @Test
    fun `processInstall enters foreground before download starts`() = runTest {
        val fusedDownload = initTest()
        var statusAtForeground: Status? = null

        appInstallProcessor.processInstall(fusedDownload.id, false) {
            statusAtForeground = fusedDownload.status
        }

        assertEquals(Status.AWAITING, statusAtForeground)
    }

    @Test
    fun `processInstall update completion ignores installation issues`() = runTest {
        stubUpdateNotificationContext()
        every { updatesTracker.hasSuccessfulUpdatedApps() } returns true
        every { updatesTracker.successfulUpdatedAppsCount() } returns 1

        processCompletedUpdate(Status.INSTALLATION_ISSUE)

        verify { updatesTracker.addSuccessfullyUpdatedApp(any()) }
        verify { updatesNotificationSender.showNotification("Update", "Updated message") }
        verify { updatesTracker.clearSuccessfullyUpdatedApps() }
    }

    @Test
    fun `processInstall update completion ignores purchase needed`() = runTest {
        stubUpdateNotificationContext()
        every { updatesTracker.hasSuccessfulUpdatedApps() } returns true
        every { updatesTracker.successfulUpdatedAppsCount() } returns 1

        processCompletedUpdate(Status.PURCHASE_NEEDED)

        verify { updatesTracker.addSuccessfullyUpdatedApp(any()) }
        verify { updatesNotificationSender.showNotification("Update", "Updated message") }
        verify { updatesTracker.clearSuccessfullyUpdatedApps() }
    }

    @Test
    fun `processInstall clears tracked updates after final notification`() = runTest {
        stubUpdateNotificationContext()
        every { updatesTracker.hasSuccessfulUpdatedApps() } returns true
        every { updatesTracker.successfulUpdatedAppsCount() } returns 1

        processCompletedUpdate()

        verify { updatesTracker.clearSuccessfullyUpdatedApps() }
    }

    @Test
    fun canEnqueue_returnsTrueWhenAllChecksPass() = runTest {
        val appInstall = AppInstall(
@@ -375,6 +568,41 @@ class AppInstallProcessorTest {
        return fakeFusedDownloadDAO.getDownloadById(appInstall.id)
    }

    private suspend fun processCompletedUpdate(ignoredStatus: Status? = null) {
        val fusedDownload = initTest()
        fakeFusedManagerRepository.isAppInstalled = true

        ignoredStatus?.let {
            fakeFusedDownloadDAO.addDownload(
                AppInstall(
                    id = "ignored-$it",
                    status = it,
                    downloadURLList = mutableListOf("apk"),
                    packageName = "com.example.$it"
                )
            )
        }

        appInstallProcessor.processInstall(fusedDownload.id, true) {
            // _ignored_
        }
    }

    private suspend fun stubUpdateNotificationContext() {
        val authData = AuthData(email = "user@example.com", isAnonymous = false).apply {
            locale = Locale.US
        }
        coEvery { playStoreAuthStore.awaitAuthData() } returns authData
        every { context.getString(R.string.update) } returns "Update"
        every {
            context.getString(
                R.string.message_last_update_triggered,
                any(),
                any()
            )
        } returns "Updated message"
    }

    private fun createFusedDownload(
        packageName: String? = null,
        downloadUrlList: MutableList<String>? = null
@@ -407,3 +635,11 @@ class AppInstallProcessorTest {
        )
    }
}

private class RecordingAppEventDispatcher : AppEventDispatcher {
    val events = mutableListOf<AppEvent>()

    override suspend fun dispatch(event: AppEvent) {
        events.add(event)
    }
}