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

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

test: cover download refresh failure handling

Pin the refresh error branches introduced by the install refactor so purchase and retry flows keep their current side effects.
parent eb3d947a
Loading
Loading
Loading
Loading
+181 −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.installProcessor

import com.aurora.gplayapi.exceptions.InternalException
import foundation.e.apps.data.enums.Source
import foundation.e.apps.data.enums.Type
import foundation.e.apps.data.event.AppEvent
import foundation.e.apps.data.application.ApplicationRepository
import foundation.e.apps.data.install.AppInstallRepository
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.workmanager.AppInstallDownloadUrlRefresher
import foundation.e.apps.data.playstore.utils.GplayHttpRequestException
import foundation.e.apps.domain.model.install.Status
import io.mockk.coEvery
import io.mockk.coVerify
import io.mockk.mockk
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest
import kotlin.test.assertFailsWith
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test

@OptIn(ExperimentalCoroutinesApi::class)
class AppInstallDownloadUrlRefresherTest {
    private lateinit var applicationRepository: ApplicationRepository
    private lateinit var appInstallRepository: AppInstallRepository
    private lateinit var appManagerWrapper: AppManagerWrapper
    private lateinit var appEventDispatcher: FakeAppEventDispatcher
    private lateinit var appManager: AppManager
    private lateinit var refresher: AppInstallDownloadUrlRefresher

    @Before
    fun setup() {
        applicationRepository = mockk(relaxed = true)
        appInstallRepository = mockk(relaxed = true)
        appManagerWrapper = mockk(relaxed = true)
        appEventDispatcher = FakeAppEventDispatcher()
        appManager = mockk(relaxed = true)
        refresher = AppInstallDownloadUrlRefresher(
            applicationRepository,
            appInstallRepository,
            appManagerWrapper,
            appEventDispatcher,
            appManager
        )
    }

    @Test
    fun updateDownloadUrls_returnsTrueWhenRefreshSucceeds() = runTest {
        val appInstall = createNativeInstall()
        coEvery {
            applicationRepository.updateFusedDownloadWithDownloadingInfo(Source.PLAY_STORE, appInstall)
        } returns Unit

        val result = refresher.updateDownloadUrls(appInstall, false)

        assertTrue(result)
        coVerify(exactly = 0) { appManagerWrapper.installationIssue(any()) }
    }

    @Test
    fun updateDownloadUrls_handlesFreeAppNotPurchasedAsRestricted() = runTest {
        val appInstall = createNativeInstall(isFree = true)
        coEvery {
            applicationRepository.updateFusedDownloadWithDownloadingInfo(Source.PLAY_STORE, appInstall)
        } throws InternalException.AppNotPurchased()

        val result = refresher.updateDownloadUrls(appInstall, false)

        assertFalse(result)
        assertTrue(appEventDispatcher.events.any { it is AppEvent.AppRestrictedOrUnavailable })
        coVerify { appManager.addDownload(appInstall) }
        coVerify { appManager.updateUnavailable(appInstall) }
        coVerify(exactly = 0) { appManagerWrapper.addFusedDownloadPurchaseNeeded(any()) }
    }

    @Test
    fun updateDownloadUrls_handlesPaidAppNotPurchasedAsPurchaseNeeded() = runTest {
        val appInstall = createNativeInstall(isFree = false)
        coEvery {
            applicationRepository.updateFusedDownloadWithDownloadingInfo(Source.PLAY_STORE, appInstall)
        } throws InternalException.AppNotPurchased()

        val result = refresher.updateDownloadUrls(appInstall, false)

        assertFalse(result)
        coVerify { appManagerWrapper.addFusedDownloadPurchaseNeeded(appInstall) }
        assertTrue(appEventDispatcher.events.any { it is AppEvent.AppPurchaseEvent })
        coVerify(exactly = 0) { appManager.addDownload(any()) }
    }

    @Test
    fun updateDownloadUrls_recordsIssueWhenHttpRefreshFails() = runTest {
        val appInstall = createNativeInstall()
        coEvery { appInstallRepository.getDownloadById(appInstall.id) } returns null
        coEvery {
            applicationRepository.updateFusedDownloadWithDownloadingInfo(Source.PLAY_STORE, appInstall)
        } throws GplayHttpRequestException(403, "forbidden")

        val result = refresher.updateDownloadUrls(appInstall, false)

        assertFalse(result)
        coVerify { appInstallRepository.addDownload(appInstall) }
        coVerify { appManagerWrapper.installationIssue(appInstall) }
        assertTrue(appEventDispatcher.events.none { it is AppEvent.UpdateEvent })
    }

    @Test
    fun updateDownloadUrls_dispatchesUpdateEventWhenUpdateRefreshFails() = runTest {
        val appInstall = createNativeInstall()
        coEvery { appInstallRepository.getDownloadById(appInstall.id) } returns null
        coEvery {
            applicationRepository.updateFusedDownloadWithDownloadingInfo(Source.PLAY_STORE, appInstall)
        } throws IllegalStateException("boom")

        val result = refresher.updateDownloadUrls(appInstall, true)

        assertFalse(result)
        coVerify { appInstallRepository.addDownload(appInstall) }
        coVerify { appManagerWrapper.installationIssue(appInstall) }
        assertTrue(appEventDispatcher.events.any { it is AppEvent.UpdateEvent })
    }

    @Test
    fun updateDownloadUrls_doesNotDuplicateExistingDownloadOnFailure() = runTest {
        val appInstall = createNativeInstall()
        coEvery { appInstallRepository.getDownloadById(appInstall.id) } returns appInstall
        coEvery {
            applicationRepository.updateFusedDownloadWithDownloadingInfo(Source.PLAY_STORE, appInstall)
        } throws IllegalStateException("boom")

        val result = refresher.updateDownloadUrls(appInstall, false)

        assertFalse(result)
        coVerify(exactly = 0) { appInstallRepository.addDownload(any()) }
        coVerify { appManagerWrapper.installationIssue(appInstall) }
    }

    @Test
    fun updateDownloadUrls_rethrowsCancellation() = runTest {
        val appInstall = createNativeInstall()
        coEvery {
            applicationRepository.updateFusedDownloadWithDownloadingInfo(Source.PLAY_STORE, appInstall)
        } throws CancellationException("cancelled")

        assertFailsWith<CancellationException> {
            refresher.updateDownloadUrls(appInstall, false)
        }
    }

    private fun createNativeInstall(isFree: Boolean = true) = AppInstall(
        type = Type.NATIVE,
        source = Source.PLAY_STORE,
        id = "123",
        status = Status.AWAITING,
        packageName = "com.example.app",
        isFree = isFree
    )
}