Loading app/src/test/java/foundation/e/apps/install/workmanager/InstallHelperTest.kt 0 → 100644 +298 −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.install.workmanager import android.content.Context import android.content.ContextWrapper import android.content.pm.PackageInstaller import android.content.pm.PackageManager import android.os.Build import androidx.work.OneTimeWorkRequestBuilder import androidx.work.Operation import androidx.work.Worker import androidx.work.WorkerParameters import androidx.work.WorkManager import androidx.work.testing.WorkManagerTestInitHelper import androidx.test.core.app.ApplicationProvider import com.google.common.util.concurrent.Futures import foundation.e.apps.data.enums.Status import foundation.e.apps.data.install.AppInstallDAO import foundation.e.apps.data.install.AppManagerWrapper import foundation.e.apps.data.install.models.AppInstall import foundation.e.apps.util.MainCoroutineRule import io.mockk.every import io.mockk.mockkObject import io.mockk.unmockkObject import io.mockk.verify import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.CancellationException import kotlinx.coroutines.flow.emptyFlow import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.test.advanceUntilIdle import kotlinx.coroutines.test.runTest import org.junit.After import org.junit.Before import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import org.mockito.kotlin.any import org.mockito.kotlin.eq import org.mockito.kotlin.mock import org.mockito.kotlin.never import org.mockito.kotlin.times import org.mockito.kotlin.verify as verifyMockito import org.mockito.kotlin.whenever import org.robolectric.RobolectricTestRunner import org.robolectric.annotation.Config import java.util.concurrent.TimeUnit @OptIn(ExperimentalCoroutinesApi::class) @RunWith(RobolectricTestRunner::class) @Config(sdk = [Build.VERSION_CODES.N]) class InstallHelperTest { @get:Rule val mainCoroutineRule = MainCoroutineRule() private val context: Context = ApplicationProvider.getApplicationContext() private val installDao = mock<AppInstallDAO>() private val appManagerWrapper = mock<AppManagerWrapper>() private var isInstallWorkManagerMocked = false @Before fun setup() { WorkManagerTestInitHelper.initializeTestWorkManager(context) } @After fun teardown() { if (isInstallWorkManagerMocked) { unmockkObject(InstallWorkManager) isInstallWorkManagerMocked = false } } @Test fun init_marksStaleDownloadAsInstallationIssue() = runTest { val app = createAppInstall(status = Status.DOWNLOADING) whenever(installDao.getDownloads()).thenReturn(flowOf(listOf(app)), emptyFlow()) whenever(appManagerWrapper.isFusedDownloadInstalled(app)).thenReturn(false) val helper = InstallHelper(context, this, appManagerWrapper, installDao) helper.init() advanceUntilIdle() verifyMockito(appManagerWrapper).installationIssue(app) verifyMockito(appManagerWrapper, never()).updateDownloadStatus(any(), any()) } @Test fun init_updatesStatusToInstalledWhenAppAlreadyInstalled() = runTest { val app = createAppInstall(status = Status.DOWNLOADED) whenever(installDao.getDownloads()).thenReturn(flowOf(listOf(app)), emptyFlow()) whenever(appManagerWrapper.isFusedDownloadInstalled(app)).thenReturn(true) val helper = InstallHelper(context, this, appManagerWrapper, installDao) helper.init() advanceUntilIdle() verifyMockito(appManagerWrapper).updateDownloadStatus(app, Status.INSTALLED) verifyMockito(appManagerWrapper, never()).installationIssue(any()) } @Test fun init_skipsReconciliationWhenInstallSessionIsActive() = runTest { val app = createAppInstall(status = Status.INSTALLING) val packageManager = mock<PackageManager>() val packageInstaller = mock<PackageInstaller>() val sessionInfo = mock<PackageInstaller.SessionInfo>() val wrappedContext = object : ContextWrapper(context) { override fun getPackageManager(): PackageManager = packageManager } whenever(sessionInfo.appPackageName).thenReturn(app.packageName) whenever(sessionInfo.isActive).thenReturn(true) whenever(packageManager.packageInstaller).thenReturn(packageInstaller) whenever(packageInstaller.allSessions).thenReturn(listOf(sessionInfo)) whenever(installDao.getDownloads()).thenReturn(flowOf(listOf(app)), emptyFlow()) val helper = InstallHelper(wrappedContext, this, appManagerWrapper, installDao) helper.init() advanceUntilIdle() verifyMockito(appManagerWrapper, never()).installationIssue(any()) verifyMockito(appManagerWrapper, never()).updateDownloadStatus(any(), any()) } @Test fun init_triggersAwaitingDownloadWhenNoActiveDownloadOrInstall() = runTest { val awaiting = createAppInstall(id = "app.awaiting", status = Status.AWAITING) mockInstallWorkManagerSuccess() whenever(installDao.getDownloads()).thenReturn(flowOf(emptyList()), flowOf(listOf(awaiting))) val helper = InstallHelper(context, this, appManagerWrapper, installDao) helper.init() advanceUntilIdle() verify(exactly = 1) { InstallWorkManager.enqueueWork(awaiting, false) } } @Test fun init_doesNotTriggerAwaitingWhenThereIsAnActiveDownload() = runTest { val active = createAppInstall(id = "app.active", status = Status.DOWNLOADING) val awaiting = createAppInstall(id = "app.awaiting", status = Status.AWAITING) mockInstallWorkManagerSuccess() whenever(installDao.getDownloads()) .thenReturn(flowOf(emptyList()), flowOf(listOf(active, awaiting))) val helper = InstallHelper(context, this, appManagerWrapper, installDao) helper.init() advanceUntilIdle() verify(exactly = 0) { InstallWorkManager.enqueueWork(any(), any()) } } @Test fun init_reportsInstallationIssueWhenEnqueueFails() = runTest { val awaiting = createAppInstall(id = "app.awaiting", status = Status.AWAITING) mockInstallWorkManagerFailure() whenever(installDao.getDownloads()).thenReturn(flowOf(emptyList()), flowOf(listOf(awaiting))) val helper = InstallHelper(context, this, appManagerWrapper, installDao) helper.init() advanceUntilIdle() verifyMockito(appManagerWrapper, times(1)).installationIssue(eq(awaiting)) } @Test fun init_skipsReconciliationWhenActiveWorkExists() = runTest { val app = createAppInstall(id = "app.active.work", status = Status.DOWNLOADING) val request = OneTimeWorkRequestBuilder<NoOpWorker>() .setInitialDelay(1, TimeUnit.HOURS) .addTag(app.id) .build() WorkManager.getInstance(context).enqueue(request).result.get() whenever(installDao.getDownloads()).thenReturn(flowOf(listOf(app)), emptyFlow()) val helper = InstallHelper(context, this, appManagerWrapper, installDao) helper.init() advanceUntilIdle() verifyMockito(appManagerWrapper, never()).installationIssue(any()) verifyMockito(appManagerWrapper, never()).updateDownloadStatus(any(), any()) verifyMockito(appManagerWrapper, never()).isFusedDownloadInstalled(any()) } @Test fun init_doesNotReportIssueWhenEnqueueSucceeds() = runTest { val awaiting = createAppInstall(id = "app.awaiting.success", status = Status.AWAITING) mockInstallWorkManagerSuccess() whenever(installDao.getDownloads()).thenReturn(flowOf(emptyList()), flowOf(listOf(awaiting))) val helper = InstallHelper(context, this, appManagerWrapper, installDao) helper.init() advanceUntilIdle() verifyMockito(appManagerWrapper, never()).installationIssue(any()) } @Test fun init_continuesToObserveWhenReconciliationThrowsException() = runTest { val awaiting = createAppInstall(id = "app.awaiting.after.exception", status = Status.AWAITING) mockInstallWorkManagerSuccess() whenever(installDao.getDownloads()) .thenThrow(RuntimeException("reconcile failed")) .thenReturn(flowOf(listOf(awaiting))) val helper = InstallHelper(context, this, appManagerWrapper, installDao) helper.init() advanceUntilIdle() verify(exactly = 1) { InstallWorkManager.enqueueWork(awaiting, false) } } @Test fun init_stopsAfterCancellationExceptionDuringReconciliation() = runTest { whenever(installDao.getDownloads()).thenThrow(CancellationException("cancel reconcile")) val helper = InstallHelper(context, this, appManagerWrapper, installDao) helper.init() advanceUntilIdle() verifyMockito(installDao, times(1)).getDownloads() } private fun mockInstallWorkManagerSuccess() { mockkObject(InstallWorkManager) isInstallWorkManagerMocked = true every { InstallWorkManager.getUniqueWorkName(any()) } answers { callOriginal() } every { InstallWorkManager.enqueueWork(any(), any()) } returns successfulOperation() } private fun mockInstallWorkManagerFailure() { mockkObject(InstallWorkManager) isInstallWorkManagerMocked = true every { InstallWorkManager.getUniqueWorkName(any()) } answers { callOriginal() } every { InstallWorkManager.enqueueWork(any(), any()) } throws RuntimeException("enqueue failed") } private fun successfulOperation(): Operation { val operation = mock<Operation>() whenever(operation.result).thenReturn(Futures.immediateFuture(Operation.SUCCESS)) return operation } private fun createAppInstall( id: String = "app.id", packageName: String = "foundation.e.app", status: Status, ) = AppInstall( id = id, name = id, packageName = packageName, status = status ) class NoOpWorker( appContext: Context, workerParams: WorkerParameters ) : Worker(appContext, workerParams) { override fun doWork(): Result = Result.success() } } Loading
app/src/test/java/foundation/e/apps/install/workmanager/InstallHelperTest.kt 0 → 100644 +298 −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.install.workmanager import android.content.Context import android.content.ContextWrapper import android.content.pm.PackageInstaller import android.content.pm.PackageManager import android.os.Build import androidx.work.OneTimeWorkRequestBuilder import androidx.work.Operation import androidx.work.Worker import androidx.work.WorkerParameters import androidx.work.WorkManager import androidx.work.testing.WorkManagerTestInitHelper import androidx.test.core.app.ApplicationProvider import com.google.common.util.concurrent.Futures import foundation.e.apps.data.enums.Status import foundation.e.apps.data.install.AppInstallDAO import foundation.e.apps.data.install.AppManagerWrapper import foundation.e.apps.data.install.models.AppInstall import foundation.e.apps.util.MainCoroutineRule import io.mockk.every import io.mockk.mockkObject import io.mockk.unmockkObject import io.mockk.verify import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.CancellationException import kotlinx.coroutines.flow.emptyFlow import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.test.advanceUntilIdle import kotlinx.coroutines.test.runTest import org.junit.After import org.junit.Before import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import org.mockito.kotlin.any import org.mockito.kotlin.eq import org.mockito.kotlin.mock import org.mockito.kotlin.never import org.mockito.kotlin.times import org.mockito.kotlin.verify as verifyMockito import org.mockito.kotlin.whenever import org.robolectric.RobolectricTestRunner import org.robolectric.annotation.Config import java.util.concurrent.TimeUnit @OptIn(ExperimentalCoroutinesApi::class) @RunWith(RobolectricTestRunner::class) @Config(sdk = [Build.VERSION_CODES.N]) class InstallHelperTest { @get:Rule val mainCoroutineRule = MainCoroutineRule() private val context: Context = ApplicationProvider.getApplicationContext() private val installDao = mock<AppInstallDAO>() private val appManagerWrapper = mock<AppManagerWrapper>() private var isInstallWorkManagerMocked = false @Before fun setup() { WorkManagerTestInitHelper.initializeTestWorkManager(context) } @After fun teardown() { if (isInstallWorkManagerMocked) { unmockkObject(InstallWorkManager) isInstallWorkManagerMocked = false } } @Test fun init_marksStaleDownloadAsInstallationIssue() = runTest { val app = createAppInstall(status = Status.DOWNLOADING) whenever(installDao.getDownloads()).thenReturn(flowOf(listOf(app)), emptyFlow()) whenever(appManagerWrapper.isFusedDownloadInstalled(app)).thenReturn(false) val helper = InstallHelper(context, this, appManagerWrapper, installDao) helper.init() advanceUntilIdle() verifyMockito(appManagerWrapper).installationIssue(app) verifyMockito(appManagerWrapper, never()).updateDownloadStatus(any(), any()) } @Test fun init_updatesStatusToInstalledWhenAppAlreadyInstalled() = runTest { val app = createAppInstall(status = Status.DOWNLOADED) whenever(installDao.getDownloads()).thenReturn(flowOf(listOf(app)), emptyFlow()) whenever(appManagerWrapper.isFusedDownloadInstalled(app)).thenReturn(true) val helper = InstallHelper(context, this, appManagerWrapper, installDao) helper.init() advanceUntilIdle() verifyMockito(appManagerWrapper).updateDownloadStatus(app, Status.INSTALLED) verifyMockito(appManagerWrapper, never()).installationIssue(any()) } @Test fun init_skipsReconciliationWhenInstallSessionIsActive() = runTest { val app = createAppInstall(status = Status.INSTALLING) val packageManager = mock<PackageManager>() val packageInstaller = mock<PackageInstaller>() val sessionInfo = mock<PackageInstaller.SessionInfo>() val wrappedContext = object : ContextWrapper(context) { override fun getPackageManager(): PackageManager = packageManager } whenever(sessionInfo.appPackageName).thenReturn(app.packageName) whenever(sessionInfo.isActive).thenReturn(true) whenever(packageManager.packageInstaller).thenReturn(packageInstaller) whenever(packageInstaller.allSessions).thenReturn(listOf(sessionInfo)) whenever(installDao.getDownloads()).thenReturn(flowOf(listOf(app)), emptyFlow()) val helper = InstallHelper(wrappedContext, this, appManagerWrapper, installDao) helper.init() advanceUntilIdle() verifyMockito(appManagerWrapper, never()).installationIssue(any()) verifyMockito(appManagerWrapper, never()).updateDownloadStatus(any(), any()) } @Test fun init_triggersAwaitingDownloadWhenNoActiveDownloadOrInstall() = runTest { val awaiting = createAppInstall(id = "app.awaiting", status = Status.AWAITING) mockInstallWorkManagerSuccess() whenever(installDao.getDownloads()).thenReturn(flowOf(emptyList()), flowOf(listOf(awaiting))) val helper = InstallHelper(context, this, appManagerWrapper, installDao) helper.init() advanceUntilIdle() verify(exactly = 1) { InstallWorkManager.enqueueWork(awaiting, false) } } @Test fun init_doesNotTriggerAwaitingWhenThereIsAnActiveDownload() = runTest { val active = createAppInstall(id = "app.active", status = Status.DOWNLOADING) val awaiting = createAppInstall(id = "app.awaiting", status = Status.AWAITING) mockInstallWorkManagerSuccess() whenever(installDao.getDownloads()) .thenReturn(flowOf(emptyList()), flowOf(listOf(active, awaiting))) val helper = InstallHelper(context, this, appManagerWrapper, installDao) helper.init() advanceUntilIdle() verify(exactly = 0) { InstallWorkManager.enqueueWork(any(), any()) } } @Test fun init_reportsInstallationIssueWhenEnqueueFails() = runTest { val awaiting = createAppInstall(id = "app.awaiting", status = Status.AWAITING) mockInstallWorkManagerFailure() whenever(installDao.getDownloads()).thenReturn(flowOf(emptyList()), flowOf(listOf(awaiting))) val helper = InstallHelper(context, this, appManagerWrapper, installDao) helper.init() advanceUntilIdle() verifyMockito(appManagerWrapper, times(1)).installationIssue(eq(awaiting)) } @Test fun init_skipsReconciliationWhenActiveWorkExists() = runTest { val app = createAppInstall(id = "app.active.work", status = Status.DOWNLOADING) val request = OneTimeWorkRequestBuilder<NoOpWorker>() .setInitialDelay(1, TimeUnit.HOURS) .addTag(app.id) .build() WorkManager.getInstance(context).enqueue(request).result.get() whenever(installDao.getDownloads()).thenReturn(flowOf(listOf(app)), emptyFlow()) val helper = InstallHelper(context, this, appManagerWrapper, installDao) helper.init() advanceUntilIdle() verifyMockito(appManagerWrapper, never()).installationIssue(any()) verifyMockito(appManagerWrapper, never()).updateDownloadStatus(any(), any()) verifyMockito(appManagerWrapper, never()).isFusedDownloadInstalled(any()) } @Test fun init_doesNotReportIssueWhenEnqueueSucceeds() = runTest { val awaiting = createAppInstall(id = "app.awaiting.success", status = Status.AWAITING) mockInstallWorkManagerSuccess() whenever(installDao.getDownloads()).thenReturn(flowOf(emptyList()), flowOf(listOf(awaiting))) val helper = InstallHelper(context, this, appManagerWrapper, installDao) helper.init() advanceUntilIdle() verifyMockito(appManagerWrapper, never()).installationIssue(any()) } @Test fun init_continuesToObserveWhenReconciliationThrowsException() = runTest { val awaiting = createAppInstall(id = "app.awaiting.after.exception", status = Status.AWAITING) mockInstallWorkManagerSuccess() whenever(installDao.getDownloads()) .thenThrow(RuntimeException("reconcile failed")) .thenReturn(flowOf(listOf(awaiting))) val helper = InstallHelper(context, this, appManagerWrapper, installDao) helper.init() advanceUntilIdle() verify(exactly = 1) { InstallWorkManager.enqueueWork(awaiting, false) } } @Test fun init_stopsAfterCancellationExceptionDuringReconciliation() = runTest { whenever(installDao.getDownloads()).thenThrow(CancellationException("cancel reconcile")) val helper = InstallHelper(context, this, appManagerWrapper, installDao) helper.init() advanceUntilIdle() verifyMockito(installDao, times(1)).getDownloads() } private fun mockInstallWorkManagerSuccess() { mockkObject(InstallWorkManager) isInstallWorkManagerMocked = true every { InstallWorkManager.getUniqueWorkName(any()) } answers { callOriginal() } every { InstallWorkManager.enqueueWork(any(), any()) } returns successfulOperation() } private fun mockInstallWorkManagerFailure() { mockkObject(InstallWorkManager) isInstallWorkManagerMocked = true every { InstallWorkManager.getUniqueWorkName(any()) } answers { callOriginal() } every { InstallWorkManager.enqueueWork(any(), any()) } throws RuntimeException("enqueue failed") } private fun successfulOperation(): Operation { val operation = mock<Operation>() whenever(operation.result).thenReturn(Futures.immediateFuture(Operation.SUCCESS)) return operation } private fun createAppInstall( id: String = "app.id", packageName: String = "foundation.e.app", status: Status, ) = AppInstall( id = id, name = id, packageName = packageName, status = status ) class NoOpWorker( appContext: Context, workerParams: WorkerParameters ) : Worker(appContext, workerParams) { override fun doWork(): Result = Result.success() } }