Merge main into feat-155-yahoo-app-password

This commit is contained in:
Jason Ross
2026-07-02 19:44:32 -05:00
committed by GitHub
9 changed files with 165 additions and 1 deletions
@@ -0,0 +1,40 @@
// SPDX-License-Identifier: GPL-3.0-or-later
package org.libremail.data.settings
import androidx.datastore.preferences.core.emptyPreferences
import androidx.datastore.preferences.core.preferencesOf
import androidx.datastore.preferences.core.stringPreferencesKey
import org.junit.Test
import kotlin.test.assertEquals
import kotlin.test.assertNull
/**
* The user-chosen default account (#163), covered at the `toAppSettings()` mapping layer:
* [SettingsRepository] itself needs a real `Context` for its DataStore, so (as with [AppSettingsTest]'s
* coverage of [FetchPolicy]) the JVM-testable seam is the plain `Preferences` -> [AppSettings] mapping
* that [SettingsRepository.setDefaultAccountId] and [SettingsRepository.clearDefaultAccountId] are
* ultimately read back through.
*/
class SettingsRepositoryTest {
private val defaultAccountIdKey = stringPreferencesKey("default_account_id")
@Test
fun `in-memory default has no default account`() {
assertNull(AppSettings().defaultAccountId)
}
@Test
fun `a persisted default account id round-trips`() {
val prefs = preferencesOf(defaultAccountIdKey to "imap:me@example.org")
assertEquals("imap:me@example.org", prefs.toAppSettings().defaultAccountId)
}
@Test
fun `an absent key reads back as no default (the cleared state)`() {
// setDefaultAccountId(null) and clearDefaultAccountId both remove the key rather than store a
// sentinel value, so "cleared" and "never set" are indistinguishable and both map to null here.
assertNull(emptyPreferences().toAppSettings().defaultAccountId)
}
}
@@ -10,6 +10,7 @@ import io.mockk.slot
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.test.UnconfinedTestDispatcher
import kotlinx.coroutines.test.resetMain
import kotlinx.coroutines.test.runTest
@@ -18,6 +19,8 @@ import org.junit.After
import org.junit.Before
import org.junit.Test
import org.libremail.data.settings.AccountSettingsRepository
import org.libremail.data.settings.AppSettings
import org.libremail.data.settings.SettingsRepository
import org.libremail.data.settings.SignatureRepository
import org.libremail.domain.model.Account
import org.libremail.domain.model.AccountSettings
@@ -66,6 +69,7 @@ class ComposeViewModelTest {
signatures: Map<String, Signature> = emptyMap(),
settings: Map<String, AccountSettings> = emptyMap(),
mailRepository: MailRepository = mockk(relaxed = true),
defaultAccountId: String? = null,
): ComposeViewModel {
val accountRepository = mockk<AccountRepository>()
every { accountRepository.observeAccounts() } returns MutableStateFlow(accounts)
@@ -76,6 +80,8 @@ class ComposeViewModelTest {
}
val signatureRepository = mockk<SignatureRepository>()
coEvery { signatureRepository.getDefault(any()) } answers { signatures[firstArg<String>()] }
val settingsRepository = mockk<SettingsRepository>()
every { settingsRepository.settings } returns flowOf(AppSettings(defaultAccountId = defaultAccountId))
return ComposeViewModel(
savedStateHandle = savedState,
mailRepository = mailRepository,
@@ -83,6 +89,7 @@ class ComposeViewModelTest {
contactsRepository = mockk(relaxed = true),
accountSettingsRepository = accountSettingsRepository,
signatureRepository = signatureRepository,
settingsRepository = settingsRepository,
)
}
@@ -120,6 +127,40 @@ class ComposeViewModelTest {
assertEquals("\n\n-- \nBest, Bob", vm.state.value.body)
}
@Test
fun `uses the persisted default account when no explicit from-account is given`() = runTest(testDispatcher) {
val vm = viewModel(accounts = listOf(alice, bob), defaultAccountId = bob.id)
assertEquals(bob.id, vm.state.value.fromAccountId)
}
@Test
fun `falls back to the first account when the persisted default no longer exists`() = runTest(testDispatcher) {
// Simulates a default that outlived its account (deletion, or a Backup restore onto a device
// that never had it) — must fall back to the incidental first-alphabetically account, not crash.
val vm = viewModel(accounts = listOf(alice, bob), defaultAccountId = "imap:deleted-account")
assertEquals(alice.id, vm.state.value.fromAccountId)
}
@Test
fun `falls back to the first account when no default is set`() = runTest(testDispatcher) {
val vm = viewModel(accounts = listOf(alice, bob), defaultAccountId = null)
assertEquals(alice.id, vm.state.value.fromAccountId)
}
@Test
fun `an explicit from-account wins over the persisted default`() = runTest(testDispatcher) {
val vm = viewModel(
accounts = listOf(alice, bob),
savedState = SavedStateHandle(mapOf(Routes.COMPOSE_ARG_FROM to alice.id)),
defaultAccountId = bob.id,
)
assertEquals(alice.id, vm.state.value.fromAccountId)
}
@Test
fun `does not append a signature when the account disabled it`() = runTest(testDispatcher) {
val vm = viewModel(