diff options
| author | test@dangofactory.cos <you@example.com> | 2026-08-20 16:57:18 +0000 |
|---|---|---|
| committer | test@dangofactory.cos <you@example.com> | 2026-08-20 16:57:18 +0000 |
| commit | 50ce5d5e6cd0d720ff437d35eace545816848428 (patch) | |
| tree | b59af206a36265cf081945e434900b32b0c43edd /mail/protocols/webdav/src/test/java | |
| download | monocles_mail-50ce5d5e6cd0d720ff437d35eace545816848428.tar.gz monocles_mail-50ce5d5e6cd0d720ff437d35eace545816848428.tar.bz2 monocles_mail-50ce5d5e6cd0d720ff437d35eace545816848428.zip | |
Inital
Diffstat (limited to 'mail/protocols/webdav/src/test/java')
3 files changed, 944 insertions, 0 deletions
diff --git a/mail/protocols/webdav/src/test/java/com/fsck/k9/mail/store/webdav/WebDavFolderTest.java b/mail/protocols/webdav/src/test/java/com/fsck/k9/mail/store/webdav/WebDavFolderTest.java new file mode 100644 index 0000000..79f3809 --- /dev/null +++ b/mail/protocols/webdav/src/test/java/com/fsck/k9/mail/store/webdav/WebDavFolderTest.java @@ -0,0 +1,517 @@ +package com.fsck.k9.mail.store.webdav; + + +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.net.URISyntaxException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import com.fsck.k9.mail.FetchProfile; +import com.fsck.k9.mail.Message; +import com.fsck.k9.mail.MessageRetrievalListener; +import com.fsck.k9.mail.MessagingException; +import com.fsck.k9.mail.internet.BinaryTempFileBody; +import org.apache.http.HttpResponse; +import org.apache.http.StatusLine; +import org.apache.http.client.methods.HttpUriRequest; +import org.apache.http.entity.BasicHttpEntity; +import org.apache.http.entity.StringEntity; +import org.apache.http.protocol.HttpContext; +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 java.util.Collections.singletonList; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.anyMap; +import static org.mockito.ArgumentMatchers.isNull; +import static org.mockito.ArgumentMatchers.nullable; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@SuppressWarnings("deprecation") +public class WebDavFolderTest { + private static final int MAX_DOWNLOAD_SIZE = 1900; + + @Mock + private MessageRetrievalListener<WebDavMessage> listener; + @Mock + private WebDavStore mockStore; + @Mock + private DataSet mockDataSet; + @Mock + private WebDavHttpClient mockHttpClient; + @Captor + private ArgumentCaptor<Map<String, String>> headerCaptor; + @Captor + private ArgumentCaptor<String> urlCaptor; + @Captor + private ArgumentCaptor<StringEntity> entityCaptor; + + private WebDavFolder folder; + + private WebDavFolder destinationFolder; + private String storeUrl = "https://localhost/webDavStoreUrl"; + private String folderName = "testFolder"; + private String moveOrCopyXml = "<xml>MoveOrCopyXml</xml>"; + private HashMap<String, String> moveOrCopyHeaders; + private List<WebDavMessage> messages; + + @Before + public void before() throws MessagingException, IOException { + MockitoAnnotations.initMocks(this); + when(mockStore.getUrl()).thenReturn(storeUrl); + when(mockStore.getHttpClient()).thenReturn(mockHttpClient); + when(mockStore.getMessageEnvelopeXml(any(String[].class))).thenReturn("mockEnvelopeXml"); + when(mockStore.getMessageFlagsXml(any(String[].class))).thenReturn("mockFlagsXml"); + folder = new WebDavFolder(mockStore, folderName); + + setupTempDirectory(); + } + + private void setupTempDirectory() { + File tempDirectory = new File("temp"); + if (!tempDirectory.exists()) { + assertTrue(tempDirectory.mkdir()); + tempDirectory.deleteOnExit(); + } + BinaryTempFileBody.setTempDirectory(tempDirectory); + } + + private WebDavFolder setupDestinationFolder() { + WebDavFolder destinationFolder = new WebDavFolder(mockStore, "destFolder"); + when(mockStore.getFolder("destFolder")).thenReturn(destinationFolder); + return destinationFolder; + } + + private void setupFolderWithMessages(int count) throws MessagingException { + HashMap<String, String> headers = new HashMap<>(); + headers.put("Brief", "t"); + String messageCountXml = "<xml>MessageCountXml</xml>"; + when(mockStore.getMessageCountXml("True")).thenReturn(messageCountXml); + when(mockStore.processRequest("https://localhost/webDavStoreUrl/testFolder", + "SEARCH", messageCountXml, headers)).thenReturn(mockDataSet); + when(mockDataSet.getMessageCount()).thenReturn(count); + folder.getMessageCount(); + } + + private WebDavMessage createWebDavMessage(String uid) { + WebDavMessage webDavMessage = mock(WebDavMessage.class); + when(webDavMessage.getUid()).thenReturn(uid); + return webDavMessage; + } + + private WebDavMessage createWebDavMessage(int uid) { + return createWebDavMessage(String.valueOf(uid)); + } + + private void setupGetUrlsRequestResponse(String uid, String url) throws MessagingException { + String getUrlsXml = "<xml>GetUrls</xml>"; + when(mockStore.getMessageUrlsXml(new String[]{uid})).thenReturn(getUrlsXml); + HashMap<String, String> headers = new HashMap<>(); + headers.put("Brief", "t"); + when(mockStore.processRequest("https://localhost/webDavStoreUrl/testFolder", "SEARCH", getUrlsXml, headers)) + .thenReturn(mockDataSet); + Map<String, String> urlUids = new HashMap<>(); + urlUids.put(uid, url); + when(mockDataSet.getUidToUrl()).thenReturn(urlUids); + } + + @Test + public void folder_can_fetch_less_than_10_envelopes() throws MessagingException { + when(mockStore.processRequest(anyString(), anyString(), anyString(), anyMap())).thenReturn(mockDataSet); + + List<WebDavMessage> messages = new ArrayList<>(); + for (int i = 0; i < 5; i++) { + WebDavMessage mockMessage = createWebDavMessage(i); + messages.add(mockMessage); + } + + FetchProfile profile = new FetchProfile(); + profile.add(FetchProfile.Item.ENVELOPE); + folder.fetch(messages, profile, listener, MAX_DOWNLOAD_SIZE); + } + + @Test + public void folder_can_fetch_more_than_10_envelopes() throws MessagingException { + when(mockStore.processRequest(anyString(), anyString(), anyString(), anyMap())).thenReturn(mockDataSet); + + List<WebDavMessage> messages = new ArrayList<>(); + for (int i = 0; i < 15; i++) { + WebDavMessage mockMessage = createWebDavMessage(i); + messages.add(mockMessage); + } + FetchProfile profile = new FetchProfile(); + profile.add(FetchProfile.Item.ENVELOPE); + folder.fetch(messages, profile, listener, MAX_DOWNLOAD_SIZE); + } + + @Test + public void folder_can_fetch_less_than_20_flags() throws MessagingException { + when(mockStore.processRequest(anyString(), anyString(), anyString(), anyMap())).thenReturn(mockDataSet); + + List<WebDavMessage> messages = new ArrayList<>(); + for (int i = 0; i < 5; i++) { + WebDavMessage mockMessage = createWebDavMessage(i); + messages.add(mockMessage); + } + FetchProfile profile = new FetchProfile(); + profile.add(FetchProfile.Item.FLAGS); + folder.fetch(messages, profile, listener, MAX_DOWNLOAD_SIZE); + } + + @Test + public void folder_can_fetch_more_than_20_flags() throws MessagingException { + when(mockStore.processRequest(anyString(), anyString(), anyString(), anyMap())).thenReturn(mockDataSet); + + List<WebDavMessage> messages = new ArrayList<>(); + for (int i = 0; i < 25; i++) { + WebDavMessage mockMessage = createWebDavMessage(i); + messages.add(mockMessage); + } + + FetchProfile profile = new FetchProfile(); + profile.add(FetchProfile.Item.FLAGS); + folder.fetch(messages, profile, listener, MAX_DOWNLOAD_SIZE); + } + + @Test + public void folder_can_fetch_sensible_body_data_and_notifies_listener() + throws MessagingException, IOException, URISyntaxException { + setupStoreForMessageFetching(); + List<WebDavMessage> messages = setup25MessagesToFetch(); + + when(mockHttpClient.executeOverride(any(HttpUriRequest.class), nullable(HttpContext.class))).thenAnswer( + new Answer<HttpResponse>() { + @Override + public HttpResponse answer(InvocationOnMock invocation) throws Throwable { + HttpResponse httpResponse = mock(HttpResponse.class); + StatusLine statusLine = mock(StatusLine.class); + when(httpResponse.getStatusLine()).thenReturn(statusLine); + when(statusLine.getStatusCode()).thenReturn(200); + + BasicHttpEntity httpEntity = new BasicHttpEntity(); + String body = ""; + httpEntity.setContent(new ByteArrayInputStream(body.getBytes("UTF-8"))); + when(httpResponse.getEntity()).thenReturn(httpEntity); + return httpResponse; + } + }); + + FetchProfile profile = new FetchProfile(); + profile.add(FetchProfile.Item.BODY_SANE); + folder.fetch(messages, profile, listener, MAX_DOWNLOAD_SIZE); + verify(listener, times(25)).messageFinished(any(WebDavMessage.class)); + } + + @Test + public void folder_does_not_notify_listener_twice_when_fetching_flags_and_bodies() + throws MessagingException, IOException, URISyntaxException { + setupStoreForMessageFetching(); + when(mockStore.processRequest(anyString(), anyString(), anyString(), anyMap())).thenReturn(mockDataSet); + List<WebDavMessage> messages = setup25MessagesToFetch(); + when(mockHttpClient.executeOverride(any(HttpUriRequest.class), nullable(HttpContext.class))).thenAnswer( + new Answer<HttpResponse>() { + @Override + public HttpResponse answer(InvocationOnMock invocation) throws Throwable { + HttpResponse httpResponse = mock(HttpResponse.class); + StatusLine statusLine = mock(StatusLine.class); + when(httpResponse.getStatusLine()).thenReturn(statusLine); + when(statusLine.getStatusCode()).thenReturn(200); + + BasicHttpEntity httpEntity = new BasicHttpEntity(); + String body = ""; + httpEntity.setContent(new ByteArrayInputStream(body.getBytes("UTF-8"))); + when(httpResponse.getEntity()).thenReturn(httpEntity); + return httpResponse; + } + }); + + FetchProfile profile = new FetchProfile(); + profile.add(FetchProfile.Item.FLAGS); + profile.add(FetchProfile.Item.BODY); + folder.fetch(messages, profile, listener, MAX_DOWNLOAD_SIZE); + verify(listener, times(25)).messageFinished(any(WebDavMessage.class)); + } + + private void setupStoreForMessageFetching() { + String authString = "authString"; + when(mockStore.getAuthentication()).thenReturn(WebDavConstants.AUTH_TYPE_BASIC); + when(mockStore.getAuthString()).thenReturn(authString); + } + + private List<WebDavMessage> setup25MessagesToFetch() { + + List<WebDavMessage> messages = new ArrayList<>(); + for (int i = 0; i < 25; i++) { + WebDavMessage message = new WebDavMessage("message" + i, folder); + message.setUrl("http://example.org/Exchange/user/Inbox/message" + i + ".EML"); + messages.add(message); + } + return messages; + } + + @Test + public void folder_can_handle_empty_response_to_body_request() throws MessagingException, IOException { + setupStoreForMessageFetching(); + List<WebDavMessage> messages = setup25MessagesToFetch(); + + when(mockHttpClient.executeOverride(any(HttpUriRequest.class), nullable(HttpContext.class))).thenAnswer( + new Answer<HttpResponse>() { + @Override + public HttpResponse answer(InvocationOnMock invocation) throws Throwable { + HttpResponse httpResponse = mock(HttpResponse.class); + StatusLine statusLine = mock(StatusLine.class); + when(httpResponse.getStatusLine()).thenReturn(statusLine); + when(statusLine.getStatusCode()).thenReturn(200); + return httpResponse; + } + }); + + FetchProfile profile = new FetchProfile(); + profile.add(FetchProfile.Item.BODY_SANE); + folder.fetch(messages, profile, listener, MAX_DOWNLOAD_SIZE); + verify(listener, times(25)).messageFinished(any(WebDavMessage.class)); + } + + @Test + public void folder_ignores_exception_thrown_when_closing() throws MessagingException, IOException { + setupStoreForMessageFetching(); + List<WebDavMessage> messages = setup25MessagesToFetch(); + + when(mockHttpClient.executeOverride(any(HttpUriRequest.class), nullable(HttpContext.class))).thenAnswer( + new Answer<HttpResponse>() { + @Override + public HttpResponse answer(InvocationOnMock invocation) throws Throwable { + HttpResponse httpResponse = mock(HttpResponse.class); + StatusLine statusLine = mock(StatusLine.class); + when(httpResponse.getStatusLine()).thenReturn(statusLine); + when(statusLine.getStatusCode()).thenReturn(200); + + BasicHttpEntity httpEntity = new BasicHttpEntity(); + InputStream mockInputStream = mock(InputStream.class); + when(mockInputStream.read(any(byte[].class), anyInt(), anyInt())).thenReturn(1).thenReturn(-1); + doThrow(new IOException("Test")).when(mockInputStream).close(); + httpEntity.setContent(mockInputStream); + when(httpResponse.getEntity()).thenReturn(httpEntity); + return httpResponse; + } + }); + + FetchProfile profile = new FetchProfile(); + profile.add(FetchProfile.Item.BODY_SANE); + folder.fetch(messages, profile, listener, MAX_DOWNLOAD_SIZE); + verify(listener, times(25)).messageFinished(any(WebDavMessage.class)); + } + + @Test + public void folder_does_not_start_open() throws MessagingException { + assertFalse(folder.isOpen()); + } + + @Test + public void open_should_open_folder() throws MessagingException { + folder.open(); + assertTrue(folder.isOpen()); + } + + @Test + public void close_should_close_folder() throws MessagingException { + folder.close(); + assertFalse(folder.isOpen()); + } + + @Test + public void can_fetch_message_count() throws Exception { + int messageCount = 23; + HashMap<String, String> headers = new HashMap<>(); + headers.put("Brief", "t"); + String messageCountXml = "<xml>MessageCountXml</xml>"; + when(mockStore.getMessageCountXml("True")).thenReturn(messageCountXml); + when(mockStore.processRequest("https://localhost/webDavStoreUrl/testFolder", + "SEARCH", messageCountXml, headers)).thenReturn(mockDataSet); + when(mockDataSet.getMessageCount()).thenReturn(messageCount); + + int result = folder.getMessageCount(); + + assertEquals(messageCount, result); + } + + @Test + public void can_fetch_unread_message_count() throws Exception { + int unreadMessageCount = 13; + HashMap<String, String> headers = new HashMap<>(); + headers.put("Brief", "t"); + String messageCountXml = "<xml>MessageCountXml</xml>"; + when(mockStore.getMessageCountXml("False")).thenReturn(messageCountXml); + when(mockStore.processRequest("https://localhost/webDavStoreUrl/testFolder", + "SEARCH", messageCountXml, headers)).thenReturn(mockDataSet); + when(mockDataSet.getMessageCount()).thenReturn(unreadMessageCount); + + int result = folder.getUnreadMessageCount(); + + assertEquals(unreadMessageCount, result); + } + + @Test + public void getMessages_should_request_message_search() throws MessagingException { + int totalMessages = 23; + int messageStart = 1; + int messageEnd = 11; + setupFolderWithMessages(totalMessages); + String messagesXml = "<xml>MessagesXml</xml>"; + buildSearchResponse(mockDataSet); + when(mockStore.getMessagesXml()).thenReturn(messagesXml); + when(mockStore.processRequest(eq("https://localhost/webDavStoreUrl/testFolder"), eq("SEARCH"), + eq(messagesXml), any())).thenReturn(mockDataSet); + + folder.getMessages(messageStart, messageEnd, listener); + + verify(listener, times(5)).messageFinished(any(WebDavMessage.class)); + } + + @Test + public void getMessages_shouldProvideCorrectHeadersInRequest() throws MessagingException { + int totalMessages = 23; + int messageStart = 1; + int messageEnd = 11; + setupFolderWithMessages(totalMessages); + String messagesXml = "<xml>MessagesXml</xml>"; + buildSearchResponse(mockDataSet); + when(mockStore.getMessagesXml()).thenReturn(messagesXml); + when(mockStore.processRequest(eq("https://localhost/webDavStoreUrl/testFolder"), eq("SEARCH"), + eq(messagesXml), any())).thenReturn(mockDataSet); + + folder.getMessages(messageStart, messageEnd, listener); + + verify(mockStore, times(2)).processRequest(anyString(), anyString(), anyString(), + headerCaptor.capture()); + assertEquals(2, headerCaptor.getValue().size()); + assertEquals("t", headerCaptor.getValue().get("Brief")); + assertEquals("rows=" + (totalMessages - (messageEnd)) + "-" + (totalMessages - messageStart) + , headerCaptor.getValue().get("Range")); + } + + private void buildSearchResponse(DataSet mockDataSet) { + String[] uids = new String[]{"uid1", "uid2", "uid3", "uid4", "uid5"}; + HashMap<String, String> uidToUrls = new HashMap<>(); + uidToUrls.put("uid1", "url1"); + uidToUrls.put("uid2", "url2"); + uidToUrls.put("uid3", "url3"); + uidToUrls.put("uid4", "url4"); + uidToUrls.put("uid5", "url5"); + + when(mockDataSet.getUids()).thenReturn(uids); + when(mockDataSet.getUidToUrl()).thenReturn(uidToUrls); + } + + @Test(expected = MessagingException.class) + public void getMessages_should_throw_message_exception_if_requesting_messages_from_empty_folder() + throws MessagingException { + folder.getMessages(0, 10, listener); + } + + private void setupMoveOrCopy() throws MessagingException { + destinationFolder = setupDestinationFolder(); + String uid = "uid1"; + String url = "url1"; + messages = singletonList(createWebDavMessage(uid)); + setupGetUrlsRequestResponse(uid, url); + when(mockStore.getMoveOrCopyMessagesReadXml(eq(new String[]{url}), anyBoolean())).thenReturn(moveOrCopyXml); + moveOrCopyHeaders = new HashMap<>(); + moveOrCopyHeaders.put("Destination", "https://localhost/webDavStoreUrl/destFolder"); + moveOrCopyHeaders.put("Brief", "t"); + moveOrCopyHeaders.put("If-Match", "*"); + } + + @Test + public void moveMessages_should_requestMoveXml() throws Exception { + setupMoveOrCopy(); + + folder.moveMessages(messages, destinationFolder); + + verify(mockStore).getMoveOrCopyMessagesReadXml(eq(new String[]{"url1"}), + eq(true)); + } + + @Test + public void moveMessages_should_send_move_command() throws Exception { + setupMoveOrCopy(); + + folder.moveMessages(messages, destinationFolder); + + verify(mockStore).processRequest("https://localhost/webDavStoreUrl/testFolder", "BMOVE", + moveOrCopyXml, moveOrCopyHeaders, false); + } + + @Test + public void copyMessages_should_requestCopyXml() throws Exception { + setupMoveOrCopy(); + + folder.copyMessages(messages, destinationFolder); + + verify(mockStore).getMoveOrCopyMessagesReadXml(eq(new String[]{"url1"}), + eq(false)); + } + + @Test + public void copyMessages_should_send_copy_command() throws Exception { + setupMoveOrCopy(); + + folder.copyMessages(messages, destinationFolder); + + verify(mockStore).processRequest("https://localhost/webDavStoreUrl/testFolder", "BCOPY", + moveOrCopyXml, moveOrCopyHeaders, false); + } + + @Test + public void appendWebDavMessages_replaces_messages_with_WebDAV_versions() throws MessagingException, IOException { + List<Message> existingMessages = new ArrayList<>(); + Message existingMessage = mock(Message.class); + existingMessages.add(existingMessage); + String messageUid = "testMessageUid"; + when(existingMessage.getUid()).thenReturn(messageUid); + + List<WebDavMessage> response = folder.appendWebDavMessages(existingMessages); + + assertEquals(1, response.size(), 1); + assertEquals(WebDavMessage.class, response.get(0).getClass()); + assertEquals(messageUid, response.get(0).getUid()); + } + + @Test + public void appendWebDavMessages_sendsRequestUsingStore() throws MessagingException, IOException { + List<Message> existingMessages = new ArrayList<>(); + Message existingMessage = mock(Message.class); + existingMessages.add(existingMessage); + String messageUid = "testMessageUid"; + when(existingMessage.getUid()).thenReturn(messageUid); + + folder.appendWebDavMessages(existingMessages); + + verify(mockStore).sendRequest(urlCaptor.capture(), eq("PUT"), entityCaptor.capture(), + isNull(), eq(true)); + assertTrue(urlCaptor.getValue().startsWith(storeUrl + "/" + folderName + "/" + messageUid)); + assertTrue(urlCaptor.getValue().endsWith(".eml")); + } +} diff --git a/mail/protocols/webdav/src/test/java/com/fsck/k9/mail/store/webdav/WebDavMessageTest.java b/mail/protocols/webdav/src/test/java/com/fsck/k9/mail/store/webdav/WebDavMessageTest.java new file mode 100644 index 0000000..e4453e8 --- /dev/null +++ b/mail/protocols/webdav/src/test/java/com/fsck/k9/mail/store/webdav/WebDavMessageTest.java @@ -0,0 +1,51 @@ +package com.fsck.k9.mail.store.webdav; + + +import com.fsck.k9.mail.MessagingException; + +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.when; + +public class WebDavMessageTest { + + private WebDavMessage message; + @Mock + private WebDavFolder mockFolder; + + @Before + public void before() { + MockitoAnnotations.initMocks(this); + when(mockFolder.getServerId()).thenReturn("Inbox"); + when(mockFolder.getUrl()).thenReturn("http://example.org/Inbox"); + message = new WebDavMessage("message1", mockFolder); + } + + @Test + public void setUrl_tests() throws MessagingException { + message.setUrl("message.eml"); + assertEquals("http://example.org/Inbox/message.eml", message.getUrl()); + message.setUrl("mes sage.eml"); + assertEquals("http://example.org/Inbox/mes%20sage.eml", message.getUrl()); + message.setUrl("/message.eml"); + assertEquals("http://example.org/Inbox/message.eml", message.getUrl()); + message.setUrl("http://example.com/Inbox/message.eml"); + assertEquals("http://example.com/Inbox/message.eml", message.getUrl()); + message.setUrl("mes%20sage.eml"); + assertEquals("http://example.org/Inbox/mes%20sage.eml", message.getUrl()); + message.setUrl("sub%20folder/mes%20sage.eml"); + assertEquals("http://example.org/Inbox/sub%20folder/mes%20sage.eml", message.getUrl()); + } + + @Test + public void setNewHeaders_updates_size() throws MessagingException { + ParsedMessageEnvelope parsedMessageEnvelope = new ParsedMessageEnvelope(); + parsedMessageEnvelope.addHeader("getcontentlength", "1024"); + message.setNewHeaders(parsedMessageEnvelope); + assertEquals(1024, message.getSize()); + } +} diff --git a/mail/protocols/webdav/src/test/java/com/fsck/k9/mail/store/webdav/WebDavStoreTest.java b/mail/protocols/webdav/src/test/java/com/fsck/k9/mail/store/webdav/WebDavStoreTest.java new file mode 100644 index 0000000..0c1b464 --- /dev/null +++ b/mail/protocols/webdav/src/test/java/com/fsck/k9/mail/store/webdav/WebDavStoreTest.java @@ -0,0 +1,376 @@ +package com.fsck.k9.mail.store.webdav; + + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import com.fsck.k9.mail.AuthType; +import com.fsck.k9.mail.CertificateValidationException; +import com.fsck.k9.mail.ConnectionSecurity; +import com.fsck.k9.mail.FolderType; +import com.fsck.k9.mail.MessagingException; +import com.fsck.k9.mail.ServerSettings; +import com.fsck.k9.mail.filter.Base64; +import com.fsck.k9.mail.ssl.TrustManagerFactory; +import javax.net.ssl.SSLException; +import org.apache.http.HttpEntity; +import org.apache.http.HttpResponse; +import org.apache.http.HttpVersion; +import org.apache.http.client.protocol.ClientContext; +import org.apache.http.conn.ClientConnectionManager; +import org.apache.http.conn.scheme.Scheme; +import org.apache.http.conn.scheme.SchemeRegistry; +import org.apache.http.entity.BasicHttpEntity; +import org.apache.http.entity.StringEntity; +import org.apache.http.impl.client.BasicCookieStore; +import org.apache.http.impl.cookie.BasicClientCookie; +import org.apache.http.message.BasicHttpResponse; +import org.apache.http.message.BasicStatusLine; +import org.apache.http.params.BasicHttpParams; +import org.apache.http.params.HttpParams; +import org.apache.http.protocol.HttpContext; +import org.junit.Before; +import org.junit.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; +import org.mockito.stubbing.OngoingStubbing; + +import static junit.framework.Assert.assertSame; +import static org.junit.Assert.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + + +public class WebDavStoreTest { + private static final HttpResponse OK_200_RESPONSE = createOkResponse(); + private static final HttpResponse UNAUTHORIZED_401_RESPONSE = createResponse(401); + private static final HttpResponse SERVER_ERROR_500_RESPONSE = createResponse(500); + + + @Mock + private WebDavHttpClient.WebDavHttpClientFactory mockHttpClientFactory; + @Mock + private WebDavHttpClient mockHttpClient; + @Mock + private ClientConnectionManager mockClientConnectionManager; + @Mock + private SchemeRegistry mockSchemeRegistry; + @Mock + private TrustManagerFactory trustManagerFactory; + @Mock + private SniHostSetter sniHostSetter; + @Mock + private DraftsFolderProvider draftsFolderProvider; + + private ArgumentCaptor<HttpGeneric> requestCaptor; + + private ServerSettings serverSettings; + private WebDavStore webDavStore; + + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + + HttpParams httpParams = new BasicHttpParams(); + when(mockHttpClientFactory.create()).thenReturn(mockHttpClient); + when(mockHttpClient.getParams()).thenReturn(httpParams); + when(mockHttpClient.getConnectionManager()).thenReturn(mockClientConnectionManager); + when(mockClientConnectionManager.getSchemeRegistry()).thenReturn(mockSchemeRegistry); + + serverSettings = createServerSettings(ConnectionSecurity.SSL_TLS_REQUIRED); + webDavStore = createWebDavStore(); + } + + @Test + public void checkSettings_withWebDavUri_shouldUseInsecureConnection() throws Exception { + WebDavStore webDavStore = createWebDavStore(ConnectionSecurity.NONE); + configureHttpResponses(UNAUTHORIZED_401_RESPONSE, OK_200_RESPONSE); + + webDavStore.checkSettings(); + + assertHttpClientUsesHttps(false); + } + + @Test + public void checkSettings_withWebDavSslUri_shouldUseSecureConnection() throws Exception { + WebDavStore webDavStore = createWebDavStore(ConnectionSecurity.SSL_TLS_REQUIRED); + configureHttpResponses(UNAUTHORIZED_401_RESPONSE, OK_200_RESPONSE); + + webDavStore.checkSettings(); + + assertHttpClientUsesHttps(true); + } + + @Test + public void checkSettings_withWebDavTlsUri_shouldUseSecureConnection() throws Exception { + WebDavStore webDavStore = createWebDavStore(ConnectionSecurity.STARTTLS_REQUIRED); + configureHttpResponses(UNAUTHORIZED_401_RESPONSE, OK_200_RESPONSE); + + webDavStore.checkSettings(); + + assertHttpClientUsesHttps(true); + } + + @Test + public void checkSettings_withOkResponse_shouldPerformFormBasedAuthentication() throws Exception { + ArgumentCaptor<HttpGeneric> requestCaptor = ArgumentCaptor.forClass(HttpGeneric.class); + when(mockHttpClient.executeOverride(requestCaptor.capture(), any(HttpContext.class))) + .thenReturn(OK_200_RESPONSE) + .thenReturn(createOkResponseWithForm()) + .thenAnswer(createOkResponseWithCookie()) + .thenReturn(OK_200_RESPONSE); + + webDavStore.checkSettings(); + + List<HttpGeneric> requests = requestCaptor.getAllValues(); + assertEquals(4, requests.size()); + assertEquals("GET", requests.get(0).getMethod()); // Checking auth type + assertEquals("POST", requests.get(1).getMethod()); // Posting form data + assertEquals("https://webdav.example.org:443/exchweb/bin/auth/owaauth.dll", requests.get(1).getURI().toString()); + assertEquals("POST", requests.get(2).getMethod()); // Confirming login + assertEquals("https://webdav.example.org:443/exchweb/bin/auth/owaauth.dll", requests.get(2).getURI().toString()); + assertEquals("GET", requests.get(3).getMethod()); // Getting response + } + + @Test + public void checkSettings_withInitialUnauthorizedResponse_shouldPerformBasicAuthentication() throws Exception { + configureHttpResponses(UNAUTHORIZED_401_RESPONSE, OK_200_RESPONSE); + + webDavStore.checkSettings(); + + List<HttpGeneric> requests = requestCaptor.getAllValues(); + assertEquals(2, requests.size()); + assertEquals("GET", requests.get(0).getMethod()); + assertEquals("GET", requests.get(1).getMethod()); + assertEquals("Basic " + Base64.encode("user:password"), + requests.get(1).getHeaders("Authorization")[0].getValue()); + } + + @Test(expected = MessagingException.class) + public void checkSettings_withUnauthorizedResponses_shouldThrow() throws Exception { + configureHttpResponses(UNAUTHORIZED_401_RESPONSE, UNAUTHORIZED_401_RESPONSE); + + webDavStore.checkSettings(); + } + + @Test(expected = MessagingException.class) + public void checkSettings_withErrorResponse_shouldThrow() throws Exception { + configureHttpResponses(UNAUTHORIZED_401_RESPONSE, SERVER_ERROR_500_RESPONSE); + + webDavStore.checkSettings(); + } + + @Test(expected = CertificateValidationException.class) + public void checkSettings_withSslException_shouldThrowCertificateValidationException() throws Exception { + ArgumentCaptor<HttpGeneric> requestCaptor = ArgumentCaptor.forClass(HttpGeneric.class); + when(mockHttpClient.executeOverride(requestCaptor.capture(), any(HttpContext.class))) + .thenThrow(new SSLException("Test")); + + webDavStore.checkSettings(); + } + + //TODO: Is this really something we want to test? + @Test + public void checkSettings_shouldRegisterHttpsSchemeWithRegistry() throws Exception { + configureHttpResponses(UNAUTHORIZED_401_RESPONSE, OK_200_RESPONSE); + + webDavStore.checkSettings(); + + ArgumentCaptor<Scheme> schemeCaptor = ArgumentCaptor.forClass(Scheme.class); + verify(mockSchemeRegistry).register(schemeCaptor.capture()); + assertEquals("https", schemeCaptor.getValue().getName()); + assertEquals(WebDavSocketFactory.class, schemeCaptor.getValue().getSocketFactory().getClass()); + } + + @Test + public void getFolder_shouldReturnWebDavFolderInstance() { + WebDavFolder result = webDavStore.getFolder("INBOX"); + + assertEquals(WebDavFolder.class, result.getClass()); + } + + @Test + public void getFolder_calledTwice_shouldReturnFirstInstance() { + String folderName = "Trash"; + WebDavFolder webDavFolder = webDavStore.getFolder(folderName); + + WebDavFolder result = webDavStore.getFolder(folderName); + + assertSame(webDavFolder, result); + } + + @Test + public void getPersonalNamespaces_shouldRequestSpecialFolders() throws Exception { + configureHttpResponses(UNAUTHORIZED_401_RESPONSE, OK_200_RESPONSE, createOkPropfindResponse(), + createOkSearchResponse()); + + webDavStore.getPersonalNamespaces(); + + List<HttpGeneric> requests = requestCaptor.getAllValues(); + assertEquals(4, requests.size()); // AUTH + 2 + assertEquals("PROPFIND", requests.get(2).getMethod()); //Special Folders + } + + @Test + public void getPersonalNamespaces_shouldSetSpecialFolderNames() throws Exception { + configureHttpResponses(UNAUTHORIZED_401_RESPONSE, OK_200_RESPONSE, createOkPropfindResponse(), + createOkSearchResponse()); + + List<WebDavFolder> folders = webDavStore.getPersonalNamespaces(); + + Map<String, FolderType> folderNameToTypeMap = new HashMap<>(); + for (WebDavFolder folder : folders) { + folderNameToTypeMap.put(folder.getName(), folder.getType()); + } + assertEquals(FolderType.INBOX, folderNameToTypeMap.get("Inbox")); + assertEquals(FolderType.REGULAR, folderNameToTypeMap.get("Drafts")); + assertEquals(FolderType.REGULAR, folderNameToTypeMap.get("Folder2")); + } + + @Test + public void getPersonalNamespaces_shouldRequestFolderList() throws Exception { + configureHttpResponses(UNAUTHORIZED_401_RESPONSE, OK_200_RESPONSE, createOkPropfindResponse(), + createOkSearchResponse()); + + webDavStore.getPersonalNamespaces(); + + List<HttpGeneric> requests = requestCaptor.getAllValues(); + assertEquals(4, requests.size()); // AUTH + SPECIALFOLDERS + 1 + assertEquals("SEARCH", requests.get(3).getMethod()); + } + + @Test + public void getPersonalNamespaces_shouldProvideListOfAllFoldersSentFromResponses() throws Exception { + configureHttpResponses(UNAUTHORIZED_401_RESPONSE, OK_200_RESPONSE, createOkPropfindResponse(), + createOkSearchResponse()); + + List<WebDavFolder> folders = webDavStore.getPersonalNamespaces(); + + List<HttpGeneric> requests = requestCaptor.getAllValues(); + + assertEquals(3, folders.size()); + } + + private static BasicHttpResponse createResponse(int statusCode) { + return new BasicHttpResponse(new BasicStatusLine(HttpVersion.HTTP_1_1, statusCode, null)); + } + + private static BasicHttpResponse createOkResponse() { + return createResponse(200); + } + + //TODO: Replace XML with actual XML from an Exchange server + private BasicHttpResponse createOkSearchResponse() throws UnsupportedEncodingException { + BasicHttpResponse okSearchResponse = createOkResponse(); + HttpEntity searchResponseEntity = new StringEntity("" + + "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" + + " <D:multistatus xmlns:D=\"DAV:\"\n" + + " xmlns:R=\"http://example.org/propschema\">\n" + + " <D:response>" + + " <D:propstat>\n" + + " <uid>Inbox</uid>" + + " <href>http://example.org/Exchange/user/Inbox</href>\n" + + " </D:propstat></D:response>\n" + + " <D:response>" + + " <D:propstat>\n" + + " <uid>Drafts</uid>" + + " <href>http://example.org/Exchange/user/Drafts</href>\n" + + " </D:propstat></D:response>\n" + + " <D:response>" + + " <D:propstat>\n" + + " <uid>Folder2</uid>" + + " <href>http://example.org/Exchange/user/Folder2</href>\n" + + " </D:propstat></D:response>\n" + + " </D:multistatus>"); + okSearchResponse.setEntity(searchResponseEntity); + + return okSearchResponse; + } + + //TODO: Replace XML with actual XML from an Exchange server + private BasicHttpResponse createOkPropfindResponse() throws UnsupportedEncodingException { + BasicHttpResponse okPropfindResponse = createOkResponse(); + HttpEntity propfindResponseEntity = new StringEntity("" + + "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" + + "<D:multistatus xmlns:D=\"DAV:\" xmlns:e=\"urn:schemas:httpmail:\">\n" + + " <D:response><e:inbox>http://example.org/Exchange/user/Inbox</e:inbox></D:response>\n" + + "</D:multistatus>"); + okPropfindResponse.setEntity(propfindResponseEntity); + + return okPropfindResponse; + } + + private BasicHttpResponse createOkResponseWithForm() { + BasicHttpResponse okayResponseWithForm = createOkResponse(); + BasicHttpEntity okayResponseWithFormEntity = new BasicHttpEntity(); + String form = "<form action=\"owaauth.dll\"></form>"; + okayResponseWithFormEntity.setContent(new ByteArrayInputStream(form.getBytes())); + okayResponseWithForm.setEntity(okayResponseWithFormEntity); + return okayResponseWithForm; + } + + private Answer<HttpResponse> createOkResponseWithCookie() { + return new Answer<HttpResponse>() { + @Override + public HttpResponse answer(InvocationOnMock invocation) { + HttpContext context = (HttpContext) invocation.getArguments()[1]; + if (context.getAttribute(ClientContext.COOKIE_STORE) != null) { + BasicCookieStore cookieStore = + (BasicCookieStore) context.getAttribute(ClientContext.COOKIE_STORE); + BasicClientCookie cookie = new BasicClientCookie("cookie", "meLikeCookie"); + cookieStore.addCookie(cookie); + } + + return OK_200_RESPONSE; + } + }; + } + + private ServerSettings createServerSettings(ConnectionSecurity connectionSecurity) { + Map<String, String> extra = WebDavStoreSettings.createExtra(null, null, null, null); + return new ServerSettings( + "webdav", + "webdav.example.org", + 443, + connectionSecurity, + AuthType.PLAIN, + "user", + "password", + null, + extra); + } + + private WebDavStore createWebDavStore() { + return new WebDavStore(trustManagerFactory, sniHostSetter, serverSettings, draftsFolderProvider, + mockHttpClientFactory); + } + + private WebDavStore createWebDavStore(ConnectionSecurity connectionSecurity) { + ServerSettings serverSettings = createServerSettings(connectionSecurity); + return new WebDavStore(trustManagerFactory, sniHostSetter, serverSettings, draftsFolderProvider, + mockHttpClientFactory); + } + + private void configureHttpResponses(HttpResponse... responses) throws IOException { + requestCaptor = ArgumentCaptor.forClass(HttpGeneric.class); + OngoingStubbing<HttpResponse> stubbing = + when(mockHttpClient.executeOverride(requestCaptor.capture(), any(HttpContext.class))); + + for (HttpResponse response : responses) { + stubbing = stubbing.thenReturn(response); + } + } + + private void assertHttpClientUsesHttps(boolean expected) { + assertEquals(expected, requestCaptor.getValue().getURI().getScheme().startsWith("https")); + } +} |
