Merge pull request #6805 from thundernest/convert_to_kotlin

Convert `ImapResponseParserTest` to Kotlin
This commit is contained in:
cketti 2023-04-11 14:58:00 +02:00 committed by GitHub
commit 87ff2338ba
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 585 additions and 563 deletions

View file

@ -1,563 +0,0 @@
package com.fsck.k9.mail.store.imap;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import com.fsck.k9.mail.filter.FixedLengthInputStream;
import com.fsck.k9.mail.filter.PeekableInputStream;
import kotlin.text.Charsets;
import org.junit.Test;
import static java.util.Arrays.asList;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
public class ImapResponseParserTest {
private PeekableInputStream peekableInputStream;
@Test
public void testSimpleOkResponse() throws IOException {
ImapResponseParser parser = createParser("* OK\r\n");
ImapResponse response = parser.readResponse();
assertNotNull(response);
assertEquals(1, response.size());
assertEquals("OK", response.get(0));
}
@Test
public void testOkResponseWithText() throws IOException {
ImapResponseParser parser = createParser("* OK Some text here\r\n");
ImapResponse response = parser.readResponse();
assertNotNull(response);
assertEquals(2, response.size());
assertEquals("OK", response.get(0));
assertEquals("Some text here", response.get(1));
}
@Test
public void testOkResponseWithRespTextCode() throws IOException {
ImapResponseParser parser = createParser("* OK [UIDVALIDITY 3857529045]\r\n");
ImapResponse response = parser.readResponse();
assertNotNull(response);
assertEquals(2, response.size());
assertEquals("OK", response.get(0));
assertTrue(response.get(1) instanceof ImapList);
ImapList respTextCode = (ImapList) response.get(1);
assertEquals(2, respTextCode.size());
assertEquals("UIDVALIDITY", respTextCode.get(0));
assertEquals("3857529045", respTextCode.get(1));
}
@Test
public void testOkResponseWithRespTextCodeAndText() throws IOException {
ImapResponseParser parser = createParser("* OK [token1 token2] {x} test [...]\r\n");
ImapResponse response = parser.readResponse();
assertNotNull(response);
assertEquals(3, response.size());
assertEquals("OK", response.get(0));
assertTrue(response.get(1) instanceof ImapList);
assertEquals("{x} test [...]", response.get(2));
ImapList respTextCode = (ImapList) response.get(1);
assertEquals(2, respTextCode.size());
assertEquals("token1", respTextCode.get(0));
assertEquals("token2", respTextCode.get(1));
}
@Test
public void testReadStatusResponseWithOKResponse() throws Exception {
ImapResponseParser parser = createParser("* COMMAND BAR\tBAZ\r\n" +
"TAG OK COMMAND completed\r\n");
List<ImapResponse> responses = parser.readStatusResponse("TAG", null, null, null);
assertEquals(2, responses.size());
assertEquals(asList("COMMAND", "BAR", "BAZ"), responses.get(0));
assertEquals(asList("OK", "COMMAND completed"), responses.get(1));
}
@Test
public void testReadStatusResponseUntaggedHandlerGetsUntaggedOnly() throws Exception {
ImapResponseParser parser = createParser(
"* UNTAGGED\r\n" +
"A2 OK COMMAND completed\r\n");
TestUntaggedHandler untaggedHandler = new TestUntaggedHandler();
parser.readStatusResponse("A2", null, null, untaggedHandler);
assertEquals(1, untaggedHandler.responses.size());
assertEquals(asList("UNTAGGED"), untaggedHandler.responses.get(0));
}
@Test
public void testReadStatusResponseSkippingWrongTag() throws Exception {
ImapResponseParser parser = createParser("* UNTAGGED\r\n" +
"* 0 EXPUNGE\r\n" +
"* 42 EXISTS\r\n" +
"A1 COMMAND BAR BAZ\r\n" +
"A2 OK COMMAND completed\r\n");
TestUntaggedHandler untaggedHandler = new TestUntaggedHandler();
List<ImapResponse> responses = parser.readStatusResponse("A2", null, null, untaggedHandler);
assertEquals(3, responses.size());
assertEquals(asList("0", "EXPUNGE"), responses.get(0));
assertEquals(asList("42", "EXISTS"), responses.get(1));
assertEquals(asList("OK", "COMMAND completed"), responses.get(2));
assertEquals(asList("UNTAGGED"), untaggedHandler.responses.get(0));
assertEquals(responses.get(0), untaggedHandler.responses.get(1));
assertEquals(responses.get(1), untaggedHandler.responses.get(2));
}
@Test
public void testReadStatusResponseUntaggedHandlerStillCalledOnNegativeReply() throws Exception {
ImapResponseParser parser = createParser(
"+ text\r\n" +
"A2 NO Bad response\r\n");
TestUntaggedHandler untaggedHandler = new TestUntaggedHandler();
try {
List<ImapResponse> responses = parser.readStatusResponse("A2", null, null, untaggedHandler);
} catch (NegativeImapResponseException e) {
}
assertEquals(1, untaggedHandler.responses.size());
assertEquals(asList("text"), untaggedHandler.responses.get(0));
}
@Test(expected = NegativeImapResponseException.class)
public void testReadStatusResponseWithErrorResponse() throws Exception {
ImapResponseParser parser = createParser("* COMMAND BAR BAZ\r\nTAG ERROR COMMAND errored\r\n");
parser.readStatusResponse("TAG", null, null, null);
}
@Test
public void testRespTextCodeWithList() throws Exception {
ImapResponseParser parser = createParser("* OK [PERMANENTFLAGS (\\Answered \\Flagged \\Deleted \\Seen " +
"\\Draft NonJunk $MDNSent \\*)] Flags permitted.\r\n");
ImapResponse response = parser.readResponse();
assertEquals(3, response.size());
assertTrue(response.get(1) instanceof ImapList);
assertEquals(2, response.getList(1).size());
assertEquals("PERMANENTFLAGS", response.getList(1).getString(0));
assertTrue(response.getList(1).get(1) instanceof ImapList);
assertEquals("\\Answered", response.getList(1).getList(1).getString(0));
assertEquals("\\Flagged", response.getList(1).getList(1).getString(1));
assertEquals("\\Deleted", response.getList(1).getList(1).getString(2));
assertEquals("\\Seen", response.getList(1).getList(1).getString(3));
assertEquals("\\Draft", response.getList(1).getList(1).getString(4));
assertEquals("NonJunk", response.getList(1).getList(1).getString(5));
assertEquals("$MDNSent", response.getList(1).getList(1).getString(6));
assertEquals("\\*", response.getList(1).getList(1).getString(7));
}
@Test
public void testExistsResponse() throws Exception {
ImapResponseParser parser = createParser("* 23 EXISTS\r\n");
ImapResponse response = parser.readResponse();
assertEquals(2, response.size());
assertEquals(23, response.getNumber(0));
assertEquals("EXISTS", response.getString(1));
}
@Test(expected = IOException.class)
public void testReadStringUntilEndOfStream() throws IOException {
ImapResponseParser parser = createParser("* OK Some text ");
parser.readResponse();
}
@Test
public void testCommandContinuation() throws Exception {
ImapResponseParser parser = createParser("+ Ready for additional command text\r\n");
ImapResponse response = parser.readResponse();
assertEquals(1, response.size());
assertEquals("Ready for additional command text", response.getString(0));
}
@Test
public void testParseLiteral() throws Exception {
ImapResponseParser parser = createParser("* {4}\r\ntest\r\n");
ImapResponse response = parser.readResponse();
assertEquals(1, response.size());
assertEquals("test", response.getString(0));
}
@Test
public void testParseLiteralWithEmptyString() throws Exception {
ImapResponseParser parser = createParser("* {0}\r\n\r\n");
ImapResponse response = parser.readResponse();
assertEquals(1, response.size());
assertEquals("", response.getString(0));
}
@Test(expected = IOException.class)
public void testParseLiteralToEndOfStream() throws Exception {
ImapResponseParser parser = createParser("* {4}\r\nabc");
parser.readResponse();
}
@Test
public void testParseLiteralWithConsumingCallbackReturningNull() throws Exception {
ImapResponseParser parser = createParser("* {4}\r\ntest\r\n");
TestImapResponseCallback callback = TestImapResponseCallback.readBytesAndReturn(4, "cheeseburger");
ImapResponse response = parser.readResponse(callback);
assertEquals(1, response.size());
assertEquals("cheeseburger", response.getString(0));
}
@Test
public void testParseLiteralWithNonConsumingCallbackReturningNull() throws Exception {
ImapResponseParser parser = createParser("* {4}\r\ntest\r\n");
TestImapResponseCallback callback = TestImapResponseCallback.readBytesAndReturn(0, null);
ImapResponse response = parser.readResponse(callback);
assertEquals(1, response.size());
assertEquals("test", response.getString(0));
assertTrue(callback.foundLiteralCalled);
assertAllInputConsumed();
}
@Test
public void readResponse_withPartlyConsumingCallbackReturningNull_shouldThrow() throws Exception {
ImapResponseParser parser = createParser("* {4}\r\ntest\r\n");
TestImapResponseCallback callback = TestImapResponseCallback.readBytesAndReturn(2, null);
try {
parser.readResponse(callback);
fail();
} catch (AssertionError e) {
assertEquals("Callback consumed some data but returned no result", e.getMessage());
}
}
@Test
public void readResponse_withPartlyConsumingCallbackThatThrows_shouldReadAllDataAndThrow() throws Exception {
ImapResponseParser parser = createParser("* {4}\r\ntest\r\n");
TestImapResponseCallback callback = TestImapResponseCallback.readBytesAndThrow(2);
try {
parser.readResponse(callback);
fail();
} catch (ImapResponseParserException e) {
assertEquals("readResponse(): Exception in callback method", e.getMessage());
assertEquals(ImapResponseParserTestException.class, e.getCause().getClass());
}
assertAllInputConsumed();
}
@Test
public void readResponse_withCallbackThatThrowsRepeatedly_shouldConsumeAllInputAndThrowFirstException()
throws Exception {
ImapResponseParser parser = createParser("* {3}\r\none {3}\r\ntwo\r\n");
TestImapResponseCallback callback = TestImapResponseCallback.readBytesAndThrow(3);
try {
parser.readResponse(callback);
fail();
} catch (ImapResponseParserException e) {
assertEquals("readResponse(): Exception in callback method", e.getMessage());
assertEquals(ImapResponseParserTestException.class, e.getCause().getClass());
assertEquals(0, ((ImapResponseParserTestException) e.getCause()).instanceNumber);
}
assertAllInputConsumed();
}
@Test
public void testParseLiteralWithIncompleteConsumingCallbackReturningString() throws Exception {
ImapResponseParser parser = createParser("* {4}\r\ntest\r\n");
TestImapResponseCallback callback = TestImapResponseCallback.readBytesAndReturn(2, "ninja");
ImapResponse response = parser.readResponse(callback);
assertEquals(1, response.size());
assertEquals("ninja", response.getString(0));
assertAllInputConsumed();
}
@Test
public void testParseLiteralWithThrowingCallback() throws Exception {
ImapResponseParser parser = createParser("* {4}\r\ntest\r\n");
ImapResponseCallback callback = TestImapResponseCallback.readBytesAndThrow(0);
try {
parser.readResponse(callback);
fail();
} catch (ImapResponseParserException e) {
assertEquals("readResponse(): Exception in callback method", e.getMessage());
}
assertAllInputConsumed();
}
@Test(expected = IOException.class)
public void testParseLiteralWithCallbackThrowingIOException() throws Exception {
ImapResponseParser parser = createParser("* {4}\r\ntest\r\n");
ImapResponseCallback callback = new ImapResponseCallback() {
@Override
public Object foundLiteral(ImapResponse response, FixedLengthInputStream literal) throws Exception {
throw new IOException();
}
};
parser.readResponse(callback);
}
@Test
public void testParseQuoted() throws Exception {
ImapResponseParser parser = createParser("* \"qu\\\"oted\"\r\n");
ImapResponse response = parser.readResponse();
assertEquals(1, response.size());
assertEquals("qu\"oted", response.getString(0));
}
@Test
public void utf8InQuotedString() throws Exception {
ImapResponseParser parser = createParser("* \"quöted\"\r\n");
ImapResponse response = parser.readResponse();
assertEquals(1, response.size());
assertEquals("quöted", response.getString(0));
}
@Test(expected = IOException.class)
public void testParseQuotedToEndOfStream() throws Exception {
ImapResponseParser parser = createParser("* \"abc");
parser.readResponse();
}
@Test(expected = IOException.class)
public void testParseAtomToEndOfStream() throws Exception {
ImapResponseParser parser = createParser("* abc");
parser.readResponse();
}
@Test(expected = IOException.class)
public void testParseUntaggedResponseWithoutSpace() throws Exception {
ImapResponseParser parser = createParser("*\r\n");
parser.readResponse();
}
@Test
public void testListResponseContainingFolderNameWithBrackets() throws Exception {
ImapResponseParser parser = createParser("* LIST (\\HasNoChildren) \".\" [FolderName]\r\n");
ImapResponse response = parser.readResponse();
assertEquals(4, response.size());
assertEquals("LIST", response.get(0));
assertEquals(1, response.getList(1).size());
assertEquals("\\HasNoChildren", response.getList(1).getString(0));
assertEquals(".", response.get(2));
assertEquals("[FolderName]", response.get(3));
}
@Test(expected = IOException.class)
public void testListResponseContainingFolderNameContainingBracketsThrowsException() throws Exception {
ImapResponseParser parser = createParser(
"* LIST (\\NoInferiors) \"/\" Root/Folder/Subfolder()\r\n");
parser.readResponse();
}
@Test
public void readResponseShouldReadWholeListResponseLine() throws Exception {
ImapResponseParser parser = createParser("* LIST (\\HasNoChildren) \".\" [FolderName]\r\n" +
"TAG OK [List complete]\r\n");
parser.readResponse();
ImapResponse responseTwo = parser.readResponse();
assertEquals("TAG", responseTwo.getTag());
}
@Test
public void readResponse_withListResponseContainingNil() throws Exception {
ImapResponseParser parser = createParser("* LIST (\\NoInferiors) NIL INBOX\r\n");
ImapResponse response = parser.readResponse();
assertEquals(4, response.size());
assertEquals("LIST", response.get(0));
assertEquals(1, response.getList(1).size());
assertEquals("\\NoInferiors", response.getList(1).getString(0));
assertEquals(null, response.get(2));
assertEquals("INBOX", response.get(3));
}
@Test
public void readResponse_withListAsFirstToken_shouldThrow() throws Exception {
ImapResponseParser parser = createParser("* [1 2] 3\r\n");
try {
parser.readResponse();
fail("Expected exception");
} catch (IOException e) {
assertEquals("Unexpected non-string token: ImapList - [1, 2]", e.getMessage());
}
}
@Test
public void testFetchResponse() throws Exception {
ImapResponseParser parser = createParser("* 1 FETCH (" +
"UID 23 " +
"INTERNALDATE \"01-Jul-2015 12:34:56 +0200\" " +
"RFC822.SIZE 3456 " +
"BODY[HEADER.FIELDS (date subject from)] \"<headers>\" " +
"FLAGS (\\Seen))\r\n");
ImapResponse response = parser.readResponse();
assertEquals(3, response.size());
assertEquals("1", response.getString(0));
assertEquals("FETCH", response.getString(1));
assertEquals("UID", response.getList(2).getString(0));
assertEquals(23, response.getList(2).getNumber(1));
assertEquals("INTERNALDATE", response.getList(2).getString(2));
assertEquals("01-Jul-2015 12:34:56 +0200", response.getList(2).getString(3));
assertEquals("RFC822.SIZE", response.getList(2).getString(4));
assertEquals(3456, response.getList(2).getNumber(5));
assertEquals("BODY", response.getList(2).getString(6));
assertEquals(2, response.getList(2).getList(7).size());
assertEquals("HEADER.FIELDS", response.getList(2).getList(7).getString(0));
assertEquals(3, response.getList(2).getList(7).getList(1).size());
assertEquals("date", response.getList(2).getList(7).getList(1).getString(0));
assertEquals("subject", response.getList(2).getList(7).getList(1).getString(1));
assertEquals("from", response.getList(2).getList(7).getList(1).getString(2));
assertEquals("<headers>", response.getList(2).getString(8));
assertEquals("FLAGS", response.getList(2).getString(9));
assertEquals(1, response.getList(2).getList(10).size());
assertEquals("\\Seen", response.getList(2).getList(10).getString(0));
}
@Test
public void readStatusResponse_withNoResponse_shouldThrow() throws Exception {
ImapResponseParser parser = createParser("1 NO\r\n");
try {
parser.readStatusResponse("1", "COMMAND", "[logId]", null);
fail("Expected exception");
} catch (NegativeImapResponseException e) {
assertEquals("Command: COMMAND; response: #1# [NO]", e.getMessage());
}
}
@Test
public void readStatusResponse_withNoResponseAndAlertText_shouldThrowWithAlertText() throws Exception {
ImapResponseParser parser = createParser("1 NO [ALERT] Access denied\r\n");
try {
parser.readStatusResponse("1", "COMMAND", "[logId]", null);
fail("Expected exception");
} catch (NegativeImapResponseException e) {
assertEquals("Access denied", e.getAlertText());
}
}
private ImapResponseParser createParser(String response) {
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(response.getBytes(Charsets.UTF_8));
peekableInputStream = new PeekableInputStream(byteArrayInputStream);
return new ImapResponseParser(peekableInputStream);
}
private void assertAllInputConsumed() throws IOException {
assertEquals(0, peekableInputStream.available());
}
static class TestImapResponseCallback implements ImapResponseCallback {
private final int readNumberOfBytes;
private final Object returnValue;
private final boolean throwException;
private int exceptionCount = 0;
public boolean foundLiteralCalled = false;
public static TestImapResponseCallback readBytesAndReturn(int readNumberOfBytes, Object returnValue) {
return new TestImapResponseCallback(readNumberOfBytes, returnValue, false);
}
public static TestImapResponseCallback readBytesAndThrow(int readNumberOfBytes) {
return new TestImapResponseCallback(readNumberOfBytes, null, true);
}
private TestImapResponseCallback(int readNumberOfBytes, Object returnValue, boolean throwException) {
this.readNumberOfBytes = readNumberOfBytes;
this.returnValue = returnValue;
this.throwException = throwException;
}
@Override
public Object foundLiteral(ImapResponse response, FixedLengthInputStream literal) throws Exception {
foundLiteralCalled = true;
int skipBytes = readNumberOfBytes;
while (skipBytes > 0) {
long skippedBytes = literal.skip(skipBytes);
skipBytes -= skippedBytes;
}
if (throwException) {
throw new ImapResponseParserTestException(exceptionCount++);
}
return returnValue;
}
}
static class ImapResponseParserTestException extends RuntimeException {
public final int instanceNumber;
public ImapResponseParserTestException(int instanceNumber) {
this.instanceNumber = instanceNumber;
}
}
static class TestUntaggedHandler implements UntaggedHandler {
public final List<ImapResponse> responses = new ArrayList<>();
@Override
public void handleAsyncUntaggedResponse(ImapResponse response) {
responses.add(response);
}
}
}

View file

@ -0,0 +1,585 @@
package com.fsck.k9.mail.store.imap
import assertk.all
import assertk.assertThat
import assertk.assertions.cause
import assertk.assertions.containsExactly
import assertk.assertions.hasMessage
import assertk.assertions.hasSize
import assertk.assertions.index
import assertk.assertions.isEqualTo
import assertk.assertions.isFailure
import assertk.assertions.isInstanceOf
import assertk.assertions.isNotNull
import assertk.assertions.isNull
import assertk.assertions.isTrue
import assertk.assertions.prop
import com.fsck.k9.mail.filter.FixedLengthInputStream
import com.fsck.k9.mail.filter.PeekableInputStream
import java.io.ByteArrayInputStream
import java.io.IOException
import org.junit.Test
class ImapResponseParserTest {
private var peekableInputStream: PeekableInputStream? = null
@Test
fun `readResponse() with untagged OK response`() {
val parser = createParserWithResponses("* OK")
val response = parser.readResponse()
assertThat(response).containsExactly("OK")
assertThatAllInputWasConsumed()
}
@Test
fun `readResponse() with untagged OK response containing text`() {
val parser = createParserWithResponses("* OK Some text here")
val response = parser.readResponse()
assertThat(response).containsExactly("OK", "Some text here")
assertThatAllInputWasConsumed()
}
@Test
fun `readResponse() with untagged OK response containing resp-text code`() {
val parser = createParserWithResponses("* OK [UIDVALIDITY 3857529045]")
val response = parser.readResponse()
assertThat(response).hasSize(2)
assertThat(response).index(0).isEqualTo("OK")
assertThat(response).index(1).isInstanceOf(ImapList::class).containsExactly("UIDVALIDITY", "3857529045")
assertThatAllInputWasConsumed()
}
@Test
fun `readResponse() with untagged OK response containing resp-text code and text`() {
val parser = createParserWithResponses("* OK [token1 token2] {x} test [...]")
val response = parser.readResponse()
assertThat(response).hasSize(3)
assertThat(response).index(0).isEqualTo("OK")
assertThat(response).index(1).isInstanceOf(ImapList::class).containsExactly("token1", "token2")
assertThat(response).index(2).isEqualTo("{x} test [...]")
assertThatAllInputWasConsumed()
}
@Test
fun `readStatusResponse() with OK response`() {
val parser = createParserWithResponses(
"* COMMAND BAR\tBAZ",
"TAG OK COMMAND completed",
)
val responses = parser.readStatusResponse("TAG", null, null, null)
assertThat(responses).hasSize(2)
assertThat(responses).index(0).containsExactly("COMMAND", "BAR", "BAZ")
assertThat(responses).index(1).containsExactly("OK", "COMMAND completed")
assertThatAllInputWasConsumed()
}
@Test
fun `readStatusResponse() should only deliver untagged responses to UntaggedHandler`() {
val parser = createParserWithResponses(
"* UNTAGGED",
"A2 OK COMMAND completed",
)
val untaggedHandler = TestUntaggedHandler()
parser.readStatusResponse("A2", null, null, untaggedHandler)
assertThat(untaggedHandler.responses).hasSize(1)
assertThat(untaggedHandler.responses).index(0).containsExactly("UNTAGGED")
assertThatAllInputWasConsumed()
}
@Test
fun `readStatusResponse() should skip tagged response that does not match tag`() {
val parser = createParserWithResponses(
"* UNTAGGED",
"* 0 EXPUNGE",
"* 42 EXISTS",
"A1 COMMAND BAR BAZ",
"A2 OK COMMAND completed",
)
val untaggedHandler = TestUntaggedHandler()
val responses = parser.readStatusResponse("A2", null, null, untaggedHandler)
assertThat(responses).hasSize(3)
assertThat(responses).index(0).containsExactly("0", "EXPUNGE")
assertThat(responses).index(1).containsExactly("42", "EXISTS")
assertThat(responses).index(2).containsExactly("OK", "COMMAND completed")
assertThat(untaggedHandler.responses).hasSize(3)
assertThat(untaggedHandler.responses).index(0).containsExactly("UNTAGGED")
assertThat(untaggedHandler.responses).index(1).containsExactly("0", "EXPUNGE")
assertThat(untaggedHandler.responses).index(2).containsExactly("42", "EXISTS")
assertThatAllInputWasConsumed()
}
@Test
fun `readStatusResponse() should deliver untagged responses to UntaggedHandler even on negative tagged response`() {
val parser = createParserWithResponses(
"* untagged",
"A2 NO Bad response",
)
val untaggedHandler = TestUntaggedHandler()
try {
parser.readStatusResponse("A2", null, null, untaggedHandler)
} catch (ignored: NegativeImapResponseException) {
}
assertThat(untaggedHandler.responses).hasSize(1)
assertThat(untaggedHandler.responses).index(0).containsExactly("untagged")
assertThatAllInputWasConsumed()
}
@Test
fun `readStatusResponse() with error response should throw`() {
val parser = createParserWithResponses(
"* COMMAND BAR BAZ",
"TAG ERROR COMMAND errored",
)
assertThat {
parser.readStatusResponse("TAG", null, null, null)
}.isFailure()
.isInstanceOf(NegativeImapResponseException::class)
}
@Test
fun `readResponse() with resp-text code containing a list`() {
val parser = createParserWithResponses(
"""* OK [PERMANENTFLAGS (\Answered \Flagged \Deleted \Seen \Draft NonJunk ${"$"}MDNSent \*)] """ +
"Flags permitted.",
)
val response = parser.readResponse()
assertThat(response).hasSize(3)
assertThat(response).index(0).isEqualTo("OK")
assertThat(response).index(1).isInstanceOf(ImapList::class).all {
index(0).isEqualTo("PERMANENTFLAGS")
index(1).isInstanceOf(ImapList::class).containsExactly(
"""\Answered""",
"""\Flagged""",
"""\Deleted""",
"""\Seen""",
"""\Draft""",
"NonJunk",
"\$MDNSent",
"""\*""",
)
}
assertThat(response).index(2).isEqualTo("Flags permitted.")
assertThatAllInputWasConsumed()
}
@Test
fun `readResponse() with untagged EXISTS response`() {
val parser = createParserWithResponses("* 23 EXISTS")
val response = parser.readResponse()
assertThat(response).hasSize(2)
assertThat(response).transform { it.getNumber(0) }.isEqualTo(23)
assertThat(response).transform { it.getString(1) }.isEqualTo("EXISTS")
assertThatAllInputWasConsumed()
}
@Test
fun `readResponse() should throw if stream ends before end of line is found`() {
val parser = createParserWithData("* OK Some text ")
assertThat {
parser.readResponse()
}.isFailure()
.isInstanceOf(IOException::class)
}
@Test
fun `readResponse() with command continuation`() {
val parser = createParserWithResponses("+ Ready for additional command text")
val response = parser.readResponse()
assertThat(response.isContinuationRequested).isTrue()
assertThat(response).containsExactly("Ready for additional command text")
assertThatAllInputWasConsumed()
}
@Test
fun `readResponse() with literal`() {
val parser = createParserWithResponses("* {4}\r\ntest")
val response = parser.readResponse()
assertThat(response).containsExactly("test")
assertThatAllInputWasConsumed()
}
@Test
fun `readResponse() with empty literal`() {
val parser = createParserWithResponses("* {0}\r\n")
val response = parser.readResponse()
assertThat(response).containsExactly("")
assertThatAllInputWasConsumed()
}
@Test
fun `readResponse() should throw when end of stream is reached while reading literal`() {
val parser = createParserWithData("* {4}\r\nabc")
assertThat {
parser.readResponse()
}.isFailure()
.isInstanceOf(IOException::class)
}
@Test
fun `readResponse() with literal should include return value of ImapResponseCallback_foundLiteral() in response`() {
val parser = createParserWithResponses("* {4}\r\ntest")
val callback = TestImapResponseCallback.readBytesAndReturn(4, "replacement value")
val response = parser.readResponse(callback)
assertThat(response).containsExactly("replacement value")
assertThatAllInputWasConsumed()
}
@Test
fun `readResponse() with literal should read literal when ImapResponseCallback_foundLiteral() returns null`() {
val parser = createParserWithResponses("* {4}\r\ntest")
val callback = TestImapResponseCallback.readBytesAndReturn(0, null)
val response = parser.readResponse(callback)
assertThat(response).containsExactly("test")
assertThat(callback.foundLiteralCalled).isTrue()
assertThatAllInputWasConsumed()
}
@Test
fun `readResponse() with partly consuming callback returning null should throw`() {
val parser = createParserWithResponses("* {4}\r\ntest")
val callback = TestImapResponseCallback.readBytesAndReturn(2, null)
assertThat {
parser.readResponse(callback)
}.isFailure()
.isInstanceOf(AssertionError::class)
.hasMessage("Callback consumed some data but returned no result")
}
@Test
fun `readResponse() with partly consuming callback that throws should read all data and throw`() {
val parser = createParserWithResponses("* {4}\r\ntest")
val callback = TestImapResponseCallback.readBytesAndThrow(2)
assertThat {
parser.readResponse(callback)
}.isFailure()
.isInstanceOf(ImapResponseParserException::class)
.all {
hasMessage("readResponse(): Exception in callback method")
cause().isNotNull().isInstanceOf(ImapResponseParserTestException::class)
}
assertThatAllInputWasConsumed()
}
@Test
fun `readResponse() with callback that throws repeatedly should consume all input and throw first exception`() {
val parser = createParserWithResponses("* {3}\r\none {3}\r\ntwo")
val callback = TestImapResponseCallback.readBytesAndThrow(3)
assertThat {
parser.readResponse(callback)
}.isFailure()
.isInstanceOf(ImapResponseParserException::class)
.all {
hasMessage("readResponse(): Exception in callback method")
cause().isNotNull().isInstanceOf(ImapResponseParserTestException::class)
.prop(ImapResponseParserTestException::instanceNumber).isEqualTo(0)
}
assertThatAllInputWasConsumed()
}
@Test
fun `readResponse() with callback not consuming the entire literal should skip the rest of the literal`() {
val parser = createParserWithResponses("* {3}\r\none two")
val callback = TestImapResponseCallback.readBytesAndReturn(2, "replacement value")
val response = parser.readResponse(callback)
assertThat(response).containsExactly("replacement value", "two")
assertThatAllInputWasConsumed()
}
@Test
fun `readResponse() with callback not consuming and throwing should read response and throw`() {
val parser = createParserWithResponses("* {4}\r\ntest")
val callback = TestImapResponseCallback.readBytesAndThrow(0)
assertThat {
parser.readResponse(callback)
}.isFailure()
.isInstanceOf(ImapResponseParserException::class)
.hasMessage("readResponse(): Exception in callback method")
assertThatAllInputWasConsumed()
}
@Test
fun `readResponse() with callback throwing IOException should re-throw that exception`() {
val parser = createParserWithResponses("* {4}\r\ntest")
val exception = IOException()
val callback = ImapResponseCallback { _, _ -> throw exception }
assertThat {
parser.readResponse(callback)
}.isFailure()
.isEqualTo(exception)
}
@Test
fun `readResponse() with quoted string containing an escaped quote character`() {
val parser = createParserWithResponses("""* "qu\"oted"""")
val response = parser.readResponse()
assertThat(response).containsExactly("""qu"oted""")
assertThatAllInputWasConsumed()
}
@Test
fun `readResponse() with UTF-8 data in quoted string`() {
val parser = createParserWithResponses("""* "quöted"""")
val response = parser.readResponse()
assertThat(response).containsExactly("quöted")
assertThatAllInputWasConsumed()
}
@Test
fun `readResponse() should throw when end of stream is reached before end of quoted string`() {
val parser = createParserWithResponses("* \"abc")
assertThat {
parser.readResponse()
}.isFailure()
.isInstanceOf(IOException::class)
}
@Test
fun `readResponse() should throw if end of stream is reached before end of atom`() {
val parser = createParserWithData("* abc")
assertThat {
parser.readResponse()
}.isFailure()
.isInstanceOf(IOException::class)
}
@Test
fun `readResponse() should throw if untagged response indicator is not followed by a space`() {
val parser = createParserWithResponses("*")
assertThat {
parser.readResponse()
}.isFailure()
.isInstanceOf(IOException::class)
}
@Test
fun `readResponse() with LIST response containing folder name with brackets`() {
val parser = createParserWithResponses("""* LIST (\HasNoChildren) "." [FolderName]""")
val response = parser.readResponse()
assertThat(response).hasSize(4)
assertThat(response).index(0).isEqualTo("LIST")
assertThat(response).index(1).isInstanceOf(ImapList::class).containsExactly("""\HasNoChildren""")
assertThat(response).index(2).isEqualTo(".")
assertThat(response).index(3).isEqualTo("[FolderName]")
assertThatAllInputWasConsumed()
}
@Test
fun `readResponse() with LIST response containing folder name with parentheses should throw`() {
val parser = createParserWithResponses("""* LIST (\NoInferiors) "/" Root/Folder/Subfolder()""")
assertThat {
parser.readResponse()
}.isFailure()
.isInstanceOf(IOException::class)
}
@Test
fun `readResponse() should read whole LIST response line`() {
val parser = createParserWithResponses(
"""* LIST (\HasNoChildren) "." [FolderName]""",
"TAG OK [List complete]",
)
parser.readResponse()
val responseTwo = parser.readResponse()
assertThat(responseTwo.tag).isEqualTo("TAG")
assertThatAllInputWasConsumed()
}
@Test
fun `readResponse() with LIST response containing NIL`() {
val parser = createParserWithResponses("""* LIST (\NoInferiors) NIL INBOX""")
val response = parser.readResponse()
assertThat(response).hasSize(4)
assertThat(response).index(0).isEqualTo("LIST")
assertThat(response).index(1).isInstanceOf(ImapList::class).containsExactly("""\NoInferiors""")
assertThat(response).index(2).isNull()
assertThat(response).index(3).isEqualTo("INBOX")
assertThatAllInputWasConsumed()
}
@Test
fun `readResponse() with list as first token should throw`() {
val parser = createParserWithResponses("* [1 2] 3")
assertThat {
parser.readResponse()
}.isFailure()
.isInstanceOf(IOException::class)
.hasMessage("Unexpected non-string token: ImapList - [1, 2]")
}
@Test
fun `readResponse() with FETCH response`() {
val parser = createParserWithResponses(
"* 1 FETCH (" +
"UID 23 " +
"""INTERNALDATE "01-Jul-2015 12:34:56 +0200" """ +
"RFC822.SIZE 3456 " +
"""BODY[HEADER.FIELDS (date subject from)] "<headers>" """ +
"""FLAGS (\Seen)""" +
")",
)
val response = parser.readResponse()
assertThat(response).hasSize(3)
assertThat(response).index(0).isEqualTo("1")
assertThat(response).index(1).isEqualTo("FETCH")
assertThat(response).index(2).isInstanceOf(ImapList::class).all {
hasSize(11)
index(0).isEqualTo("UID")
index(1).isEqualTo("23")
index(2).isEqualTo("INTERNALDATE")
index(3).isEqualTo("01-Jul-2015 12:34:56 +0200")
index(4).isEqualTo("RFC822.SIZE")
index(5).isEqualTo("3456")
index(6).isEqualTo("BODY")
index(7).isInstanceOf(ImapList::class).all {
hasSize(2)
index(0).isEqualTo("HEADER.FIELDS")
index(1).isInstanceOf(ImapList::class).containsExactly("date", "subject", "from")
}
index(8).isEqualTo("<headers>")
index(9).isEqualTo("FLAGS")
index(10).isInstanceOf(ImapList::class).containsExactly("""\Seen""")
}
assertThatAllInputWasConsumed()
}
@Test
fun `readStatusResponse() with NO response should throw`() {
val parser = createParserWithResponses("1 NO")
assertThat {
parser.readStatusResponse("1", "COMMAND", "[logId]", null)
}.isFailure()
.isInstanceOf(NegativeImapResponseException::class)
.hasMessage("Command: COMMAND; response: #1# [NO]")
}
@Test
fun `readStatusResponse() with NO response and alert text should throw with alert text`() {
val parser = createParserWithResponses("1 NO [ALERT] Access denied\r\n")
assertThat {
parser.readStatusResponse("1", "COMMAND", "[logId]", null)
}.isFailure()
.isInstanceOf(NegativeImapResponseException::class)
.prop(NegativeImapResponseException::getAlertText).isEqualTo("Access denied")
}
private fun createParserWithResponses(vararg responses: String): ImapResponseParser {
val response = responses.joinToString(separator = "\r\n", postfix = "\r\n")
return createParserWithData(response)
}
private fun createParserWithData(response: String): ImapResponseParser {
val byteArrayInputStream = ByteArrayInputStream(response.toByteArray(Charsets.UTF_8))
peekableInputStream = PeekableInputStream(byteArrayInputStream)
return ImapResponseParser(peekableInputStream)
}
private fun assertThatAllInputWasConsumed() {
assertThat(peekableInputStream).isNotNull().prop(PeekableInputStream::available).isEqualTo(0)
}
}
private class TestImapResponseCallback(
private val readNumberOfBytes: Long,
private val returnValue: Any?,
private val throwException: Boolean,
) : ImapResponseCallback {
private var exceptionCount = 0
var foundLiteralCalled = false
override fun foundLiteral(response: ImapResponse, literal: FixedLengthInputStream): Any? {
foundLiteralCalled = true
var skipBytes = readNumberOfBytes
while (skipBytes > 0) {
skipBytes -= literal.skip(skipBytes)
}
if (throwException) {
throw ImapResponseParserTestException(exceptionCount++)
}
return returnValue
}
companion object {
fun readBytesAndReturn(readNumberOfBytes: Int, returnValue: Any?): TestImapResponseCallback {
return TestImapResponseCallback(readNumberOfBytes.toLong(), returnValue, false)
}
fun readBytesAndThrow(readNumberOfBytes: Int): TestImapResponseCallback {
return TestImapResponseCallback(readNumberOfBytes.toLong(), null, true)
}
}
}
private class ImapResponseParserTestException(val instanceNumber: Int) : RuntimeException()
private class TestUntaggedHandler : UntaggedHandler {
val responses = mutableListOf<ImapResponse>()
override fun handleAsyncUntaggedResponse(response: ImapResponse) {
responses.add(response)
}
}