Merge pull request #231 from JMR-dev/test-cache-lock-gate-coverage
test(sync): cover the cache-lock gate on SyncWorker/SendWorker + the provisioner await
This commit was merged in pull request #231.
This commit is contained in:
@@ -28,6 +28,7 @@ import org.libremail.data.settings.SettingsRepository
|
||||
import java.io.File
|
||||
import java.util.concurrent.Executors
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFalse
|
||||
|
||||
/**
|
||||
* [DatabaseProvisioner] holds the one-time startup sequence that `DatabaseModule.provideDatabase` used
|
||||
@@ -97,6 +98,28 @@ class DatabaseProvisionerTest {
|
||||
verify(exactly = 1) { DatabaseEncryption.ensureNativeLibraryLoaded() }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `prepareCache suspends on the auth-bound passphrase until it resolves`() = runTest {
|
||||
every { settingsRepository.settings } returns flowOf(AppSettings(encryptCache = true, appLock = true))
|
||||
val enteredResolve = CompletableDeferred<Unit>()
|
||||
val releasePassphrase = CompletableDeferred<String>()
|
||||
// Model the auth-sealed key: resolvePassphrase parks until the user authenticates. Keeping this
|
||||
// await off a background thread is exactly why the pre-auth DB entry points gate on
|
||||
// EncryptedCacheGuard — this pins that prepareCache really does block on it.
|
||||
coEvery { keyStore.resolvePassphrase(true) } coAnswers {
|
||||
enteredResolve.complete(Unit)
|
||||
releasePassphrase.await()
|
||||
}
|
||||
|
||||
val prepared = async { provisioner().prepareCache() }
|
||||
enteredResolve.await() // the startup sequence has reached the passphrase await
|
||||
|
||||
assertFalse(prepared.isCompleted, "prepareCache must not complete while the passphrase is unresolved")
|
||||
|
||||
releasePassphrase.complete(PASSPHRASE)
|
||||
assertEquals(CacheOpenMode.Encrypted(PASSPHRASE), prepared.await())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `a pending clear wipes and resets the seals before the migrator runs`() = runTest {
|
||||
coEvery { keyStore.isClearPending() } returns true
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
package org.libremail.data.sync
|
||||
|
||||
import androidx.work.ListenableWorker.Result
|
||||
import dagger.Lazy
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.coVerify
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import io.mockk.verify
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
import org.libremail.data.attachment.AttachmentUriGrants
|
||||
import org.libremail.data.local.dao.AccountDao
|
||||
import org.libremail.data.local.dao.OutboxDao
|
||||
import org.libremail.data.security.EncryptedCacheGuard
|
||||
import org.libremail.mail.GraphSender
|
||||
import org.libremail.mail.SmtpSender
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
/**
|
||||
* [SendWorker] already gates on [EncryptedCacheGuard]; this locks that invariant in — while the cache
|
||||
* is locked it must defer without resolving any of its (`Lazy`) DB-backed dependencies (resolving any
|
||||
* of them opens the Room DB, which blocks on the passphrase await).
|
||||
*/
|
||||
class SendWorkerTest {
|
||||
|
||||
private val outboxDao = mockk<OutboxDao>()
|
||||
private val accountDao = mockk<AccountDao>()
|
||||
private val connectionFactory = mockk<MailConnectionFactory>()
|
||||
private val attachmentUriGrants = mockk<AttachmentUriGrants>()
|
||||
private val lazyOutbox = mockk<Lazy<OutboxDao>> { every { get() } returns outboxDao }
|
||||
private val lazyAccount = mockk<Lazy<AccountDao>> { every { get() } returns accountDao }
|
||||
private val lazyConnection = mockk<Lazy<MailConnectionFactory>> { every { get() } returns connectionFactory }
|
||||
private val lazyGrants = mockk<Lazy<AttachmentUriGrants>> { every { get() } returns attachmentUriGrants }
|
||||
private val cacheGuard = mockk<EncryptedCacheGuard>()
|
||||
|
||||
private fun worker() = SendWorker(
|
||||
mockk(relaxed = true),
|
||||
mockk(relaxed = true),
|
||||
lazyOutbox,
|
||||
lazyAccount,
|
||||
mockk<SmtpSender>(),
|
||||
mockk<GraphSender>(),
|
||||
lazyConnection,
|
||||
cacheGuard,
|
||||
lazyGrants,
|
||||
)
|
||||
|
||||
@Test
|
||||
fun `retries without resolving any DB dependency when the cache is locked`() = runTest {
|
||||
coEvery { cacheGuard.isCacheLocked() } returns true
|
||||
|
||||
assertEquals(Result.retry(), worker().doWork())
|
||||
|
||||
verify(exactly = 0) { lazyOutbox.get() }
|
||||
verify(exactly = 0) { lazyAccount.get() }
|
||||
verify(exactly = 0) { lazyConnection.get() }
|
||||
verify(exactly = 0) { lazyGrants.get() }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `drains the outbox when the cache is unlocked`() = runTest {
|
||||
coEvery { cacheGuard.isCacheLocked() } returns false
|
||||
coEvery { outboxDao.getAll() } returns emptyList()
|
||||
|
||||
assertEquals(Result.success(), worker().doWork())
|
||||
|
||||
coVerify(exactly = 1) { outboxDao.getAll() }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
package org.libremail.data.sync
|
||||
|
||||
import androidx.work.ListenableWorker
|
||||
import dagger.Lazy
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.coVerify
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import io.mockk.verify
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
import org.libremail.data.security.EncryptedCacheGuard
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
/**
|
||||
* [SyncWorker] already gates on [EncryptedCacheGuard]; this locks that invariant in as regression
|
||||
* cover for the class of bug fixed in PruneWorker/BackfillWorker — while the cache is locked it must
|
||||
* defer without resolving the (`Lazy`) [MailSyncer] (whose construction opens the Room DB).
|
||||
*/
|
||||
class SyncWorkerTest {
|
||||
|
||||
private val mailSyncer = mockk<MailSyncer>()
|
||||
private val lazySyncer = mockk<Lazy<MailSyncer>> { every { get() } returns mailSyncer }
|
||||
private val cacheGuard = mockk<EncryptedCacheGuard>()
|
||||
|
||||
private fun worker() = SyncWorker(mockk(relaxed = true), mockk(relaxed = true), lazySyncer, cacheGuard)
|
||||
|
||||
@Test
|
||||
fun `retries without resolving the syncer when the cache is locked`() = runTest {
|
||||
coEvery { cacheGuard.isCacheLocked() } returns true
|
||||
|
||||
assertEquals(ListenableWorker.Result.retry(), worker().doWork())
|
||||
|
||||
verify(exactly = 0) { lazySyncer.get() }
|
||||
coVerify(exactly = 0) { mailSyncer.syncAll() }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `syncs and succeeds when the cache is unlocked`() = runTest {
|
||||
coEvery { cacheGuard.isCacheLocked() } returns false
|
||||
coEvery { mailSyncer.syncAll() } returns Result.success(0)
|
||||
|
||||
assertEquals(ListenableWorker.Result.success(), worker().doWork())
|
||||
|
||||
coVerify(exactly = 1) { mailSyncer.syncAll() }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `retries when syncing fails`() = runTest {
|
||||
coEvery { cacheGuard.isCacheLocked() } returns false
|
||||
coEvery { mailSyncer.syncAll() } returns Result.failure(IllegalStateException("boom"))
|
||||
|
||||
assertEquals(ListenableWorker.Result.retry(), worker().doWork())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user