Rewrite ImapSyncTest to use less Mockito
This commit is contained in:
parent
f993b32aae
commit
4ab0dff096
7 changed files with 522 additions and 397 deletions
|
@ -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 {
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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")
|
||||
}
|
||||
}
|
|
@ -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")
|
||||
}
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
package com.fsck.k9.mail.store.imap
|
||||
|
||||
fun createImapMessage(uid: String) = ImapMessage(uid)
|
|
@ -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> {
|
||||
|
|
Loading…
Reference in a new issue