Merge branch 'main' into fix-backfill-gaps
This commit is contained in:
@@ -42,16 +42,20 @@ class FolderDrawerTest {
|
||||
folders = listOf(
|
||||
folder("imap:a", "INBOX", "INBOX", FolderRole.INBOX),
|
||||
folder("imap:a", "[Gmail]/Sent Mail", "Sent Mail", FolderRole.SENT),
|
||||
folder("imap:a", "Archive", "Archive", FolderRole.ARCHIVE),
|
||||
// Give ARCHIVE a server name that differs from its friendly label so the assertion
|
||||
// below actually discriminates a role-to-label regression (displayName != friendly).
|
||||
folder("imap:a", "[Gmail]/All Mail", "All Mail", FolderRole.ARCHIVE),
|
||||
folder("imap:a", "Receipts", "Receipts", FolderRole.NORMAL),
|
||||
),
|
||||
)
|
||||
|
||||
composeTestRule.onNodeWithText(string(R.string.folder_inbox)).assertIsDisplayed()
|
||||
composeTestRule.onNodeWithText(string(R.string.folder_archive)).assertIsDisplayed()
|
||||
// Standard folders use the friendly role name ("Sent"), not the raw server name ("Sent Mail").
|
||||
// Standard folders use the friendly role name, not the raw server name — verified for both
|
||||
// Sent ("Sent Mail" -> "Sent") and Archive ("All Mail" -> "Archive").
|
||||
composeTestRule.onNodeWithText(string(R.string.folder_sent)).assertIsDisplayed()
|
||||
composeTestRule.onNodeWithText("Sent Mail").assertDoesNotExist()
|
||||
composeTestRule.onNodeWithText(string(R.string.folder_archive)).assertIsDisplayed()
|
||||
composeTestRule.onNodeWithText("All Mail").assertDoesNotExist()
|
||||
// Normal folders keep their server name.
|
||||
composeTestRule.onNodeWithText("Receipts").assertIsDisplayed()
|
||||
// A single account shows no account switcher / "All Inboxes" entry.
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
package org.libremail.data.local
|
||||
|
||||
import org.junit.Test
|
||||
import org.libremail.domain.model.FolderRole
|
||||
import org.libremail.mail.FetchedFolder
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFalse
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
/**
|
||||
* Pins the one production link between a server LIST response and a persisted folder:
|
||||
* [FetchedFolder.toEntity] derives the folder's role (via [FolderRole.roleOf]) and its server
|
||||
* special-use flag (via [FolderRole.isServerSpecial]) from the IMAP attributes, and toDomain carries
|
||||
* that flag back out. Guards the mutation the PR #54 review found: dropping the specialUse assignment
|
||||
* still compiles (the column defaults to false) and silently persists a non-special folder for every
|
||||
* server folder.
|
||||
*/
|
||||
class FolderMapperTest {
|
||||
|
||||
private fun entityFor(attributes: List<String>, displayName: String = "Neutral") = FetchedFolder(
|
||||
fullName = "Parent/$displayName",
|
||||
displayName = displayName,
|
||||
attributes = attributes,
|
||||
selectable = true,
|
||||
).toEntity(accountId = "acct", sortOrder = 3)
|
||||
|
||||
@Test
|
||||
fun `role-bearing special-use attributes map to their role and mark the folder special`() {
|
||||
val roleByAttribute = mapOf(
|
||||
"\\Sent" to FolderRole.SENT,
|
||||
"\\Drafts" to FolderRole.DRAFTS,
|
||||
"\\Junk" to FolderRole.SPAM,
|
||||
"\\Trash" to FolderRole.TRASH,
|
||||
"\\Archive" to FolderRole.ARCHIVE,
|
||||
)
|
||||
roleByAttribute.forEach { (attribute, expectedRole) ->
|
||||
val entity = entityFor(listOf(attribute))
|
||||
assertEquals(expectedRole.name, entity.role, "role for $attribute")
|
||||
assertTrue(entity.specialUse, "specialUse for $attribute")
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `the all-mail and starred attributes mark the folder special but drive no role of their own`() {
|
||||
// Gmail's "All Mail" (\All) and "Starred" (\Flagged) are server special-use, yet roleOf maps
|
||||
// neither to a role (the role still comes from the name — here neutral, so NORMAL). specialUse
|
||||
// and role are independent axes; this documents the current wiring ahead of the #65 refactor.
|
||||
listOf("\\All", "\\Flagged").forEach { attribute ->
|
||||
val entity = entityFor(listOf(attribute))
|
||||
assertTrue(entity.specialUse, "specialUse for $attribute")
|
||||
assertEquals(FolderRole.NORMAL.name, entity.role, "role for $attribute")
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `a folder with no special-use attributes is not marked special`() {
|
||||
val entity = entityFor(attributes = emptyList(), displayName = "Receipts")
|
||||
assertFalse(entity.specialUse)
|
||||
assertEquals(FolderRole.NORMAL.name, entity.role)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `a non special-use list flag alone does not mark the folder special`() {
|
||||
// \HasNoChildren is a structural LIST flag, not an RFC 6154 SPECIAL-USE attribute.
|
||||
assertFalse(entityFor(listOf("\\HasNoChildren"), displayName = "Receipts").specialUse)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `the server sort order is carried onto the persisted entity`() {
|
||||
assertEquals(3, entityFor(listOf("\\Sent")).sortOrder)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `toDomain carries the special-use flag back out for both values`() {
|
||||
assertTrue(entityFor(listOf("\\Drafts")).toDomain().specialUse)
|
||||
assertFalse(entityFor(emptyList(), displayName = "Receipts").toDomain().specialUse)
|
||||
}
|
||||
}
|
||||
@@ -35,6 +35,7 @@ import org.libremail.domain.model.MailSecurity
|
||||
import org.libremail.domain.model.ReplyMode
|
||||
import org.libremail.mail.AttachmentPart
|
||||
import org.libremail.mail.DownloadedAttachment
|
||||
import org.libremail.mail.FetchedFolder
|
||||
import org.libremail.mail.ImapClient
|
||||
import org.libremail.mail.MessageContent
|
||||
import org.libremail.mail.ReplyContext
|
||||
@@ -94,19 +95,52 @@ class MailRepositoryImplTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `observeFolders maps cached folders with their roles`() = runTest {
|
||||
fun `observeFolders maps cached folders with their roles and server special-use flag`() = runTest {
|
||||
every { folderDao.observeForAccount("acct") } returns flowOf(
|
||||
listOf(FolderEntity("acct", "[Gmail]/Sent Mail", "Sent Mail", "SENT", selectable = true, sortOrder = 1)),
|
||||
listOf(
|
||||
FolderEntity(
|
||||
accountId = "acct",
|
||||
fullName = "[Gmail]/Sent Mail",
|
||||
displayName = "Sent Mail",
|
||||
role = "SENT",
|
||||
selectable = true,
|
||||
sortOrder = 1,
|
||||
specialUse = true,
|
||||
),
|
||||
),
|
||||
)
|
||||
repository.observeFolders("acct").test {
|
||||
val folders = awaitItem()
|
||||
assertEquals(1, folders.size)
|
||||
assertEquals(FolderRole.SENT, folders.first().role)
|
||||
assertEquals("Sent Mail", folders.first().displayName)
|
||||
// toDomain must carry the persisted special-use flag through to the domain model.
|
||||
assertTrue(folders.first().specialUse)
|
||||
awaitComplete()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `refreshFolders persists the server special-use flag derived from folder attributes`() = runTest {
|
||||
coEvery { accountDao.getById("acct") } returns accountEntity()
|
||||
coEvery { connectionFactory.imapParamsFor(any()) } returns imapParams()
|
||||
// A provider-built \Drafts folder (RFC 6154 SPECIAL-USE) beside an attribute-less user folder.
|
||||
coEvery { imapClient.listFolders(any()) } returns listOf(
|
||||
FetchedFolder("[Gmail]/Drafts", "Drafts", listOf("\\Drafts"), selectable = true),
|
||||
FetchedFolder("Receipts", "Receipts", emptyList(), selectable = true),
|
||||
)
|
||||
val persisted = slot<List<FolderEntity>>()
|
||||
coEvery { folderDao.replaceForAccount(any(), capture(persisted)) } just Runs
|
||||
|
||||
val result = repository.refreshFolders("acct")
|
||||
|
||||
assertTrue(result.isSuccess)
|
||||
// The one production link (FetchedFolder.toEntity) must persist server special-use per folder,
|
||||
// alongside the role it derives from the same attributes.
|
||||
assertEquals(listOf(true, false), persisted.captured.map { it.specialUse })
|
||||
assertEquals(listOf("DRAFTS", "NORMAL"), persisted.captured.map { it.role })
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `openMessage fetches the body from the message's own folder, not the inbox`() = runTest {
|
||||
val id = "acct:Archive:5"
|
||||
|
||||
@@ -34,10 +34,20 @@ class FolderLabelsTest {
|
||||
smtp = ServerConfig("smtp", 587, MailSecurity.STARTTLS),
|
||||
)
|
||||
|
||||
/** Builds base labels the way the drawer does: the friendly role name, else the raw display name. */
|
||||
/**
|
||||
* Builds base labels from an independent, hand-written [friendly] map, NOT by calling the
|
||||
* production label source [org.libremail.ui.mailbox.folderDisplayLabel] — that is `@Composable`
|
||||
* (it reads `stringResource`) and can't run in a JVM unit test. So this is a stand-in literal
|
||||
* fixture that only mirrors the `folder_*` string values by hand; it deliberately pins the
|
||||
* de-duplication logic in [resolveDrawerLabels], not the role-to-wording mapping. A change to the
|
||||
* `folder_*` strings (or a new [folderDisplayLabel] branch) is out of scope here and won't be
|
||||
* caught by this suite — FolderDrawerTest (androidTest, real resources) guards that wiring.
|
||||
*/
|
||||
private fun baseLabelsOf(folders: List<Folder>, friendly: Map<FolderRole, String>) =
|
||||
folders.associate { it.fullName to (friendly[it.role] ?: it.displayName) }
|
||||
|
||||
// Hand-copied stand-ins for the current folder_* string values, not read from resources. See the
|
||||
// note on baseLabelsOf: these pin the de-dup behavior, not the actual role-to-string wording.
|
||||
private val friendlyNames = mapOf(
|
||||
FolderRole.INBOX to "Inbox",
|
||||
FolderRole.SENT to "Sent",
|
||||
|
||||
Reference in New Issue
Block a user