Rewrite ImapSyncTest to use less Mockito

This commit is contained in:
cketti 2022-01-05 01:01:27 +01:00
parent f993b32aae
commit 4ab0dff096
7 changed files with 522 additions and 397 deletions

View file

@ -14,9 +14,12 @@ dependencies {
implementation "com.jakewharton.timber:timber:${versions.timber}"
testImplementation project(":mail:testing")
testImplementation project(":backend:testing")
testImplementation "junit:junit:${versions.junit}"
testImplementation "org.mockito:mockito-core:${versions.mockito}"
testImplementation "org.mockito.kotlin:mockito-kotlin:${versions.mockitoKotlin}"
testImplementation "com.google.truth:truth:${versions.truth}"
testImplementation "org.apache.james:apache-mime4j-dom:${versions.mime4j}"
}
android {

View file

@ -1,387 +0,0 @@
package com.fsck.k9.backend.imap;
import java.util.Collections;
import java.util.Date;
import java.util.EnumSet;
import java.util.List;
import java.util.Set;
import com.fsck.k9.backend.api.BackendFolder;
import com.fsck.k9.backend.api.BackendStorage;
import com.fsck.k9.backend.api.SyncConfig;
import com.fsck.k9.backend.api.SyncConfig.ExpungePolicy;
import com.fsck.k9.backend.api.SyncListener;
import com.fsck.k9.mail.FetchProfile;
import com.fsck.k9.mail.Flag;
import com.fsck.k9.mail.MessageRetrievalListener;
import com.fsck.k9.mail.MessagingException;
import com.fsck.k9.mail.store.imap.ImapFolder;
import com.fsck.k9.mail.store.imap.ImapMessage;
import com.fsck.k9.mail.store.imap.ImapStore;
import com.fsck.k9.mail.store.imap.OpenMode;
import org.junit.Before;
import org.junit.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.nullable;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.Mockito.atLeast;
import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@SuppressWarnings("unchecked")
public class ImapSyncTest {
private static final String EXTRA_UID_VALIDITY = "imapUidValidity";
private static final String ACCOUNT_NAME = "Account";
private static final String FOLDER_NAME = "Folder";
private static final Long FOLDER_UID_VALIDITY = 42L;
private static final int MAXIMUM_SMALL_MESSAGE_SIZE = 1000;
private static final String MESSAGE_UID1 = "message-uid1";
private static final int DEFAULT_VISIBLE_LIMIT = 25;
private static final Set<Flag> SYNC_FLAGS = EnumSet.of(Flag.SEEN, Flag.FLAGGED, Flag.ANSWERED, Flag.FORWARDED);
private ImapSync imapSync;
@Mock
private SyncListener listener;
@Mock
private ImapFolder remoteFolder;
@Mock
private BackendStorage backendStorage;
@Mock
private BackendFolder backendFolder;
@Mock
private ImapStore remoteStore;
@Captor
private ArgumentCaptor<List<String>> messageListCaptor;
@Captor
private ArgumentCaptor<FetchProfile> fetchProfileCaptor;
private SyncConfig syncConfig;
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
imapSync = new ImapSync(ACCOUNT_NAME, backendStorage, remoteStore);
configureSyncConfig();
configureBackendStorage();
configureRemoteStoreWithFolder();
}
@Test
public void sync_withOneMessageInRemoteFolder_shouldFinishWithoutError() {
messageCountInRemoteFolder(1);
imapSync.sync(FOLDER_NAME, syncConfig, listener);
verify(listener).syncFinished(FOLDER_NAME);
}
@Test
public void sync_withEmptyRemoteFolder_shouldFinishWithoutError() {
messageCountInRemoteFolder(0);
imapSync.sync(FOLDER_NAME, syncConfig, listener);
verify(listener).syncFinished(FOLDER_NAME);
}
@Test
public void sync_withNegativeMessageCountInRemoteFolder_shouldFinishWithError() {
messageCountInRemoteFolder(-1);
imapSync.sync(FOLDER_NAME, syncConfig, listener);
verify(listener).syncFailed(eq(FOLDER_NAME), eq("Exception: Message count -1 for folder Folder"),
any(Exception.class));
}
@Test
public void sync_shouldOpenRemoteFolder() throws Exception {
messageCountInRemoteFolder(1);
imapSync.sync(FOLDER_NAME, syncConfig, listener);
verify(remoteFolder).open(OpenMode.READ_ONLY);
}
@Test
public void sync_shouldCloseRemoteFolder() {
messageCountInRemoteFolder(1);
imapSync.sync(FOLDER_NAME, syncConfig, listener);
verify(remoteFolder).close();
}
@Test
public void sync_withAccountPolicySetToExpungeOnPoll_shouldExpungeRemoteFolder() throws Exception {
messageCountInRemoteFolder(1);
configureSyncConfigWithExpungePolicy(ExpungePolicy.ON_POLL);
imapSync.sync(FOLDER_NAME, syncConfig, listener);
verify(remoteFolder).expunge();
}
@Test
public void sync_withAccountPolicySetToExpungeManually_shouldNotExpungeRemoteFolder() throws Exception {
messageCountInRemoteFolder(1);
configureSyncConfigWithExpungePolicy(ExpungePolicy.MANUALLY);
imapSync.sync(FOLDER_NAME, syncConfig, listener);
verify(remoteFolder, never()).expunge();
}
@Test
public void sync_withAccountSetToSyncRemoteDeletions_shouldDeleteLocalCopiesOfDeletedMessages() {
messageCountInRemoteFolder(0);
configureSyncConfigWithSyncRemoteDeletions(true);
when(backendFolder.getAllMessagesAndEffectiveDates()).thenReturn(Collections.singletonMap(MESSAGE_UID1, 0L));
imapSync.sync(FOLDER_NAME, syncConfig, listener);
verify(backendFolder).destroyMessages(messageListCaptor.capture());
assertEquals(MESSAGE_UID1, messageListCaptor.getValue().get(0));
}
@Test
public void sync_withAccountSetToSyncRemoteDeletions_shouldNotDeleteLocalCopiesOfExistingMessagesAfterEarliestPollDate()
throws Exception {
messageCountInRemoteFolder(1);
Date dateOfEarliestPoll = new Date();
ImapMessage remoteMessage = messageOnServer();
configureSyncConfigWithSyncRemoteDeletionsAndEarliestPollDate(dateOfEarliestPoll);
when(remoteMessage.olderThan(dateOfEarliestPoll)).thenReturn(false);
imapSync.sync(FOLDER_NAME, syncConfig, listener);
verify(backendFolder, never()).destroyMessages(messageListCaptor.capture());
}
@Test
public void sync_withAccountSetToSyncRemoteDeletions_shouldDeleteLocalCopiesOfExistingMessagesBeforeEarliestPollDate()
throws Exception {
messageCountInRemoteFolder(1);
ImapMessage remoteMessage = messageOnServer();
Date dateOfEarliestPoll = new Date();
configureSyncConfigWithSyncRemoteDeletionsAndEarliestPollDate(dateOfEarliestPoll);
when(remoteMessage.olderThan(dateOfEarliestPoll)).thenReturn(true);
when(backendFolder.getAllMessagesAndEffectiveDates()).thenReturn(Collections.singletonMap(MESSAGE_UID1, 0L));
imapSync.sync(FOLDER_NAME, syncConfig, listener);
verify(backendFolder).destroyMessages(messageListCaptor.capture());
assertEquals(MESSAGE_UID1, messageListCaptor.getValue().get(0));
}
@Test
public void sync_withAccountSetNotToSyncRemoteDeletions_shouldNotDeleteLocalCopiesOfMessages() {
messageCountInRemoteFolder(0);
configureSyncConfigWithSyncRemoteDeletions(false);
imapSync.sync(FOLDER_NAME, syncConfig, listener);
verify(backendFolder, never()).destroyMessages(messageListCaptor.capture());
}
@Test
public void sync_shouldFetchUnsynchronizedMessagesListAndFlags() throws Exception {
messageCountInRemoteFolder(1);
hasUnsyncedRemoteMessage();
imapSync.sync(FOLDER_NAME, syncConfig, listener);
verify(remoteFolder, atLeastOnce()).fetch(any(List.class), fetchProfileCaptor.capture(),
nullable(MessageRetrievalListener.class), anyInt());
assertTrue(fetchProfileCaptor.getAllValues().get(0).contains(FetchProfile.Item.FLAGS));
assertTrue(fetchProfileCaptor.getAllValues().get(0).contains(FetchProfile.Item.ENVELOPE));
assertEquals(2, fetchProfileCaptor.getAllValues().get(0).size());
}
@Test
public void sync_withUnsyncedNewSmallMessage_shouldFetchBodyOfSmallMessage() throws Exception {
ImapMessage smallMessage = buildSmallNewMessage();
messageCountInRemoteFolder(1);
hasUnsyncedRemoteMessage();
respondToFetchEnvelopesWithMessage(smallMessage);
imapSync.sync(FOLDER_NAME, syncConfig, listener);
verify(remoteFolder, atLeast(2)).fetch(any(List.class), fetchProfileCaptor.capture(),
nullable(MessageRetrievalListener.class), anyInt());
assertEquals(1, fetchProfileCaptor.getAllValues().get(1).size());
assertTrue(fetchProfileCaptor.getAllValues().get(1).contains(FetchProfile.Item.BODY));
}
@Test
public void sync_withUnsyncedNewSmallMessage_shouldFetchStructureAndLimitedBodyOfLargeMessage() throws Exception {
ImapMessage largeMessage = buildLargeNewMessage();
messageCountInRemoteFolder(1);
hasUnsyncedRemoteMessage();
respondToFetchEnvelopesWithMessage(largeMessage);
imapSync.sync(FOLDER_NAME, syncConfig, listener);
//TODO: Don't bother fetching messages of a size we don't have
verify(remoteFolder, atLeast(4)).fetch(any(List.class), fetchProfileCaptor.capture(),
nullable(MessageRetrievalListener.class), anyInt());
assertEquals(1, fetchProfileCaptor.getAllValues().get(2).size());
assertEquals(FetchProfile.Item.STRUCTURE, fetchProfileCaptor.getAllValues().get(2).get(0));
assertEquals(1, fetchProfileCaptor.getAllValues().get(3).size());
assertEquals(FetchProfile.Item.BODY_SANE, fetchProfileCaptor.getAllValues().get(3).get(0));
}
@Test
public void sync_withUidValidityChange_shouldClearAllMessages() {
when(backendFolder.getFolderExtraNumber(EXTRA_UID_VALIDITY)).thenReturn(23L);
imapSync.sync(FOLDER_NAME, syncConfig, listener);
verify(backendFolder).clearAllMessages();
verify(backendFolder).setFolderExtraNumber(EXTRA_UID_VALIDITY, FOLDER_UID_VALIDITY);
}
@Test
public void sync_withoutUidValidityChange_shouldNotClearAllMessages() {
when(backendFolder.getFolderExtraNumber(EXTRA_UID_VALIDITY)).thenReturn(FOLDER_UID_VALIDITY);
imapSync.sync(FOLDER_NAME, syncConfig, listener);
verify(backendFolder, never()).clearAllMessages();
}
@Test
public void sync_withFirstUidValidityValue_shouldNotClearAllMessages() {
when(backendFolder.getFolderExtraNumber(EXTRA_UID_VALIDITY)).thenReturn(null);
imapSync.sync(FOLDER_NAME, syncConfig, listener);
verify(backendFolder, never()).clearAllMessages();
verify(backendFolder).setFolderExtraNumber(EXTRA_UID_VALIDITY, FOLDER_UID_VALIDITY);
}
private void respondToFetchEnvelopesWithMessage(final ImapMessage message) throws MessagingException {
doAnswer(new Answer() {
@Override
public Void answer(InvocationOnMock invocation) {
FetchProfile fetchProfile = (FetchProfile) invocation.getArguments()[1];
if (invocation.getArguments()[2] != null) {
MessageRetrievalListener listener = (MessageRetrievalListener) invocation.getArguments()[2];
if (fetchProfile.contains(FetchProfile.Item.ENVELOPE)) {
listener.messageStarted("UID", 1, 1);
listener.messageFinished(message, 1, 1);
listener.messagesFinished(1);
}
}
return null;
}
}).when(remoteFolder).fetch(any(List.class), any(FetchProfile.class), nullable(MessageRetrievalListener.class),
anyInt());
}
private ImapMessage buildSmallNewMessage() {
ImapMessage message = mock(ImapMessage.class);
when(message.olderThan(nullable(Date.class))).thenReturn(false);
when(message.getSize()).thenReturn((long) MAXIMUM_SMALL_MESSAGE_SIZE);
return message;
}
private ImapMessage buildLargeNewMessage() {
ImapMessage message = mock(ImapMessage.class);
when(message.olderThan(nullable(Date.class))).thenReturn(false);
when(message.getSize()).thenReturn((long) (MAXIMUM_SMALL_MESSAGE_SIZE + 1));
return message;
}
private void messageCountInRemoteFolder(int value) {
when(remoteFolder.getMessageCount()).thenReturn(value);
}
private ImapMessage messageOnServer() throws MessagingException {
String messageUid = "UID";
ImapMessage remoteMessage = mock(ImapMessage.class);
when(remoteMessage.getUid()).thenReturn(messageUid);
when(remoteFolder.getMessages(anyInt(), anyInt(), nullable(Date.class),
nullable(MessageRetrievalListener.class))).thenReturn(Collections.singletonList(remoteMessage));
return remoteMessage;
}
private void hasUnsyncedRemoteMessage() throws MessagingException {
String messageUid = "UID";
ImapMessage remoteMessage = mock(ImapMessage.class);
when(remoteMessage.getUid()).thenReturn(messageUid);
when(remoteFolder.getMessages(anyInt(), anyInt(), nullable(Date.class),
nullable(MessageRetrievalListener.class))).thenReturn(Collections.singletonList(remoteMessage));
}
private void configureSyncConfig() {
syncConfig = new SyncConfig(
ExpungePolicy.MANUALLY,
null,
true,
MAXIMUM_SMALL_MESSAGE_SIZE,
DEFAULT_VISIBLE_LIMIT,
SYNC_FLAGS);
}
private void configureRemoteStoreWithFolder() {
when(remoteStore.getFolder(FOLDER_NAME)).thenReturn(remoteFolder);
when(remoteFolder.getServerId()).thenReturn(FOLDER_NAME);
when(remoteFolder.getUidValidity()).thenReturn(FOLDER_UID_VALIDITY);
}
private void configureBackendStorage() {
when(backendStorage.getFolder(FOLDER_NAME)).thenReturn(backendFolder);
}
private void configureSyncConfigWithExpungePolicy(ExpungePolicy expungePolicy) {
syncConfig = syncConfig.copy(
expungePolicy,
syncConfig.getEarliestPollDate(),
syncConfig.getSyncRemoteDeletions(),
syncConfig.getMaximumAutoDownloadMessageSize(),
syncConfig.getDefaultVisibleLimit(),
syncConfig.getSyncFlags());
}
private void configureSyncConfigWithSyncRemoteDeletions(boolean syncRemoteDeletions) {
syncConfig = syncConfig.copy(
syncConfig.getExpungePolicy(),
syncConfig.getEarliestPollDate(),
syncRemoteDeletions,
syncConfig.getMaximumAutoDownloadMessageSize(),
syncConfig.getDefaultVisibleLimit(),
syncConfig.getSyncFlags());
}
private void configureSyncConfigWithSyncRemoteDeletionsAndEarliestPollDate(Date earliestPollDate) {
syncConfig = syncConfig.copy(
syncConfig.getExpungePolicy(),
earliestPollDate,
true,
syncConfig.getMaximumAutoDownloadMessageSize(),
syncConfig.getDefaultVisibleLimit(),
syncConfig.getSyncFlags());
}
}

View file

@ -0,0 +1,277 @@
package com.fsck.k9.backend.imap
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.SyncListener
import com.fsck.k9.mail.Flag
import com.fsck.k9.mail.FolderType
import com.fsck.k9.mail.Message
import com.fsck.k9.mail.MessageDownloadState
import com.fsck.k9.mail.buildMessage
import com.google.common.truth.Truth.assertThat
import java.util.Date
import org.apache.james.mime4j.dom.field.DateTimeField
import org.apache.james.mime4j.field.DefaultFieldParser
import org.junit.Ignore
import org.junit.Test
import org.mockito.Mockito.verify
import org.mockito.kotlin.any
import org.mockito.kotlin.eq
import org.mockito.kotlin.mock
import org.mockito.kotlin.never
private const val ACCOUNT_NAME = "Account-1"
private const val FOLDER_SERVER_ID = "FOLDER_ONE"
private const val MAXIMUM_AUTO_DOWNLOAD_MESSAGE_SIZE = 1000
private const val DEFAULT_VISIBLE_LIMIT = 25
private const val DEFAULT_MESSAGE_DATE = "Tue, 04 Jan 2022 10:00:00 +0100"
class ImapSyncTest {
private val backendStorage = createBackendStorage()
private val backendFolder = backendStorage.getFolder(FOLDER_SERVER_ID)
private val imapStore = TestImapStore()
private val imapFolder = imapStore.addFolder(FOLDER_SERVER_ID)
private val imapSync = ImapSync(ACCOUNT_NAME, backendStorage, imapStore)
private val syncListener = mock<SyncListener>()
private val defaultSyncConfig = createSyncConfig()
@Test
fun `sync of empty folder should notify listener`() {
imapSync.sync(FOLDER_SERVER_ID, defaultSyncConfig, syncListener)
verify(syncListener).syncStarted(FOLDER_SERVER_ID)
verify(syncListener).syncAuthenticationSuccess()
verify(syncListener).syncFinished(FOLDER_SERVER_ID)
verify(syncListener, never()).syncFailed(folderServerId = any(), message = any(), exception = any())
}
@Test
fun `sync of folder with negative messageCount should return an error`() {
imapFolder.messageCount = -1
imapSync.sync(FOLDER_SERVER_ID, defaultSyncConfig, syncListener)
verify(syncListener).syncFailed(
folderServerId = eq(FOLDER_SERVER_ID),
message = eq("Exception: Message count -1 for folder $FOLDER_SERVER_ID"),
exception = any()
)
}
@Test
fun `successful sync should close folder`() {
imapSync.sync(FOLDER_SERVER_ID, defaultSyncConfig, syncListener)
assertThat(imapFolder.isClosed).isTrue()
}
@Test
fun `sync with error should close folder`() {
imapFolder.messageCount = -1
imapSync.sync(FOLDER_SERVER_ID, defaultSyncConfig, syncListener)
assertThat(imapFolder.isClosed).isTrue()
}
@Test
fun `sync with ExpungePolicy ON_POLL should expunge remote folder`() {
val syncConfig = defaultSyncConfig.copy(expungePolicy = ExpungePolicy.ON_POLL)
imapSync.sync(FOLDER_SERVER_ID, syncConfig, syncListener)
assertThat(imapFolder.wasExpunged).isTrue()
}
@Test
fun `sync with ExpungePolicy MANUALLY should not expunge remote folder`() {
val syncConfig = defaultSyncConfig.copy(expungePolicy = ExpungePolicy.MANUALLY)
imapSync.sync(FOLDER_SERVER_ID, syncConfig, syncListener)
assertThat(imapFolder.wasExpunged).isFalse()
}
@Test
fun `sync with ExpungePolicy IMMEDIATELY should not expunge remote folder`() {
val syncConfig = defaultSyncConfig.copy(expungePolicy = ExpungePolicy.IMMEDIATELY)
imapSync.sync(FOLDER_SERVER_ID, syncConfig, syncListener)
assertThat(imapFolder.wasExpunged).isFalse()
}
@Test
fun `sync with syncRemoteDeletions=true should remove local messages`() {
addMessageToBackendFolder(uid = 42)
val syncConfig = defaultSyncConfig.copy(syncRemoteDeletions = true)
imapSync.sync(FOLDER_SERVER_ID, syncConfig, syncListener)
assertThat(backendFolder.getMessageServerIds()).isEmpty()
verify(syncListener).syncStarted(FOLDER_SERVER_ID)
verify(syncListener).syncFinished(FOLDER_SERVER_ID)
}
@Test
fun `sync with syncRemoteDeletions=false should not remove local messages`() {
addMessageToBackendFolder(uid = 23)
val syncConfig = defaultSyncConfig.copy(syncRemoteDeletions = false)
imapSync.sync(FOLDER_SERVER_ID, syncConfig, syncListener)
assertThat(backendFolder.getMessageServerIds()).containsExactly("23")
verify(syncListener).syncStarted(FOLDER_SERVER_ID)
verify(syncListener).syncFinished(FOLDER_SERVER_ID)
}
@Test
fun `sync should remove messages older than earliestPollDate`() {
addMessageToImapAndBackendFolder(uid = 23, date = "Mon, 03 Jan 2022 10:00:00 +0100")
addMessageToImapAndBackendFolder(uid = 42, date = "Wed, 05 Jan 2022 20:00:00 +0100")
val syncConfig = defaultSyncConfig.copy(
syncRemoteDeletions = true,
earliestPollDate = "Tue, 04 Jan 2022 12:00:00 +0100".toDate()
)
imapSync.sync(FOLDER_SERVER_ID, syncConfig, syncListener)
assertThat(backendFolder.getMessageServerIds()).containsExactly("42")
}
@Test
fun `sync with new messages on server should download messages`() {
addMessageToImapFolder(uid = 9)
addMessageToImapFolder(uid = 13)
imapSync.sync(FOLDER_SERVER_ID, defaultSyncConfig, syncListener)
assertThat(backendFolder.getMessageServerIds()).containsExactly("9", "13")
verify(syncListener).syncNewMessage(FOLDER_SERVER_ID, messageServerId = "9", isOldMessage = false)
verify(syncListener).syncNewMessage(FOLDER_SERVER_ID, messageServerId = "13", isOldMessage = false)
}
@Test
fun `sync downloading old messages should notify listener with isOldMessage=true`() {
addMessageToBackendFolder(uid = 42)
addMessageToImapFolder(uid = 23)
addMessageToImapFolder(uid = 42)
imapSync.sync(FOLDER_SERVER_ID, defaultSyncConfig, syncListener)
assertThat(backendFolder.getMessageServerIds()).containsExactly("23", "42")
verify(syncListener).syncNewMessage(FOLDER_SERVER_ID, messageServerId = "23", isOldMessage = true)
}
@Test
@Ignore("This is currently broken")
fun `determining the highest UID should use numerical ordering`() {
addMessageToBackendFolder(uid = 9)
addMessageToBackendFolder(uid = 100)
// When text ordering is used: "9" > "100" -> highest UID = 9 (when it should be 100)
// With 80 > 9 the message on the server is considered a new message, but it shouldn't be (80 < 100)
addMessageToImapFolder(uid = 80)
imapSync.sync(FOLDER_SERVER_ID, defaultSyncConfig, syncListener)
verify(syncListener).syncNewMessage(FOLDER_SERVER_ID, messageServerId = "80", isOldMessage = true)
}
@Test
fun `sync should update flags of existing messages`() {
addMessageToBackendFolder(uid = 2)
addMessageToImapFolder(uid = 2, flags = setOf(Flag.SEEN, Flag.ANSWERED))
imapSync.sync(FOLDER_SERVER_ID, defaultSyncConfig, syncListener)
assertThat(backendFolder.getMessageFlags(messageServerId = "2")).containsAtLeast(Flag.SEEN, Flag.ANSWERED)
}
@Test
fun `sync with UIDVALIDITY change should clear all messages`() {
imapFolder.setUidValidity(1)
addMessageToImapFolder(uid = 300)
addMessageToImapFolder(uid = 301)
val syncConfig = defaultSyncConfig.copy(syncRemoteDeletions = false)
imapSync.sync(FOLDER_SERVER_ID, syncConfig, syncListener)
assertThat(backendFolder.getMessageServerIds()).containsExactly("300", "301")
imapFolder.setUidValidity(9000)
imapFolder.removeAllMessages()
addMessageToImapFolder(uid = 1)
imapSync.sync(FOLDER_SERVER_ID, syncConfig, syncListener)
assertThat(backendFolder.getMessageServerIds()).containsExactly("1")
verify(syncListener).syncNewMessage(FOLDER_SERVER_ID, messageServerId = "1", isOldMessage = false)
}
private fun addMessageToBackendFolder(uid: Long, date: String = DEFAULT_MESSAGE_DATE) {
val messageServerId = uid.toString()
val message = createSimpleMessage(messageServerId, date).apply {
setUid(messageServerId)
}
backendFolder.saveMessage(message, MessageDownloadState.FULL)
}
private fun addMessageToImapFolder(uid: Long, flags: Set<Flag> = emptySet(), date: String = DEFAULT_MESSAGE_DATE) {
val messageServerId = uid.toString()
val message = createSimpleMessage(messageServerId, date)
imapFolder.addMessage(uid, message)
if (flags.isNotEmpty()) {
val imapMessage = imapFolder.getMessage(messageServerId)
imapFolder.setFlags(listOf(imapMessage), flags, true)
}
}
private fun addMessageToImapAndBackendFolder(uid: Long, date: String) {
addMessageToBackendFolder(uid, date)
addMessageToImapFolder(uid, date = date)
}
private fun createBackendStorage(): InMemoryBackendStorage {
return InMemoryBackendStorage().apply {
createFolderUpdater().use { updater ->
val folderInfo = FolderInfo(
serverId = FOLDER_SERVER_ID,
name = "irrelevant",
type = FolderType.REGULAR
)
updater.createFolders(listOf(folderInfo))
}
}
}
private fun createSyncConfig(): SyncConfig {
return SyncConfig(
expungePolicy = ExpungePolicy.MANUALLY,
earliestPollDate = null,
syncRemoteDeletions = true,
maximumAutoDownloadMessageSize = MAXIMUM_AUTO_DOWNLOAD_MESSAGE_SIZE,
defaultVisibleLimit = DEFAULT_VISIBLE_LIMIT,
syncFlags = setOf(Flag.SEEN, Flag.FLAGGED, Flag.ANSWERED, Flag.FORWARDED)
)
}
private fun createSimpleMessage(uid: String, date: String, text: String = "UID: $uid"): Message {
return buildMessage {
header("Subject", "Test Message")
header("From", "alice@domain.example")
header("To", "Bob <bob@domain.example>")
header("Date", date)
header("Message-ID", "<msg-$uid@domain.example>")
textBody(text)
}
}
}
private fun String.toDate(): Date {
val dateTimeField = DefaultFieldParser.parse("Date: $this") as DateTimeField
return dateTimeField.date
}

View file

@ -0,0 +1,179 @@
package com.fsck.k9.backend.imap
import com.fsck.k9.mail.BodyFactory
import com.fsck.k9.mail.FetchProfile
import com.fsck.k9.mail.Flag
import com.fsck.k9.mail.Message
import com.fsck.k9.mail.MessageRetrievalListener
import com.fsck.k9.mail.Part
import com.fsck.k9.mail.store.imap.ImapFolder
import com.fsck.k9.mail.store.imap.ImapMessage
import com.fsck.k9.mail.store.imap.OpenMode
import com.fsck.k9.mail.store.imap.createImapMessage
import java.util.Date
class TestImapFolder(override val serverId: String) : ImapFolder {
override var mode: OpenMode? = null
private set
override var messageCount: Int = 0
var wasExpunged: Boolean = false
private set
val isClosed: Boolean
get() = mode == null
private val messages = mutableMapOf<Long, Message>()
private val messageFlags = mutableMapOf<Long, MutableSet<Flag>>()
private var uidValidity: Long? = null
fun addMessage(uid: Long, message: Message) {
require(!messages.containsKey(uid)) {
"Folder '$serverId' already contains a message with the UID $uid"
}
messages[uid] = message
messageFlags[uid] = mutableSetOf()
messageCount = messages.size
}
fun removeAllMessages() {
messages.clear()
messageFlags.clear()
}
fun setUidValidity(value: Long) {
uidValidity = value
}
override fun open(mode: OpenMode) {
this.mode = mode
}
override fun close() {
mode = null
}
override fun exists(): Boolean {
throw UnsupportedOperationException("not implemented")
}
override fun getUidValidity() = uidValidity
override fun getMessage(uid: String): ImapMessage {
return createImapMessage(uid)
}
override fun getUidFromMessageId(messageId: String): String? {
throw UnsupportedOperationException("not implemented")
}
override fun getMessages(
start: Int,
end: Int,
earliestDate: Date?,
listener: MessageRetrievalListener<ImapMessage>?
): List<ImapMessage> {
require(start > 0)
require(end >= start)
require(end <= messages.size)
return messages.keys.sortedDescending()
.slice((start - 1) until end)
.map { createImapMessage(uid = it.toString()) }
}
override fun areMoreMessagesAvailable(indexOfOldestMessage: Int, earliestDate: Date?): Boolean {
throw UnsupportedOperationException("not implemented")
}
override fun fetch(
messages: List<ImapMessage>,
fetchProfile: FetchProfile,
listener: MessageRetrievalListener<ImapMessage>?,
maxDownloadSize: Int
) {
if (messages.isEmpty()) return
messages.forEachIndexed { index, imapMessage ->
val uid = imapMessage.uid.toLong()
val flags = messageFlags[uid].orEmpty().toSet()
imapMessage.setFlags(flags, true)
val storedMessage = this.messages[uid] ?: error("Message $uid not found")
for (header in storedMessage.headers) {
imapMessage.addHeader(header.name, header.value)
}
imapMessage.body = storedMessage.body
listener?.messageFinished(imapMessage, index, messages.size)
}
}
override fun fetchPart(
message: ImapMessage,
part: Part,
listener: MessageRetrievalListener<ImapMessage>?,
bodyFactory: BodyFactory,
maxDownloadSize: Int
) {
throw UnsupportedOperationException("not implemented")
}
override fun search(
queryString: String?,
requiredFlags: Set<Flag>?,
forbiddenFlags: Set<Flag>?,
performFullTextSearch: Boolean
): List<ImapMessage> {
throw UnsupportedOperationException("not implemented")
}
override fun appendMessages(messages: List<Message>): Map<String, String>? {
throw UnsupportedOperationException("not implemented")
}
override fun setFlags(flags: Set<Flag>, value: Boolean) {
if (value) {
for (messageFlagSet in messageFlags.values) {
messageFlagSet.addAll(flags)
}
} else {
for (messageFlagSet in messageFlags.values) {
messageFlagSet.removeAll(flags)
}
}
}
override fun setFlags(messages: List<ImapMessage>, flags: Set<Flag>, value: Boolean) {
for (message in messages) {
val uid = message.uid.toLong()
val messageFlagSet = messageFlags[uid] ?: error("Unknown message with UID $uid")
if (value) {
messageFlagSet.addAll(flags)
} else {
messageFlagSet.removeAll(flags)
}
}
}
override fun copyMessages(messages: List<ImapMessage>, folder: ImapFolder): Map<String, String>? {
throw UnsupportedOperationException("not implemented")
}
override fun moveMessages(messages: List<ImapMessage>, folder: ImapFolder): Map<String, String>? {
throw UnsupportedOperationException("not implemented")
}
override fun expunge() {
mode = OpenMode.READ_WRITE
wasExpunged = true
}
override fun expungeUids(uids: List<String>) {
throw UnsupportedOperationException("not implemented")
}
}

View file

@ -0,0 +1,41 @@
package com.fsck.k9.backend.imap
import com.fsck.k9.mail.FolderType
import com.fsck.k9.mail.store.imap.FolderListItem
import com.fsck.k9.mail.store.imap.ImapFolder
import com.fsck.k9.mail.store.imap.ImapStore
class TestImapStore : ImapStore {
private val folders = mutableMapOf<String, TestImapFolder>()
fun addFolder(name: String): TestImapFolder {
require(!folders.containsKey(name)) { "Folder '$name' already exists" }
return TestImapFolder(name).also { folder ->
folders[name] = folder
}
}
override fun getFolder(name: String): ImapFolder {
return folders[name] ?: error("Folder '$name' not found")
}
override fun getFolders(): List<FolderListItem> {
return folders.values.map { folder ->
FolderListItem(
serverId = folder.serverId,
name = "irrelevant",
type = FolderType.REGULAR,
oldServerId = null
)
}
}
override fun checkSettings() {
throw UnsupportedOperationException("not implemented")
}
override fun closeAllConnections() {
throw UnsupportedOperationException("not implemented")
}
}

View file

@ -0,0 +1,3 @@
package com.fsck.k9.mail.store.imap
fun createImapMessage(uid: String) = ImapMessage(uid)

View file

@ -1,6 +1,7 @@
package app.k9mail.backend.testing
import com.fsck.k9.backend.api.BackendFolder
import com.fsck.k9.backend.api.BackendFolder.MoreMessages
import com.fsck.k9.mail.Flag
import com.fsck.k9.mail.FolderType
import com.fsck.k9.mail.Message
@ -17,6 +18,9 @@ class InMemoryBackendFolder(override var name: String, var type: FolderType) : B
val extraNumbers: MutableMap<String, Long> = mutableMapOf()
private val messages = mutableMapOf<String, Message>()
private val messageFlags = mutableMapOf<String, MutableSet<Flag>>()
private var moreMessages: MoreMessages = MoreMessages.UNKNOWN
private var status: String? = null
private var lastChecked = 0L
override var visibleLimit: Int = 25
@ -60,7 +64,11 @@ class InMemoryBackendFolder(override var name: String, var type: FolderType) : B
}
override fun getAllMessagesAndEffectiveDates(): Map<String, Long?> {
throw UnsupportedOperationException("not implemented")
return messages
.map { (serverId, message) ->
serverId to message.sentDate.time
}
.toMap()
}
override fun destroyMessages(messageServerIds: List<String>) {
@ -75,27 +83,28 @@ class InMemoryBackendFolder(override var name: String, var type: FolderType) : B
}
override fun getLastUid(): Long? {
throw UnsupportedOperationException("not implemented")
// This is using string ordering because that's what K9BackendFolder is using, too.
return messages.keys
.maxOrNull()
?.toLongOrNull()
}
override fun getMoreMessages(): BackendFolder.MoreMessages {
throw UnsupportedOperationException("not implemented")
}
override fun getMoreMessages(): MoreMessages = moreMessages
override fun setMoreMessages(moreMessages: BackendFolder.MoreMessages) {
throw UnsupportedOperationException("not implemented")
override fun setMoreMessages(moreMessages: MoreMessages) {
this.moreMessages = moreMessages
}
override fun setLastChecked(timestamp: Long) {
throw UnsupportedOperationException("not implemented")
lastChecked = timestamp
}
override fun setStatus(status: String?) {
throw UnsupportedOperationException("not implemented")
this.status = status
}
override fun isMessagePresent(messageServerId: String): Boolean {
throw UnsupportedOperationException("not implemented")
return messages[messageServerId] != null
}
override fun getMessageFlags(messageServerId: String): Set<Flag> {