]> git.ipfire.org Git - thirdparty/dovecot/core.git/commitdiff
lib-imap-client: Add unit test
authorTimo Sirainen <timo.sirainen@dovecot.fi>
Mon, 10 Apr 2017 14:10:21 +0000 (17:10 +0300)
committerTimo Sirainen <timo.sirainen@dovecot.fi>
Mon, 10 Apr 2017 15:00:13 +0000 (18:00 +0300)
Initially this tests some connection and reconnection problems.

src/lib-imap-client/Makefile.am
src/lib-imap-client/imapc-client.c
src/lib-imap-client/imapc-client.h
src/lib-imap-client/test-imapc-client.c [new file with mode: 0644]

index 80d6248552f649da6b8714726e4d87e27d90a109..3dfcf197fd8035e7174f29e0c73e86c167235db6 100644 (file)
@@ -2,6 +2,7 @@ noinst_LTLIBRARIES = libimap_client.la
 
 AM_CPPFLAGS = \
        -I$(top_srcdir)/src/lib \
+       -I$(top_srcdir)/src/lib-test \
        -I$(top_srcdir)/src/lib-dns \
        -I$(top_srcdir)/src/lib-sasl \
        -I$(top_srcdir)/src/lib-ssl-iostream \
@@ -21,3 +22,33 @@ headers = \
 
 pkginc_libdir=$(pkgincludedir)
 pkginc_lib_HEADERS = $(headers)
+
+test_programs = \
+       test-imapc-client
+
+noinst_PROGRAMS = $(test_programs)
+
+test_deps = \
+       $(noinst_LTLIBRARIES) \
+       ../lib-ssl-iostream/libssl_iostream.la \
+       ../lib-sasl/libsasl.la \
+       ../lib-imap/libimap.la \
+       ../lib-mail/libmail.la \
+       ../lib-charset/libcharset.la \
+       ../lib-dns/libdns.la \
+       ../lib-test/libtest.la \
+       ../lib/liblib.la
+
+test_libs = \
+       $(test_deps) \
+       $(MODULE_LIBS)
+
+test_imapc_client_SOURCES = test-imapc-client.c
+test_imapc_client_LDADD = $(test_libs)
+test_imapc_client_DEPENDENCIES = $(test_deps)
+
+check: check-am check-test
+check-test: all-am
+       for bin in $(test_programs); do \
+         if ! $(RUN_TEST) ./$$bin; then exit 1; fi; \
+       done
index 602eadae57a48c9f07fcc7f1ff3930f46b2f1bd4..a7a58cc2ac31128452725a86e939aabf2e68972d 100644 (file)
 
 #include <unistd.h>
 
+const char *imapc_command_state_names[] = {
+       "OK", "NO", "BAD", "(auth failed)", "(disconnected)"
+
+};
+
 const struct imapc_capability_name imapc_capability_names[] = {
        { "SASL-IR", IMAPC_CAPABILITY_SASL_IR },
        { "LITERAL+", IMAPC_CAPABILITY_LITERALPLUS },
index b07e4f057a017a9eb66d248be8d66f030b735607..8687a994f0e4d7c641465137387cd86319dce053 100644 (file)
@@ -15,6 +15,7 @@ enum imapc_command_state {
        /* Client was unexpectedly disconnected. */
        IMAPC_COMMAND_STATE_DISCONNECTED
 };
+extern const char *imapc_command_state_names[];
 
 enum imapc_capability {
        IMAPC_CAPABILITY_SASL_IR        = 0x01,
diff --git a/src/lib-imap-client/test-imapc-client.c b/src/lib-imap-client/test-imapc-client.c
new file mode 100644 (file)
index 0000000..4352b06
--- /dev/null
@@ -0,0 +1,605 @@
+/* Copyright (c) 2017 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "hostpid.h"
+#include "net.h"
+#include "fd-set-nonblock.h"
+#include "istream.h"
+#include "ostream.h"
+#include "ioloop.h"
+#include "unlink-directory.h"
+#include "test-common.h"
+#include "imapc-client-private.h"
+
+#include <stdio.h>
+#include <unistd.h>
+#include <signal.h>
+#include <sys/stat.h>
+#include <sys/wait.h>
+
+#define IMAPC_COMMAND_STATE_INVALID (enum imapc_command_state)-1
+
+typedef void test_server_init_t(void);
+typedef void test_client_init_t(void);
+
+struct test_server {
+       in_port_t port;
+       pid_t pid;
+
+       int fd_listen, fd;
+       struct istream *input;
+       struct ostream *output;
+};
+
+static struct ip_addr bind_ip;
+static struct test_server server;
+static struct imapc_client *imapc_client;
+static enum imapc_command_state imapc_login_last_reply;
+static ARRAY(enum imapc_command_state) imapc_cmd_last_replies;
+static bool debug = FALSE;
+
+static struct imapc_client_settings test_imapc_default_settings = {
+       .host = "127.0.0.1",
+       .username = "testuser",
+       .password = "testpass",
+
+       .dns_client_socket_path = "",
+       .temp_path_prefix = ".test-tmp/",
+       .rawlog_dir = "",
+
+       .connect_timeout_msecs = 500,
+       .connect_retry_count = 3,
+       .connect_retry_interval_msecs = 10,
+
+       .max_idle_time = 10000,
+};
+
+static int test_open_server_fd(in_port_t *bind_port)
+{
+       int fd = net_listen(&bind_ip, bind_port, 128);
+       if (debug)
+               i_debug("server listening on %u", *bind_port);
+       if (fd == -1) {
+               i_fatal("listen(%s:%u) failed: %m",
+                       net_ip2addr(&bind_ip), *bind_port);
+       }
+       fd_set_nonblock(fd, FALSE);
+       return fd;
+}
+
+static void
+test_server_wait_connection(struct test_server *server, bool send_banner)
+{
+       server->fd = net_accept(server->fd_listen, NULL, NULL);
+       i_assert(server->fd >= 0);
+
+       fd_set_nonblock(server->fd, FALSE);
+       server->input = i_stream_create_fd(server->fd, (size_t)-1, FALSE);
+       server->output = o_stream_create_fd(server->fd, (size_t)-1, FALSE);
+       o_stream_set_no_error_handling(server->output, TRUE);
+
+       if (send_banner) {
+               o_stream_nsend_str(server->output,
+                       "* OK [CAPABILITY IMAP4rev1] ready\r\n");
+       }
+}
+
+static void test_server_disconnect(struct test_server *server)
+{
+       if (server->input != NULL)
+               i_stream_unref(&server->input);
+       if (server->output != NULL)
+               o_stream_unref(&server->output);
+       if (server->fd != -1)
+               i_close_fd(&server->fd);
+}
+
+static void test_server_disconnect_and_wait(bool send_banner)
+{
+       test_server_disconnect(&server);
+       test_server_wait_connection(&server, send_banner);
+}
+
+static void test_server_kill(void)
+{
+       if (server.pid != (pid_t)-1) {
+               if (kill(server.pid, SIGKILL) < 0)
+                       i_fatal("kill(%ld) failed: %m", (long)server.pid);
+               if (waitpid(server.pid, NULL, 0) < 0)
+                       i_fatal("waitpid(%ld) failed: %m", (long)server.pid);
+               server.pid = -1;
+       }
+}
+
+static void test_run_client_server(
+       const struct imapc_client_settings *client_set,
+       test_client_init_t *client_test,
+       test_server_init_t *server_test)
+{
+       struct imapc_client_settings client_set_copy = *client_set;
+       struct ioloop *ioloop;
+
+       imapc_client_cmd_tag_counter = 0;
+       imapc_login_last_reply = IMAPC_COMMAND_STATE_INVALID;
+       t_array_init(&imapc_cmd_last_replies, 4);
+
+       i_zero(&server);
+       server.pid = (pid_t)-1;
+       server.fd = -1;
+       server.fd_listen = server_test == NULL ? -1 :
+               test_open_server_fd(&server.port);
+       client_set_copy.port = server.port;
+
+       if (mkdir(client_set->temp_path_prefix, 0700) < 0 && errno != EEXIST)
+               i_fatal("mkdir(%s) failed: %m", client_set->temp_path_prefix);
+
+       if ((server.pid = fork()) == (pid_t)-1)
+               i_fatal("fork() failed: %m");
+       if (server.pid == 0) {
+               server.pid = (pid_t)-1;
+               hostpid_init();
+               if (debug)
+                       i_debug("server: PID=%s", my_pid);
+               /* child: server */
+               ioloop = io_loop_create();
+               server_test();
+               test_server_disconnect(&server);
+               io_loop_destroy(&ioloop);
+               /* wait for it to be killed; this way, valgrind will not
+                  object to this process going away inelegantly. */
+               sleep(60);
+               exit(1);
+       }
+       /* parent: client */
+
+       usleep(100000); /* wait a little for server setup */
+
+       ioloop = io_loop_create();
+       imapc_client = imapc_client_init(&client_set_copy);
+       client_test();
+       test_assert(array_count(&imapc_cmd_last_replies) == 0);
+       if (imapc_client != NULL)
+               imapc_client_deinit(&imapc_client);
+       io_loop_destroy(&ioloop);
+
+       if (server.fd_listen != -1)
+               i_close_fd(&server.fd_listen);
+       test_server_kill();
+       (void)unlink_directory(client_set->temp_path_prefix, UNLINK_DIRECTORY_FLAG_RMDIR);
+}
+
+static enum imapc_command_state test_imapc_cmd_last_reply_pop(void)
+{
+       const enum imapc_command_state *replies;
+       enum imapc_command_state reply;
+       unsigned int count;
+
+       replies = array_get(&imapc_cmd_last_replies, &count);
+       if (count == 0)
+               return IMAPC_COMMAND_STATE_INVALID;
+       reply = replies[0];
+       array_delete(&imapc_cmd_last_replies, 0, 1);
+       return reply;
+}
+
+static bool test_imapc_cmd_last_reply_expect(enum imapc_command_state state)
+{
+       if (array_count(&imapc_cmd_last_replies) == 0)
+               imapc_client_run(imapc_client);
+       return test_imapc_cmd_last_reply_pop() == state;
+}
+
+static bool test_imapc_server_expect(const char *expected_line)
+{
+       const char *line = i_stream_read_next_line(server.input);
+
+       if (line == NULL) {
+               printf("imapc client disconnected unexpectedly: %s\n",
+                      i_stream_get_error(server.input));
+               return FALSE;
+       } else if (strcmp(line, expected_line) != 0) {
+               printf("imapc client sent '%s' when expecting '%s'\n",
+                      line, expected_line);
+               return FALSE;
+       } else {
+               return TRUE;
+       }
+}
+
+static void imapc_login_callback(const struct imapc_command_reply *reply,
+                                void *context ATTR_UNUSED)
+{
+       if (debug) {
+               i_debug("Login reply: %s %s",
+                       imapc_command_state_names[reply->state],
+                       reply->text_full);
+       }
+       imapc_login_last_reply = reply->state;
+       imapc_client_stop(imapc_client);
+}
+
+static void imapc_command_callback(const struct imapc_command_reply *reply,
+                                  void *context ATTR_UNUSED)
+{
+       if (debug) {
+               i_debug("Command reply: %s %s",
+                       imapc_command_state_names[reply->state],
+                       reply->text_full);
+       }
+       array_append(&imapc_cmd_last_replies, &reply->state, 1);
+       imapc_client_stop(imapc_client);
+}
+
+static void imapc_reopen_callback(void *context)
+{
+       struct imapc_client_mailbox *box = context;
+       struct imapc_command *cmd;
+
+       cmd = imapc_client_mailbox_cmd(box, imapc_command_callback, NULL);
+       imapc_command_set_flags(cmd, IMAPC_COMMAND_FLAG_SELECT);
+       imapc_command_send(cmd, "SELECT");
+}
+
+static void test_imapc_connect_failed_client(void)
+{
+       imapc_client_set_login_callback(imapc_client,
+                                       imapc_login_callback, NULL);
+       imapc_client_login(imapc_client);
+       /* connection refused & one reconnect */
+       test_expect_errors(2);
+       imapc_client_run(imapc_client);
+       test_expect_no_more_errors();
+       test_assert(imapc_login_last_reply == IMAPC_COMMAND_STATE_DISCONNECTED);
+}
+
+static void test_imapc_connect_failed(void)
+{
+       struct imapc_client_settings set = test_imapc_default_settings;
+
+       test_begin("imapc connect failed");
+       test_run_client_server(&set, test_imapc_connect_failed_client,
+                              NULL);
+       test_end();
+}
+
+static void test_imapc_banner_hangs_client(void)
+{
+       imapc_client_set_login_callback(imapc_client,
+                                       imapc_login_callback, NULL);
+       imapc_client_login(imapc_client);
+       test_expect_errors(2);
+       imapc_client_run(imapc_client);
+       test_expect_no_more_errors();
+       test_assert(imapc_login_last_reply == IMAPC_COMMAND_STATE_DISCONNECTED);
+}
+
+static void test_imapc_banner_hangs_server(void)
+{
+       struct test_server server2 = { .fd_listen = server.fd_listen };
+
+       test_server_wait_connection(&server, FALSE);
+       test_server_wait_connection(&server2, FALSE);
+       test_assert(i_stream_read_next_line(server2.input) == NULL);
+       test_server_disconnect(&server2);
+}
+
+static void test_imapc_banner_hangs(void)
+{
+       struct imapc_client_settings set = test_imapc_default_settings;
+
+       test_begin("imapc banner hangs");
+       test_run_client_server(&set, test_imapc_banner_hangs_client,
+                              test_imapc_banner_hangs_server);
+       test_end();
+}
+
+static void test_imapc_login_hangs_client(void)
+{
+       imapc_client_set_login_callback(imapc_client,
+                                       imapc_login_callback, NULL);
+       imapc_client_login(imapc_client);
+       test_expect_errors(2);
+       imapc_client_run(imapc_client);
+       test_expect_no_more_errors();
+       test_assert(imapc_login_last_reply == IMAPC_COMMAND_STATE_DISCONNECTED);
+}
+
+static void test_imapc_login_hangs_server(void)
+{
+       struct test_server server2 = { .fd_listen = server.fd_listen };
+
+       test_server_wait_connection(&server, TRUE);
+       test_assert(test_imapc_server_expect("1 LOGIN \"testuser\" \"testpass\""));
+
+       test_server_wait_connection(&server2, TRUE);
+       test_assert(test_imapc_server_expect("1 LOGIN \"testuser\" \"testpass\""));
+
+       test_assert(i_stream_read_next_line(server2.input) == NULL);
+       test_server_disconnect(&server2);
+}
+
+static void test_imapc_login_hangs(void)
+{
+       struct imapc_client_settings set = test_imapc_default_settings;
+
+       test_begin("imapc login hangs");
+       test_run_client_server(&set, test_imapc_login_hangs_client,
+                              test_imapc_login_hangs_server);
+       test_end();
+}
+
+static void test_imapc_reconnect_client(void)
+{
+       struct imapc_command *cmd;
+
+       /* login to server */
+       imapc_client_set_login_callback(imapc_client,
+                                       imapc_login_callback, NULL);
+       imapc_client_login(imapc_client);
+       imapc_client_run(imapc_client);
+       test_assert(imapc_login_last_reply == IMAPC_COMMAND_STATE_OK);
+       imapc_login_last_reply = IMAPC_COMMAND_STATE_INVALID;
+
+       /* disconnect */
+       cmd = imapc_client_cmd(imapc_client, imapc_command_callback, NULL);
+       imapc_command_send(cmd, "DISCONNECT");
+       test_expect_error_string("reconnecting");
+       imapc_client_run(imapc_client);
+       test_expect_no_more_errors();
+       test_assert(test_imapc_cmd_last_reply_pop() == IMAPC_COMMAND_STATE_DISCONNECTED);
+
+       /* we should be reconnected now. try a command. */
+       cmd = imapc_client_cmd(imapc_client, imapc_command_callback, NULL);
+       imapc_command_send(cmd, "NOOP");
+       imapc_client_run(imapc_client);
+       test_assert(imapc_login_last_reply == IMAPC_COMMAND_STATE_INVALID);
+       test_assert(test_imapc_cmd_last_reply_pop() == IMAPC_COMMAND_STATE_OK);
+}
+
+static void test_imapc_reconnect_server(void)
+{
+       test_server_wait_connection(&server, TRUE);
+       test_assert(test_imapc_server_expect("1 LOGIN \"testuser\" \"testpass\""));
+       o_stream_nsend_str(server.output, "1 OK \r\n");
+
+       test_assert(test_imapc_server_expect("2 DISCONNECT"));
+       test_server_disconnect_and_wait(TRUE);
+
+       test_assert(test_imapc_server_expect("4 LOGIN \"testuser\" \"testpass\""));
+       o_stream_nsend_str(server.output, "4 OK \r\n");
+       test_assert(test_imapc_server_expect("3 NOOP"));
+       o_stream_nsend_str(server.output, "3 OK \r\n");
+
+       test_assert(test_imapc_server_expect("4 LOGOUT"));
+}
+
+static void test_imapc_reconnect(void)
+{
+       struct imapc_client_settings set = test_imapc_default_settings;
+
+       test_begin("imapc reconnect");
+       test_run_client_server(&set, test_imapc_reconnect_client,
+                              test_imapc_reconnect_server);
+       test_end();
+}
+
+static void test_imapc_reconnect_resend_cmds_client(void)
+{
+       struct imapc_command *cmd;
+
+       /* login to server */
+       imapc_client_set_login_callback(imapc_client,
+                                       imapc_login_callback, NULL);
+       imapc_client_login(imapc_client);
+       imapc_client_run(imapc_client);
+       test_assert(imapc_login_last_reply == IMAPC_COMMAND_STATE_OK);
+       imapc_login_last_reply = IMAPC_COMMAND_STATE_INVALID;
+
+       /* send two commands */
+       cmd = imapc_client_cmd(imapc_client, imapc_command_callback, NULL);
+       imapc_command_set_flags(cmd, IMAPC_COMMAND_FLAG_RETRIABLE);
+       imapc_command_send(cmd, "RETRY1");
+       cmd = imapc_client_cmd(imapc_client, imapc_command_callback, NULL);
+       imapc_command_set_flags(cmd, IMAPC_COMMAND_FLAG_RETRIABLE);
+       imapc_command_send(cmd, "RETRY2");
+
+       /* disconnect & reconnect automatically */
+       cmd = imapc_client_cmd(imapc_client, imapc_command_callback, NULL);
+       imapc_command_send(cmd, "DISCONNECT");
+       test_expect_error_string("reconnecting");
+       imapc_client_run(imapc_client);
+       test_expect_no_more_errors();
+       test_assert(test_imapc_cmd_last_reply_expect(IMAPC_COMMAND_STATE_DISCONNECTED));
+
+       /* continue reconnection */
+       test_assert(test_imapc_cmd_last_reply_expect(IMAPC_COMMAND_STATE_OK));
+       test_assert(test_imapc_cmd_last_reply_expect(IMAPC_COMMAND_STATE_OK));
+}
+
+static void test_imapc_reconnect_resend_cmds_server(void)
+{
+       test_server_wait_connection(&server, TRUE);
+       test_assert(test_imapc_server_expect("1 LOGIN \"testuser\" \"testpass\""));
+       o_stream_nsend_str(server.output, "1 OK \r\n");
+
+       test_assert(test_imapc_server_expect("2 RETRY1"));
+       test_assert(test_imapc_server_expect("3 RETRY2"));
+       test_assert(test_imapc_server_expect("4 DISCONNECT"));
+       test_server_disconnect_and_wait(TRUE);
+
+       test_assert(test_imapc_server_expect("5 LOGIN \"testuser\" \"testpass\""));
+       o_stream_nsend_str(server.output, "5 OK \r\n");
+       test_assert(test_imapc_server_expect("2 RETRY1"));
+       o_stream_nsend_str(server.output, "2 OK \r\n");
+       test_assert(test_imapc_server_expect("3 RETRY2"));
+       o_stream_nsend_str(server.output, "3 OK \r\n");
+
+       test_assert(test_imapc_server_expect("4 LOGOUT"));
+}
+
+static void test_imapc_reconnect_resend_commands(void)
+{
+       struct imapc_client_settings set = test_imapc_default_settings;
+
+       test_begin("imapc reconnect resend commands");
+       test_run_client_server(&set, test_imapc_reconnect_resend_cmds_client,
+                              test_imapc_reconnect_resend_cmds_server);
+       test_end();
+}
+
+static void test_imapc_reconnect_resend_cmds_failed_client(void)
+{
+       struct imapc_command *cmd;
+
+       /* login to server */
+       imapc_client_set_login_callback(imapc_client,
+                                       imapc_login_callback, NULL);
+       imapc_client_login(imapc_client);
+       imapc_client_run(imapc_client);
+       test_assert(imapc_login_last_reply == IMAPC_COMMAND_STATE_OK);
+       imapc_login_last_reply = IMAPC_COMMAND_STATE_INVALID;
+
+       /* send two commands */
+       cmd = imapc_client_cmd(imapc_client, imapc_command_callback, NULL);
+       imapc_command_set_flags(cmd, IMAPC_COMMAND_FLAG_RETRIABLE);
+       imapc_command_send(cmd, "RETRY1");
+       cmd = imapc_client_cmd(imapc_client, imapc_command_callback, NULL);
+       imapc_command_set_flags(cmd, IMAPC_COMMAND_FLAG_RETRIABLE);
+       imapc_command_send(cmd, "RETRY2");
+
+       /* disconnect & try to reconnect automatically */
+       cmd = imapc_client_cmd(imapc_client, imapc_command_callback, NULL);
+       imapc_command_send(cmd, "DISCONNECT");
+       test_expect_error_string("reconnecting");
+       imapc_client_run(imapc_client);
+       test_expect_no_more_errors();
+       test_assert(test_imapc_cmd_last_reply_expect(IMAPC_COMMAND_STATE_DISCONNECTED));
+       test_expect_error_string("timed out");
+       test_assert(test_imapc_cmd_last_reply_expect(IMAPC_COMMAND_STATE_DISCONNECTED));
+       test_expect_no_more_errors();
+       test_assert(test_imapc_cmd_last_reply_expect(IMAPC_COMMAND_STATE_DISCONNECTED));
+}
+
+static void test_imapc_reconnect_resend_cmds_failed_server(void)
+{
+       test_server_wait_connection(&server, TRUE);
+       test_assert(test_imapc_server_expect("1 LOGIN \"testuser\" \"testpass\""));
+       o_stream_nsend_str(server.output, "1 OK \r\n");
+
+       test_assert(test_imapc_server_expect("2 RETRY1"));
+       test_assert(test_imapc_server_expect("3 RETRY2"));
+       test_assert(test_imapc_server_expect("4 DISCONNECT"));
+       test_server_disconnect(&server);
+}
+
+static void test_imapc_reconnect_resend_commands_failed(void)
+{
+       struct imapc_client_settings set = test_imapc_default_settings;
+
+       test_begin("imapc reconnect resend commands failed");
+       test_run_client_server(&set, test_imapc_reconnect_resend_cmds_failed_client,
+                              test_imapc_reconnect_resend_cmds_failed_server);
+       test_end();
+}
+
+static void test_imapc_reconnect_mailbox_client(void)
+{
+       struct imapc_command *cmd;
+       struct imapc_client_mailbox *box;
+
+       /* login to server */
+       imapc_client_set_login_callback(imapc_client,
+                                       imapc_login_callback, NULL);
+       imapc_client_login(imapc_client);
+       imapc_client_run(imapc_client);
+       test_assert(imapc_login_last_reply == IMAPC_COMMAND_STATE_OK);
+       imapc_login_last_reply = IMAPC_COMMAND_STATE_INVALID;
+
+       /* select a mailbox */
+       box = imapc_client_mailbox_open(imapc_client, NULL);
+       imapc_client_mailbox_set_reopen_cb(box, imapc_reopen_callback, box);
+
+       cmd = imapc_client_mailbox_cmd(box, imapc_command_callback, NULL);
+       imapc_command_set_flags(cmd, IMAPC_COMMAND_FLAG_SELECT);
+       imapc_command_send(cmd, "SELECT");
+       imapc_client_run(imapc_client);
+       test_assert(test_imapc_cmd_last_reply_expect(IMAPC_COMMAND_STATE_OK));
+
+       /* send a command */
+       cmd = imapc_client_mailbox_cmd(box, imapc_command_callback, NULL);
+       imapc_command_set_flags(cmd, IMAPC_COMMAND_FLAG_RETRIABLE);
+       imapc_command_send(cmd, "RETRY");
+
+       /* disconnect & reconnect automatically */
+       cmd = imapc_client_cmd(imapc_client, imapc_command_callback, NULL);
+       imapc_command_send(cmd, "DISCONNECT");
+       test_expect_error_string("reconnecting");
+       imapc_client_run(imapc_client);
+       test_expect_no_more_errors();
+       test_assert(test_imapc_cmd_last_reply_expect(IMAPC_COMMAND_STATE_DISCONNECTED));
+
+       /* continue reconnection */
+       test_assert(test_imapc_cmd_last_reply_expect(IMAPC_COMMAND_STATE_OK));
+       test_assert(test_imapc_cmd_last_reply_expect(IMAPC_COMMAND_STATE_OK));
+
+       imapc_client_mailbox_close(&box);
+}
+
+static void test_imapc_reconnect_mailbox_server(void)
+{
+       test_server_wait_connection(&server, TRUE);
+       test_assert(test_imapc_server_expect("1 LOGIN \"testuser\" \"testpass\""));
+       o_stream_nsend_str(server.output, "1 OK \r\n");
+
+       test_assert(test_imapc_server_expect("2 SELECT"));
+       o_stream_nsend_str(server.output, "2 OK \r\n");
+
+       test_assert(test_imapc_server_expect("3 RETRY"));
+       test_assert(test_imapc_server_expect("4 DISCONNECT"));
+       test_server_disconnect_and_wait(TRUE);
+
+       test_assert(test_imapc_server_expect("5 LOGIN \"testuser\" \"testpass\""));
+       o_stream_nsend_str(server.output, "5 OK \r\n");
+       test_assert(test_imapc_server_expect("6 SELECT"));
+       o_stream_nsend_str(server.output, "6 OK \r\n");
+       test_assert(test_imapc_server_expect("3 RETRY"));
+       o_stream_nsend_str(server.output, "3 OK \r\n");
+       i_info("retry");
+
+       test_assert(test_imapc_server_expect("4 LOGOUT"));
+}
+
+static void test_imapc_reconnect_mailbox(void)
+{
+       struct imapc_client_settings set = test_imapc_default_settings;
+
+       test_begin("imapc reconnect mailbox");
+       test_run_client_server(&set, test_imapc_reconnect_mailbox_client,
+                              test_imapc_reconnect_mailbox_server);
+       test_end();
+}
+
+int main(int argc ATTR_UNUSED, char *argv[])
+{
+       static void (*test_functions[])(void) = {
+               test_imapc_connect_failed,
+               test_imapc_banner_hangs,
+               test_imapc_login_hangs,
+               test_imapc_reconnect,
+               test_imapc_reconnect_resend_commands,
+               test_imapc_reconnect_resend_commands_failed,
+               test_imapc_reconnect_mailbox,
+               NULL
+       };
+
+       debug = null_strcmp(argv[1], "-D") == 0;
+       test_imapc_default_settings.debug = debug;
+
+       /* listen on localhost */
+       i_zero(&bind_ip);
+       bind_ip.family = AF_INET;
+       bind_ip.u.ip4.s_addr = htonl(INADDR_LOOPBACK);
+
+       return test_run(test_functions);
+}