summaryrefslogtreecommitdiff
path: root/backend/jmap/src/test/java
diff options
context:
space:
mode:
authortest@dangofactory.cos <you@example.com>2026-08-20 16:57:18 +0000
committertest@dangofactory.cos <you@example.com>2026-08-20 16:57:18 +0000
commit50ce5d5e6cd0d720ff437d35eace545816848428 (patch)
treeb59af206a36265cf081945e434900b32b0c43edd /backend/jmap/src/test/java
downloadmonocles_mail-50ce5d5e6cd0d720ff437d35eace545816848428.tar.gz
monocles_mail-50ce5d5e6cd0d720ff437d35eace545816848428.tar.bz2
monocles_mail-50ce5d5e6cd0d720ff437d35eace545816848428.zip
Inital
Diffstat (limited to 'backend/jmap/src/test/java')
-rw-r--r--backend/jmap/src/test/java/com/fsck/k9/backend/jmap/CommandRefreshFolderListTest.kt179
-rw-r--r--backend/jmap/src/test/java/com/fsck/k9/backend/jmap/CommandSyncTest.kt266
-rw-r--r--backend/jmap/src/test/java/com/fsck/k9/backend/jmap/LoggingSyncListener.kt96
-rw-r--r--backend/jmap/src/test/java/com/fsck/k9/backend/jmap/MockWebServerHelper.kt35
4 files changed, 576 insertions, 0 deletions
diff --git a/backend/jmap/src/test/java/com/fsck/k9/backend/jmap/CommandRefreshFolderListTest.kt b/backend/jmap/src/test/java/com/fsck/k9/backend/jmap/CommandRefreshFolderListTest.kt
new file mode 100644
index 0000000..f79740b
--- /dev/null
+++ b/backend/jmap/src/test/java/com/fsck/k9/backend/jmap/CommandRefreshFolderListTest.kt
@@ -0,0 +1,179 @@
+package com.fsck.k9.backend.jmap
+
+import app.k9mail.backend.testing.InMemoryBackendStorage
+import com.fsck.k9.backend.api.BackendFolderUpdater
+import com.fsck.k9.backend.api.FolderInfo
+import com.fsck.k9.backend.api.updateFolders
+import com.fsck.k9.mail.AuthenticationFailedException
+import com.fsck.k9.mail.FolderType
+import com.fsck.k9.mail.MessagingException
+import junit.framework.AssertionFailedError
+import okhttp3.HttpUrl
+import okhttp3.mockwebserver.MockResponse
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertTrue
+import org.junit.Assert.fail
+import org.junit.Test
+import rs.ltt.jmap.client.JmapClient
+
+class CommandRefreshFolderListTest {
+ private val backendStorage = InMemoryBackendStorage()
+
+ @Test
+ fun sessionResourceWithAuthenticationError() {
+ val command = createCommandRefreshFolderList(
+ MockResponse().setResponseCode(401)
+ )
+
+ try {
+ command.refreshFolderList()
+ fail("Expected exception")
+ } catch (e: AuthenticationFailedException) {
+ }
+ }
+
+ @Test
+ fun invalidSessionResource() {
+ val command = createCommandRefreshFolderList(
+ MockResponse().setBody("invalid")
+ )
+
+ try {
+ command.refreshFolderList()
+ fail("Expected exception")
+ } catch (e: MessagingException) {
+ assertTrue(e.isPermanentFailure)
+ }
+ }
+
+ @Test
+ fun fetchMailboxes() {
+ val command = createCommandRefreshFolderList(
+ responseBodyFromResource("/jmap_responses/session/valid_session.json"),
+ responseBodyFromResource("/jmap_responses/mailbox/mailbox_get.json")
+ )
+
+ command.refreshFolderList()
+
+ assertFolderList("id_inbox", "id_archive", "id_drafts", "id_sent", "id_trash", "id_folder1")
+ assertFolderPresent("id_inbox", "Inbox", FolderType.INBOX)
+ assertFolderPresent("id_archive", "Archive", FolderType.ARCHIVE)
+ assertFolderPresent("id_drafts", "Drafts", FolderType.DRAFTS)
+ assertFolderPresent("id_sent", "Sent", FolderType.SENT)
+ assertFolderPresent("id_trash", "Trash", FolderType.TRASH)
+ assertFolderPresent("id_folder1", "folder1", FolderType.REGULAR)
+ assertMailboxState("23")
+ }
+
+ @Test
+ fun fetchMailboxUpdates() {
+ val command = createCommandRefreshFolderList(
+ responseBodyFromResource("/jmap_responses/session/valid_session.json"),
+ responseBodyFromResource("/jmap_responses/mailbox/mailbox_changes.json")
+ )
+ createFoldersInBackendStorage(state = "23")
+
+ command.refreshFolderList()
+
+ assertFolderList("id_inbox", "id_archive", "id_drafts", "id_sent", "id_trash", "id_folder2")
+ assertFolderPresent("id_inbox", "Inbox", FolderType.INBOX)
+ assertFolderPresent("id_archive", "Archive", FolderType.ARCHIVE)
+ assertFolderPresent("id_drafts", "Drafts", FolderType.DRAFTS)
+ assertFolderPresent("id_sent", "Sent", FolderType.SENT)
+ assertFolderPresent("id_trash", "Deleted messages", FolderType.TRASH)
+ assertFolderPresent("id_folder2", "folder2", FolderType.REGULAR)
+ assertMailboxState("42")
+ }
+
+ @Test
+ fun fetchMailboxUpdates_withHasMoreChanges() {
+ val command = createCommandRefreshFolderList(
+ responseBodyFromResource("/jmap_responses/session/valid_session.json"),
+ responseBodyFromResource("/jmap_responses/mailbox/mailbox_changes_1.json"),
+ responseBodyFromResource("/jmap_responses/mailbox/mailbox_changes_2.json")
+ )
+ createFoldersInBackendStorage(state = "23")
+
+ command.refreshFolderList()
+
+ assertFolderList("id_inbox", "id_archive", "id_drafts", "id_sent", "id_trash", "id_folder2")
+ assertFolderPresent("id_inbox", "Inbox", FolderType.INBOX)
+ assertFolderPresent("id_archive", "Archive", FolderType.ARCHIVE)
+ assertFolderPresent("id_drafts", "Drafts", FolderType.DRAFTS)
+ assertFolderPresent("id_sent", "Sent", FolderType.SENT)
+ assertFolderPresent("id_trash", "Deleted messages", FolderType.TRASH)
+ assertFolderPresent("id_folder2", "folder2", FolderType.REGULAR)
+ assertMailboxState("42")
+ }
+
+ @Test
+ fun fetchMailboxUpdates_withCannotCalculateChangesError() {
+ val command = createCommandRefreshFolderList(
+ responseBodyFromResource("/jmap_responses/session/valid_session.json"),
+ responseBodyFromResource("/jmap_responses/mailbox/mailbox_changes_error_cannot_calculate_changes.json"),
+ responseBodyFromResource("/jmap_responses/mailbox/mailbox_get.json")
+ )
+ setMailboxState("unknownToServer")
+
+ command.refreshFolderList()
+
+ assertFolderList("id_inbox", "id_archive", "id_drafts", "id_sent", "id_trash", "id_folder1")
+ assertFolderPresent("id_inbox", "Inbox", FolderType.INBOX)
+ assertFolderPresent("id_archive", "Archive", FolderType.ARCHIVE)
+ assertFolderPresent("id_drafts", "Drafts", FolderType.DRAFTS)
+ assertFolderPresent("id_sent", "Sent", FolderType.SENT)
+ assertFolderPresent("id_trash", "Trash", FolderType.TRASH)
+ assertFolderPresent("id_folder1", "folder1", FolderType.REGULAR)
+ assertMailboxState("23")
+ }
+
+ private fun createCommandRefreshFolderList(vararg mockResponses: MockResponse): CommandRefreshFolderList {
+ val server = createMockWebServer(*mockResponses)
+ return createCommandRefreshFolderList(server.url("/jmap/"))
+ }
+
+ private fun createCommandRefreshFolderList(
+ baseUrl: HttpUrl,
+ accountId: String = "test@example.com"
+ ): CommandRefreshFolderList {
+ val jmapClient = JmapClient("test", "test", baseUrl)
+ return CommandRefreshFolderList(backendStorage, jmapClient, accountId)
+ }
+
+ @Suppress("SameParameterValue")
+ private fun createFoldersInBackendStorage(state: String) {
+ backendStorage.updateFolders {
+ createFolder("id_inbox", "Inbox", FolderType.INBOX)
+ createFolder("id_archive", "Archive", FolderType.ARCHIVE)
+ createFolder("id_drafts", "Drafts", FolderType.DRAFTS)
+ createFolder("id_sent", "Sent", FolderType.SENT)
+ createFolder("id_trash", "Trash", FolderType.TRASH)
+ createFolder("id_folder1", "folder1", FolderType.REGULAR)
+ }
+ setMailboxState(state)
+ }
+
+ private fun BackendFolderUpdater.createFolder(serverId: String, name: String, type: FolderType) {
+ createFolders(listOf(FolderInfo(serverId, name, type)))
+ }
+
+ private fun setMailboxState(state: String) {
+ backendStorage.setExtraString("jmapState", state)
+ }
+
+ private fun assertFolderList(vararg folderServerIds: String) {
+ assertEquals(folderServerIds.toSet(), backendStorage.getFolderServerIds().toSet())
+ }
+
+ private fun assertFolderPresent(serverId: String, name: String, type: FolderType) {
+ val folder = backendStorage.folders[serverId]
+ ?: throw AssertionFailedError("Expected folder '$serverId' in BackendStorage")
+
+ assertEquals(name, folder.name)
+ assertEquals(type, folder.type)
+ }
+
+ private fun assertMailboxState(expected: String) {
+ assertEquals(expected, backendStorage.getExtraString("jmapState"))
+ }
+}
diff --git a/backend/jmap/src/test/java/com/fsck/k9/backend/jmap/CommandSyncTest.kt b/backend/jmap/src/test/java/com/fsck/k9/backend/jmap/CommandSyncTest.kt
new file mode 100644
index 0000000..b4bbdbb
--- /dev/null
+++ b/backend/jmap/src/test/java/com/fsck/k9/backend/jmap/CommandSyncTest.kt
@@ -0,0 +1,266 @@
+package com.fsck.k9.backend.jmap
+
+import app.k9mail.backend.testing.InMemoryBackendFolder
+import app.k9mail.backend.testing.InMemoryBackendStorage
+import com.fsck.k9.backend.api.FolderInfo
+import com.fsck.k9.backend.api.SyncConfig
+import com.fsck.k9.backend.api.SyncConfig.ExpungePolicy
+import com.fsck.k9.backend.api.updateFolders
+import com.fsck.k9.mail.AuthenticationFailedException
+import com.fsck.k9.mail.Flag
+import com.fsck.k9.mail.FolderType
+import com.fsck.k9.mail.internet.BinaryTempFileBody
+import java.io.File
+import java.util.EnumSet
+import okhttp3.HttpUrl
+import okhttp3.OkHttpClient
+import okhttp3.mockwebserver.MockResponse
+import okhttp3.mockwebserver.MockWebServer
+import org.junit.Assert.assertEquals
+import org.junit.Before
+import org.junit.Test
+import rs.ltt.jmap.client.JmapClient
+import rs.ltt.jmap.client.http.BasicAuthHttpAuthentication
+
+class CommandSyncTest {
+ private val backendStorage = InMemoryBackendStorage()
+ private val okHttpClient = OkHttpClient.Builder().build()
+ private val syncListener = LoggingSyncListener()
+ private val syncConfig = SyncConfig(
+ expungePolicy = ExpungePolicy.IMMEDIATELY,
+ earliestPollDate = null,
+ syncRemoteDeletions = true,
+ maximumAutoDownloadMessageSize = 1000,
+ defaultVisibleLimit = 25,
+ syncFlags = EnumSet.of(Flag.SEEN, Flag.FLAGGED, Flag.ANSWERED, Flag.FORWARDED)
+ )
+
+ @Before
+ fun setUp() {
+ BinaryTempFileBody.setTempDirectory(File(System.getProperty("java.io.tmpdir")))
+ createFolderInBackendStorage()
+ }
+
+ @Test
+ fun sessionResourceWithAuthenticationError() {
+ val command = createCommandSync(
+ MockResponse().setResponseCode(401)
+ )
+
+ command.sync(FOLDER_SERVER_ID, syncConfig, syncListener)
+
+ assertEquals(SyncListenerEvent.SyncStarted(FOLDER_SERVER_ID), syncListener.getNextEvent())
+ val failedEvent = syncListener.getNextEvent() as SyncListenerEvent.SyncFailed
+ assertEquals(AuthenticationFailedException::class.java, failedEvent.exception!!.javaClass)
+ }
+
+ @Test
+ fun fullSyncStartingWithEmptyLocalMailbox() {
+ val server = createMockWebServer(
+ responseBodyFromResource("/jmap_responses/session/valid_session.json"),
+ responseBodyFromResource("/jmap_responses/email/email_query_M001_and_M002.json"),
+ responseBodyFromResource("/jmap_responses/email/email_get_ids_M001_and_M002.json"),
+ responseBodyFromResource("/jmap_responses/blob/email/email_1.eml"),
+ responseBodyFromResource("/jmap_responses/blob/email/email_2.eml")
+ )
+ val baseUrl = server.url("/jmap/")
+ val command = createCommandSync(baseUrl)
+
+ command.sync(FOLDER_SERVER_ID, syncConfig, syncListener)
+
+ val backendFolder = backendStorage.getFolder(FOLDER_SERVER_ID)
+ backendFolder.assertMessages(
+ "M001" to "/jmap_responses/blob/email/email_1.eml",
+ "M002" to "/jmap_responses/blob/email/email_2.eml"
+ )
+ backendFolder.assertQueryState("50:0")
+ syncListener.assertSyncEvents(
+ SyncListenerEvent.SyncStarted(FOLDER_SERVER_ID),
+ SyncListenerEvent.SyncProgress(FOLDER_SERVER_ID, completed = 1, total = 2),
+ SyncListenerEvent.SyncProgress(FOLDER_SERVER_ID, completed = 2, total = 2),
+ SyncListenerEvent.SyncFinished(FOLDER_SERVER_ID)
+ )
+ server.skipRequests(3)
+ server.assertRequestUrlPath("/jmap/download/test%40example.com/B001/B001?accept=application%2Foctet-stream")
+ server.assertRequestUrlPath("/jmap/download/test%40example.com/B002/B002?accept=application%2Foctet-stream")
+ }
+
+ @Test
+ fun fullSyncExceedingMaxObjectsInGet() {
+ val command = createCommandSync(
+ responseBodyFromResource("/jmap_responses/session/session_with_maxObjectsInGet_2.json"),
+ responseBodyFromResource("/jmap_responses/email/email_query_M001_to_M005.json"),
+ responseBodyFromResource("/jmap_responses/email/email_get_ids_M001_and_M002.json"),
+ responseBodyFromResource("/jmap_responses/email/email_get_ids_M003_and_M004.json"),
+ responseBodyFromResource("/jmap_responses/email/email_get_ids_M005.json"),
+ responseBodyFromResource("/jmap_responses/blob/email/email_1.eml"),
+ responseBodyFromResource("/jmap_responses/blob/email/email_2.eml"),
+ responseBodyFromResource("/jmap_responses/blob/email/email_3.eml"),
+ responseBodyFromResource("/jmap_responses/blob/email/email_3.eml"),
+ responseBodyFromResource("/jmap_responses/blob/email/email_3.eml")
+ )
+
+ command.sync(FOLDER_SERVER_ID, syncConfig, syncListener)
+
+ val backendFolder = backendStorage.getFolder(FOLDER_SERVER_ID)
+ assertEquals(setOf("M001", "M002", "M003", "M004", "M005"), backendFolder.getMessageServerIds())
+ syncListener.assertSyncSuccess()
+ }
+
+ @Test
+ fun fullSyncWithLocalMessagesAndDifferentMessagesInRemoteMailbox() {
+ val backendFolder = backendStorage.getFolder(FOLDER_SERVER_ID)
+ backendFolder.createMessages(
+ "M001" to "/jmap_responses/blob/email/email_1.eml",
+ "M002" to "/jmap_responses/blob/email/email_2.eml"
+ )
+ val command = createCommandSync(
+ responseBodyFromResource("/jmap_responses/session/valid_session.json"),
+ responseBodyFromResource("/jmap_responses/email/email_query_M002_and_M003.json"),
+ responseBodyFromResource("/jmap_responses/email/email_get_ids_M003.json"),
+ responseBodyFromResource("/jmap_responses/blob/email/email_3.eml"),
+ responseBodyFromResource("/jmap_responses/email/email_get_keywords_M002.json")
+ )
+
+ command.sync(FOLDER_SERVER_ID, syncConfig, syncListener)
+
+ backendFolder.assertMessages(
+ "M002" to "/jmap_responses/blob/email/email_2.eml",
+ "M003" to "/jmap_responses/blob/email/email_3.eml"
+ )
+ syncListener.assertSyncSuccess()
+ }
+
+ @Test
+ fun fullSyncWithLocalMessagesAndEmptyRemoteMailbox() {
+ val backendFolder = backendStorage.getFolder(FOLDER_SERVER_ID)
+ backendFolder.createMessages(
+ "M001" to "/jmap_responses/blob/email/email_1.eml",
+ "M002" to "/jmap_responses/blob/email/email_2.eml"
+ )
+ val command = createCommandSync(
+ responseBodyFromResource("/jmap_responses/session/valid_session.json"),
+ responseBodyFromResource("/jmap_responses/email/email_query_empty_result.json")
+ )
+
+ command.sync(FOLDER_SERVER_ID, syncConfig, syncListener)
+
+ assertEquals(emptySet<String>(), backendFolder.getMessageServerIds())
+ syncListener.assertSyncEvents(
+ SyncListenerEvent.SyncStarted(FOLDER_SERVER_ID),
+ SyncListenerEvent.SyncFinished(FOLDER_SERVER_ID)
+ )
+ }
+
+ @Test
+ fun deltaSyncWithoutChanges() {
+ val backendFolder = backendStorage.getFolder(FOLDER_SERVER_ID)
+ backendFolder.createMessages(
+ "M001" to "/jmap_responses/blob/email/email_1.eml",
+ "M002" to "/jmap_responses/blob/email/email_2.eml"
+ )
+ backendFolder.setQueryState("50:0")
+ val command = createCommandSync(
+ responseBodyFromResource("/jmap_responses/session/valid_session.json"),
+ responseBodyFromResource("/jmap_responses/email/email_query_changes_empty_result.json"),
+ responseBodyFromResource("/jmap_responses/email/email_get_keywords_M001_and_M002.json")
+ )
+
+ command.sync(FOLDER_SERVER_ID, syncConfig, syncListener)
+
+ assertEquals(setOf("M001", "M002"), backendFolder.getMessageServerIds())
+ assertEquals(emptySet<Flag>(), backendFolder.getMessageFlags("M001"))
+ assertEquals(setOf(Flag.SEEN), backendFolder.getMessageFlags("M002"))
+ backendFolder.assertQueryState("50:0")
+ syncListener.assertSyncEvents(
+ SyncListenerEvent.SyncStarted(FOLDER_SERVER_ID),
+ SyncListenerEvent.SyncFinished(FOLDER_SERVER_ID)
+ )
+ }
+
+ @Test
+ fun deltaSyncWithLocalMessagesAndDifferentMessagesInRemoteMailbox() {
+ val backendFolder = backendStorage.getFolder(FOLDER_SERVER_ID)
+ backendFolder.createMessages(
+ "M001" to "/jmap_responses/blob/email/email_1.eml",
+ "M002" to "/jmap_responses/blob/email/email_2.eml"
+ )
+ backendFolder.setQueryState("50:0")
+ val command = createCommandSync(
+ responseBodyFromResource("/jmap_responses/session/valid_session.json"),
+ responseBodyFromResource("/jmap_responses/email/email_query_changes_M001_deleted_M003_added.json"),
+ responseBodyFromResource("/jmap_responses/email/email_get_ids_M003.json"),
+ responseBodyFromResource("/jmap_responses/blob/email/email_3.eml"),
+ responseBodyFromResource("/jmap_responses/email/email_get_keywords_M002.json")
+ )
+
+ command.sync(FOLDER_SERVER_ID, syncConfig, syncListener)
+
+ assertEquals(setOf("M002", "M003"), backendFolder.getMessageServerIds())
+ backendFolder.assertQueryState("51:0")
+ syncListener.assertSyncSuccess()
+ }
+
+ @Test
+ fun deltaSyncCannotCalculateChanges() {
+ val backendFolder = backendStorage.getFolder(FOLDER_SERVER_ID)
+ backendFolder.createMessages(
+ "M001" to "/jmap_responses/blob/email/email_1.eml",
+ "M002" to "/jmap_responses/blob/email/email_2.eml"
+ )
+ backendFolder.setQueryState("10:0")
+ val command = createCommandSync(
+ responseBodyFromResource("/jmap_responses/session/valid_session.json"),
+ responseBodyFromResource("/jmap_responses/email/email_query_changes_cannot_calculate_changes_error.json"),
+ responseBodyFromResource("/jmap_responses/email/email_query_M002_and_M003.json"),
+ responseBodyFromResource("/jmap_responses/email/email_get_ids_M003.json"),
+ responseBodyFromResource("/jmap_responses/blob/email/email_3.eml"),
+ responseBodyFromResource("/jmap_responses/email/email_get_keywords_M002.json")
+ )
+
+ command.sync(FOLDER_SERVER_ID, syncConfig, syncListener)
+
+ assertEquals(setOf("M002", "M003"), backendFolder.getMessageServerIds())
+ backendFolder.assertQueryState("50:0")
+ syncListener.assertSyncSuccess()
+ }
+
+ private fun createCommandSync(vararg mockResponses: MockResponse): CommandSync {
+ val server = createMockWebServer(*mockResponses)
+ return createCommandSync(server.url("/jmap/"))
+ }
+
+ private fun createCommandSync(baseUrl: HttpUrl): CommandSync {
+ val httpAuthentication = BasicAuthHttpAuthentication(USERNAME, PASSWORD)
+ val jmapClient = JmapClient(httpAuthentication, baseUrl)
+ return CommandSync(backendStorage, jmapClient, okHttpClient, ACCOUNT_ID, httpAuthentication)
+ }
+
+ private fun createFolderInBackendStorage() {
+ backendStorage.updateFolders {
+ createFolders(listOf(FolderInfo(FOLDER_SERVER_ID, "Regular folder", FolderType.REGULAR)))
+ }
+ }
+
+ private fun MockWebServer.assertRequestUrlPath(expected: String) {
+ val request = takeRequest()
+ val requestUrl = request.requestUrl ?: error("No request URL")
+ val requestUrlPath = requestUrl.encodedPath + "?" + requestUrl.encodedQuery
+ assertEquals(expected, requestUrlPath)
+ }
+
+ private fun InMemoryBackendFolder.assertQueryState(expected: String) {
+ assertEquals(expected, getFolderExtraString("jmapQueryState"))
+ }
+
+ private fun InMemoryBackendFolder.setQueryState(queryState: String) {
+ setFolderExtraString("jmapQueryState", queryState)
+ }
+
+ companion object {
+ private const val FOLDER_SERVER_ID = "id_folder"
+ private const val USERNAME = "username"
+ private const val PASSWORD = "password"
+ private const val ACCOUNT_ID = "test@example.com"
+ }
+}
diff --git a/backend/jmap/src/test/java/com/fsck/k9/backend/jmap/LoggingSyncListener.kt b/backend/jmap/src/test/java/com/fsck/k9/backend/jmap/LoggingSyncListener.kt
new file mode 100644
index 0000000..2bc08cf
--- /dev/null
+++ b/backend/jmap/src/test/java/com/fsck/k9/backend/jmap/LoggingSyncListener.kt
@@ -0,0 +1,96 @@
+package com.fsck.k9.backend.jmap
+
+import com.fsck.k9.backend.api.SyncListener
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertTrue
+import org.junit.Assert.fail
+
+class LoggingSyncListener : SyncListener {
+ private val events = mutableListOf<SyncListenerEvent>()
+
+ fun assertSyncSuccess() {
+ events.filterIsInstance<SyncListenerEvent.SyncFailed>().firstOrNull()?.let { syncFailed ->
+ throw AssertionError("Expected sync success", syncFailed.exception)
+ }
+
+ if (events.none { it is SyncListenerEvent.SyncFinished }) {
+ fail("Expected SyncFinished, but only got: $events")
+ }
+ }
+
+ fun assertSyncEvents(vararg events: SyncListenerEvent) {
+ for (event in events) {
+ assertEquals(event, getNextEvent())
+ }
+ assertNoMoreEventsLeft()
+ }
+
+ fun getNextEvent(): SyncListenerEvent {
+ require(events.isNotEmpty()) { "No events left" }
+ return events.removeAt(0)
+ }
+
+ private fun assertNoMoreEventsLeft() {
+ assertTrue("Expected no more events; but still have: $events", events.isEmpty())
+ }
+
+ override fun syncStarted(folderServerId: String) {
+ events.add(SyncListenerEvent.SyncStarted(folderServerId))
+ }
+
+ override fun syncAuthenticationSuccess() {
+ throw UnsupportedOperationException("not implemented")
+ }
+
+ override fun syncHeadersStarted(folderServerId: String) {
+ throw UnsupportedOperationException("not implemented")
+ }
+
+ override fun syncHeadersProgress(folderServerId: String, completed: Int, total: Int) {
+ throw UnsupportedOperationException("not implemented")
+ }
+
+ override fun syncHeadersFinished(folderServerId: String, totalMessagesInMailbox: Int, numNewMessages: Int) {
+ throw UnsupportedOperationException("not implemented")
+ }
+
+ override fun syncProgress(folderServerId: String, completed: Int, total: Int) {
+ events.add(SyncListenerEvent.SyncProgress(folderServerId, completed, total))
+ }
+
+ override fun syncNewMessage(folderServerId: String, messageServerId: String, isOldMessage: Boolean) {
+ throw UnsupportedOperationException("not implemented")
+ }
+
+ override fun syncRemovedMessage(folderServerId: String, messageServerId: String) {
+ throw UnsupportedOperationException("not implemented")
+ }
+
+ override fun syncFlagChanged(folderServerId: String, messageServerId: String) {
+ throw UnsupportedOperationException("not implemented")
+ }
+
+ override fun syncFinished(folderServerId: String) {
+ events.add(SyncListenerEvent.SyncFinished(folderServerId))
+ }
+
+ override fun syncFailed(folderServerId: String, message: String, exception: Exception?) {
+ events.add(SyncListenerEvent.SyncFailed(folderServerId, message, exception))
+ }
+
+ override fun folderStatusChanged(folderServerId: String) {
+ throw UnsupportedOperationException("not implemented")
+ }
+}
+
+sealed class SyncListenerEvent {
+ data class SyncStarted(val folderServerId: String) : SyncListenerEvent()
+ data class SyncFinished(val folderServerId: String) : SyncListenerEvent()
+ data class SyncFailed(
+ val folderServerId: String,
+ val message: String,
+ val exception: Exception?
+ ) : SyncListenerEvent()
+
+ data class SyncProgress(val folderServerId: String, val completed: Int, val total: Int) : SyncListenerEvent()
+}
diff --git a/backend/jmap/src/test/java/com/fsck/k9/backend/jmap/MockWebServerHelper.kt b/backend/jmap/src/test/java/com/fsck/k9/backend/jmap/MockWebServerHelper.kt
new file mode 100644
index 0000000..e4bee42
--- /dev/null
+++ b/backend/jmap/src/test/java/com/fsck/k9/backend/jmap/MockWebServerHelper.kt
@@ -0,0 +1,35 @@
+package com.fsck.k9.backend.jmap
+
+import java.io.InputStream
+import okhttp3.mockwebserver.MockResponse
+import okhttp3.mockwebserver.MockWebServer
+import okio.buffer
+import okio.source
+
+fun createMockWebServer(vararg mockResponses: MockResponse): MockWebServer {
+ return MockWebServer().apply {
+ for (mockResponse in mockResponses) {
+ enqueue(mockResponse)
+ }
+ start()
+ }
+}
+
+fun responseBodyFromResource(name: String): MockResponse {
+ return MockResponse().setBody(loadResource(name))
+}
+
+fun MockWebServer.skipRequests(count: Int) {
+ repeat(count) {
+ takeRequest()
+ }
+}
+
+fun loadResource(name: String): String {
+ val resourceAsStream = ResourceLoader.getResourceAsStream(name) ?: error("Couldn't load resource: $name")
+ return resourceAsStream.use { it.source().buffer().readUtf8() }
+}
+
+private object ResourceLoader {
+ fun getResourceAsStream(name: String): InputStream? = javaClass.getResourceAsStream(name)
+}