diff --git a/k9mail/src/test/java/com/fsck/k9/controller/imap/ImapSyncTest.java b/k9mail/src/test/java/com/fsck/k9/controller/imap/ImapSyncTest.java new file mode 100644 index 000000000..f887c84d3 --- /dev/null +++ b/k9mail/src/test/java/com/fsck/k9/controller/imap/ImapSyncTest.java @@ -0,0 +1,401 @@ +package com.fsck.k9.controller.imap; + + +import java.util.Collections; +import java.util.Date; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import android.content.Context; + +import com.fsck.k9.Account; +import com.fsck.k9.AccountStats; +import com.fsck.k9.K9RobolectricTestRunner; +import com.fsck.k9.controller.MessagingController; +import com.fsck.k9.controller.MessagingListener; +import com.fsck.k9.controller.SimpleMessagingListener; +import com.fsck.k9.mail.FetchProfile; +import com.fsck.k9.mail.Folder; +import com.fsck.k9.mail.Message; +import com.fsck.k9.mail.MessageRetrievalListener; +import com.fsck.k9.mail.MessagingException; +import com.fsck.k9.mail.Store; +import com.fsck.k9.mailstore.LocalFolder; +import com.fsck.k9.mailstore.LocalMessage; +import com.fsck.k9.mailstore.LocalStore; +import com.fsck.k9.notification.NotificationController; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +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 org.robolectric.shadows.ShadowApplication; +import org.robolectric.shadows.ShadowLog; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.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") +@RunWith(K9RobolectricTestRunner.class) +public class ImapSyncTest { + private static final String FOLDER_NAME = "Folder"; + private static final int MAXIMUM_SMALL_MESSAGE_SIZE = 1000; + private static final String MESSAGE_UID1 = "message-uid1"; + + + private ImapSync imapSync; + @Mock + private MessagingController controller; + @Mock + private Account account; + @Mock + private AccountStats accountStats; + @Mock + private SimpleMessagingListener listener; + @Mock + private LocalFolder localFolder; + @Mock + private Folder remoteFolder; + @Mock + private LocalStore localStore; + @Mock + private Store remoteStore; + @Mock + private NotificationController notificationController; + @Captor + private ArgumentCaptor> messageListCaptor; + @Captor + private ArgumentCaptor fetchProfileCaptor; + + private Context appContext; + + + @Before + public void setUp() throws MessagingException { + ShadowLog.stream = System.out; + MockitoAnnotations.initMocks(this); + appContext = ShadowApplication.getInstance().getApplicationContext(); + + imapSync = new ImapSync(notificationController, controller, appContext); + + setUpMessagingController(); + configureAccount(); + configureLocalStore(); + } + + @Test + public void sync_withOneMessageInRemoteFolder_shouldFinishWithoutError() throws Exception { + messageCountInRemoteFolder(1); + + imapSync.sync(account, FOLDER_NAME, listener, remoteFolder); + + verify(listener).synchronizeMailboxFinished(account, FOLDER_NAME, 1, 0); + } + + @Test + public void sync_withEmptyRemoteFolder_shouldFinishWithoutError() throws Exception { + messageCountInRemoteFolder(0); + + imapSync.sync(account, FOLDER_NAME, listener, remoteFolder); + + verify(listener).synchronizeMailboxFinished(account, FOLDER_NAME, 0, 0); + } + + @Test + public void sync_withNegativeMessageCountInRemoteFolder_shouldFinishWithError() throws Exception { + messageCountInRemoteFolder(-1); + + imapSync.sync(account, FOLDER_NAME, listener, remoteFolder); + + verify(listener).synchronizeMailboxFailed(account, FOLDER_NAME, + "Exception: Message count -1 for folder Folder"); + } + + @Test + public void sync_withRemoteFolderProvided_shouldNotOpenRemoteFolder() throws Exception { + messageCountInRemoteFolder(1); + + imapSync.sync(account, FOLDER_NAME, listener, remoteFolder); + + verify(remoteFolder, never()).open(Folder.OPEN_MODE_RW); + } + + @Test + public void sync_withNoRemoteFolderProvided_shouldOpenRemoteFolderFromStore() throws Exception { + messageCountInRemoteFolder(1); + configureRemoteStoreWithFolder(); + + imapSync.sync(account, FOLDER_NAME, listener, null); + + verify(remoteFolder).open(Folder.OPEN_MODE_RO); + } + + @Test + public void sync_withRemoteFolderProvided_shouldNotCloseRemoteFolder() throws Exception { + messageCountInRemoteFolder(1); + + imapSync.sync(account, FOLDER_NAME, listener, remoteFolder); + + verify(remoteFolder, never()).close(); + } + + @Test + public void sync_withNoRemoteFolderProvided_shouldCloseRemoteFolderFromStore() throws Exception { + messageCountInRemoteFolder(1); + configureRemoteStoreWithFolder(); + + imapSync.sync(account, FOLDER_NAME, listener, null); + + verify(remoteFolder).close(); + } + + @Test + public void sync_withAccountPolicySetToExpungeOnPoll_shouldExpungeRemoteFolder() throws Exception { + messageCountInRemoteFolder(1); + when(account.getExpungePolicy()).thenReturn(Account.Expunge.EXPUNGE_ON_POLL); + configureRemoteStoreWithFolder(); + + imapSync.sync(account, FOLDER_NAME, listener, null); + + verify(remoteFolder).expunge(); + } + + @Test + public void sync_withAccountPolicySetToExpungeManually_shouldNotExpungeRemoteFolder() throws Exception { + messageCountInRemoteFolder(1); + when(account.getExpungePolicy()).thenReturn(Account.Expunge.EXPUNGE_MANUALLY); + + imapSync.sync(account, FOLDER_NAME, listener, null); + + verify(remoteFolder, never()).expunge(); + } + + @Test + public void sync_withAccountSetToSyncRemoteDeletions_shouldDeleteLocalCopiesOfDeletedMessages() throws Exception { + messageCountInRemoteFolder(0); + LocalMessage localCopyOfRemoteDeletedMessage = mock(LocalMessage.class); + when(account.syncRemoteDeletions()).thenReturn(true); + when(localFolder.getAllMessagesAndEffectiveDates()).thenReturn(Collections.singletonMap(MESSAGE_UID1, 0L)); + when(localFolder.getMessagesByUids(any(List.class))) + .thenReturn(Collections.singletonList(localCopyOfRemoteDeletedMessage)); + + imapSync.sync(account, FOLDER_NAME, listener, remoteFolder); + + verify(localFolder).destroyMessages(messageListCaptor.capture()); + assertEquals(localCopyOfRemoteDeletedMessage, messageListCaptor.getValue().get(0)); + } + + @Test + public void sync_withAccountSetToSyncRemoteDeletions_shouldNotDeleteLocalCopiesOfExistingMessagesAfterEarliestPollDate() + throws Exception { + messageCountInRemoteFolder(1); + Date dateOfEarliestPoll = new Date(); + LocalMessage localMessage = localMessageWithCopyOnServer(); + when(account.syncRemoteDeletions()).thenReturn(true); + when(account.getEarliestPollDate()).thenReturn(dateOfEarliestPoll); + when(localMessage.olderThan(dateOfEarliestPoll)).thenReturn(false); + when(localFolder.getMessages(null)).thenReturn(Collections.singletonList(localMessage)); + + imapSync.sync(account, FOLDER_NAME, listener, remoteFolder); + + verify(localFolder, never()).destroyMessages(messageListCaptor.capture()); + } + + @Test + public void sync_withAccountSetToSyncRemoteDeletions_shouldDeleteLocalCopiesOfExistingMessagesBeforeEarliestPollDate() + throws Exception { + messageCountInRemoteFolder(1); + LocalMessage localMessage = localMessageWithCopyOnServer(); + Date dateOfEarliestPoll = new Date(); + when(account.syncRemoteDeletions()).thenReturn(true); + when(account.getEarliestPollDate()).thenReturn(dateOfEarliestPoll); + when(localMessage.olderThan(dateOfEarliestPoll)).thenReturn(true); + when(localFolder.getAllMessagesAndEffectiveDates()).thenReturn(Collections.singletonMap(MESSAGE_UID1, 0L)); + when(localFolder.getMessagesByUids(any(List.class))).thenReturn(Collections.singletonList(localMessage)); + + imapSync.sync(account, FOLDER_NAME, listener, remoteFolder); + + verify(localFolder).destroyMessages(messageListCaptor.capture()); + assertEquals(localMessage, messageListCaptor.getValue().get(0)); + } + + @Test + public void sync_withAccountSetNotToSyncRemoteDeletions_shouldNotDeleteLocalCopiesOfMessages() throws Exception { + messageCountInRemoteFolder(0); + LocalMessage remoteDeletedMessage = mock(LocalMessage.class); + when(account.syncRemoteDeletions()).thenReturn(false); + when(localFolder.getMessages(null)).thenReturn(Collections.singletonList(remoteDeletedMessage)); + + imapSync.sync(account, FOLDER_NAME, listener, remoteFolder); + + verify(localFolder, never()).destroyMessages(messageListCaptor.capture()); + } + + @Test + public void sync_withAccountSupportingFetchingFlags_shouldFetchUnsychronizedMessagesListAndFlags() + throws Exception { + messageCountInRemoteFolder(1); + hasUnsyncedRemoteMessage(); + when(remoteFolder.supportsFetchingFlags()).thenReturn(true); + + imapSync.sync(account, FOLDER_NAME, listener, remoteFolder); + + verify(remoteFolder, atLeastOnce()).fetch(any(List.class), fetchProfileCaptor.capture(), + any(MessageRetrievalListener.class)); + 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_withAccountNotSupportingFetchingFlags_shouldFetchUnsychronizedMessages() throws Exception { + messageCountInRemoteFolder(1); + hasUnsyncedRemoteMessage(); + when(remoteFolder.supportsFetchingFlags()).thenReturn(false); + + imapSync.sync(account, FOLDER_NAME, listener, remoteFolder); + + verify(remoteFolder, atLeastOnce()).fetch(any(List.class), fetchProfileCaptor.capture(), + any(MessageRetrievalListener.class)); + assertEquals(1, fetchProfileCaptor.getAllValues().get(0).size()); + assertTrue(fetchProfileCaptor.getAllValues().get(0).contains(FetchProfile.Item.ENVELOPE)); + } + + @Test + public void sync_withUnsyncedNewSmallMessage_shouldFetchBodyOfSmallMessage() throws Exception { + Message smallMessage = buildSmallNewMessage(); + messageCountInRemoteFolder(1); + hasUnsyncedRemoteMessage(); + when(remoteFolder.supportsFetchingFlags()).thenReturn(false); + respondToFetchEnvelopesWithMessage(smallMessage); + + imapSync.sync(account, FOLDER_NAME, listener, remoteFolder); + + verify(remoteFolder, atLeast(2)).fetch(any(List.class), fetchProfileCaptor.capture(), + any(MessageRetrievalListener.class)); + assertEquals(1, fetchProfileCaptor.getAllValues().get(1).size()); + assertTrue(fetchProfileCaptor.getAllValues().get(1).contains(FetchProfile.Item.BODY)); + } + + @Test + public void sync_withUnsyncedNewSmallMessage_shouldFetchStructureAndLimitedBodyOfLargeMessage() throws Exception { + Message largeMessage = buildLargeNewMessage(); + messageCountInRemoteFolder(1); + hasUnsyncedRemoteMessage(); + when(remoteFolder.supportsFetchingFlags()).thenReturn(false); + respondToFetchEnvelopesWithMessage(largeMessage); + + imapSync.sync(account, FOLDER_NAME, listener, remoteFolder); + + //TODO: Don't bother fetching messages of a size we don't have + verify(remoteFolder, atLeast(4)).fetch(any(List.class), fetchProfileCaptor.capture(), + any(MessageRetrievalListener.class)); + 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)); + } + + private void respondToFetchEnvelopesWithMessage(final Message message) throws MessagingException { + doAnswer(new Answer() { + @Override + public Void answer(InvocationOnMock invocation) throws Throwable { + 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), any(MessageRetrievalListener.class)); + } + + private Message buildSmallNewMessage() { + Message message = mock(Message.class); + when(message.olderThan(any(Date.class))).thenReturn(false); + when(message.getSize()).thenReturn((long) MAXIMUM_SMALL_MESSAGE_SIZE); + return message; + } + + private Message buildLargeNewMessage() { + Message message = mock(Message.class); + when(message.olderThan(any(Date.class))).thenReturn(false); + when(message.getSize()).thenReturn((long) (MAXIMUM_SMALL_MESSAGE_SIZE + 1)); + return message; + } + + private void messageCountInRemoteFolder(int value) throws MessagingException { + when(remoteFolder.getMessageCount()).thenReturn(value); + } + + private LocalMessage localMessageWithCopyOnServer() throws MessagingException { + String messageUid = "UID"; + Message remoteMessage = mock(Message.class); + LocalMessage localMessage = mock(LocalMessage.class); + + when(remoteMessage.getUid()).thenReturn(messageUid); + when(localMessage.getUid()).thenReturn(messageUid); + when(remoteFolder.getMessages(anyInt(), anyInt(), any(Date.class), any(MessageRetrievalListener.class))) + .thenReturn(Collections.singletonList(remoteMessage)); + return localMessage; + } + + private void hasUnsyncedRemoteMessage() throws MessagingException { + String messageUid = "UID"; + Message remoteMessage = mock(Message.class); + when(remoteMessage.getUid()).thenReturn(messageUid); + when(remoteFolder.getMessages(anyInt(), anyInt(), any(Date.class), any(MessageRetrievalListener.class))) + .thenReturn(Collections.singletonList(remoteMessage)); + } + + private void setUpMessagingController() throws MessagingException { + when(controller.getListeners(any(MessagingListener.class))).thenAnswer(new Answer>() { + @Override + public Set answer(InvocationOnMock invocation) throws Throwable { + MessagingListener listener = invocation.getArgumentAt(0, MessagingListener.class); + Set set = new HashSet<>(1); + set.add(listener); + return set; + } + }); + } + + private void configureAccount() throws MessagingException { + when(account.isAvailable(appContext)).thenReturn(true); + when(account.getLocalStore()).thenReturn(localStore); + when(account.getStats(any(Context.class))).thenReturn(accountStats); + when(account.getMaximumAutoDownloadMessageSize()).thenReturn(MAXIMUM_SMALL_MESSAGE_SIZE); + when(account.getEmail()).thenReturn("user@host.com"); + } + + private void configureLocalStore() throws MessagingException { + when(localStore.getFolder(FOLDER_NAME)).thenReturn(localFolder); + when(localFolder.getName()).thenReturn(FOLDER_NAME); + when(localStore.getPersonalNamespaces(false)).thenReturn(Collections.singletonList(localFolder)); + } + + private void configureRemoteStoreWithFolder() throws MessagingException { + when(account.getRemoteStore()).thenReturn(remoteStore); + when(remoteStore.getFolder(FOLDER_NAME)).thenReturn(remoteFolder); + when(remoteFolder.getName()).thenReturn(FOLDER_NAME); + } +}