changeset 611:9fbd1700435b

Tests: reoganize hierarchy
author David Demelier <markand@malikania.fr>
date Fri, 15 Dec 2017 15:37:58 +0100
parents 22b3cd6f991f
children 420480ce53e0
files tests/CMakeLists.txt tests/src/directory-jsapi/CMakeLists.txt tests/src/directory-jsapi/main.cpp tests/src/dynlib-plugin/CMakeLists.txt tests/src/dynlib-plugin/main.cpp tests/src/dynlib-plugin/test_plugin.cpp tests/src/elapsedtimer-jsapi/CMakeLists.txt tests/src/elapsedtimer-jsapi/empty.js tests/src/elapsedtimer-jsapi/main.cpp tests/src/file-jsapi/CMakeLists.txt tests/src/file-jsapi/empty.js tests/src/file-jsapi/main.cpp tests/src/irc/CMakeLists.txt tests/src/irc/main.cpp tests/src/irccd-jsapi/CMakeLists.txt tests/src/irccd-jsapi/empty.js tests/src/irccd-jsapi/main.cpp tests/src/js-plugin/CMakeLists.txt tests/src/js-plugin/config-assign.js tests/src/js-plugin/config-fill.js tests/src/js-plugin/irccd.conf tests/src/js-plugin/main.cpp tests/src/libirccd-js/CMakeLists.txt tests/src/libirccd-js/js-plugin/CMakeLists.txt tests/src/libirccd-js/js-plugin/config-assign.js tests/src/libirccd-js/js-plugin/config-fill.js tests/src/libirccd-js/js-plugin/irccd.conf tests/src/libirccd-js/js-plugin/main.cpp tests/src/libirccd-js/jsapi-directory/CMakeLists.txt tests/src/libirccd-js/jsapi-directory/main.cpp tests/src/libirccd-js/jsapi-elapsedtimer/CMakeLists.txt tests/src/libirccd-js/jsapi-elapsedtimer/empty.js tests/src/libirccd-js/jsapi-elapsedtimer/main.cpp tests/src/libirccd-js/jsapi-file/CMakeLists.txt tests/src/libirccd-js/jsapi-file/empty.js tests/src/libirccd-js/jsapi-file/main.cpp tests/src/libirccd-js/jsapi-irccd/CMakeLists.txt tests/src/libirccd-js/jsapi-irccd/empty.js tests/src/libirccd-js/jsapi-irccd/main.cpp tests/src/libirccd-js/jsapi-logger/CMakeLists.txt tests/src/libirccd-js/jsapi-logger/empty.js tests/src/libirccd-js/jsapi-logger/main.cpp tests/src/libirccd-js/jsapi-system/CMakeLists.txt tests/src/libirccd-js/jsapi-system/empty.js tests/src/libirccd-js/jsapi-system/main.cpp tests/src/libirccd-js/jsapi-timer/CMakeLists.txt tests/src/libirccd-js/jsapi-timer/main.cpp tests/src/libirccd-js/jsapi-timer/timer.js tests/src/libirccd-js/jsapi-unicode/CMakeLists.txt tests/src/libirccd-js/jsapi-unicode/empty.js tests/src/libirccd-js/jsapi-unicode/main.cpp tests/src/libirccd-js/jsapi-util/CMakeLists.txt tests/src/libirccd-js/jsapi-util/empty.js tests/src/libirccd-js/jsapi-util/main.cpp tests/src/libirccd/CMakeLists.txt tests/src/libirccd/command-plugin-config/CMakeLists.txt tests/src/libirccd/command-plugin-config/main.cpp tests/src/libirccd/command-plugin-info/CMakeLists.txt tests/src/libirccd/command-plugin-info/main.cpp tests/src/libirccd/command-plugin-list/CMakeLists.txt tests/src/libirccd/command-plugin-list/main.cpp tests/src/libirccd/command-plugin-load/CMakeLists.txt tests/src/libirccd/command-plugin-load/main.cpp tests/src/libirccd/command-plugin-reload/CMakeLists.txt tests/src/libirccd/command-plugin-reload/main.cpp tests/src/libirccd/command-plugin-unload/CMakeLists.txt tests/src/libirccd/command-plugin-unload/main.cpp tests/src/libirccd/command-rule-add/CMakeLists.txt tests/src/libirccd/command-rule-add/main.cpp tests/src/libirccd/command-rule-edit/CMakeLists.txt tests/src/libirccd/command-rule-edit/main.cpp tests/src/libirccd/command-rule-info/CMakeLists.txt tests/src/libirccd/command-rule-info/main.cpp tests/src/libirccd/command-rule-list/CMakeLists.txt tests/src/libirccd/command-rule-list/main.cpp tests/src/libirccd/command-rule-move/CMakeLists.txt tests/src/libirccd/command-rule-move/main.cpp tests/src/libirccd/command-rule-remove/CMakeLists.txt tests/src/libirccd/command-rule-remove/main.cpp tests/src/libirccd/command-server-connect/CMakeLists.txt tests/src/libirccd/command-server-connect/main.cpp tests/src/libirccd/command-server-disconnect/CMakeLists.txt tests/src/libirccd/command-server-disconnect/main.cpp tests/src/libirccd/command-server-info/CMakeLists.txt tests/src/libirccd/command-server-info/main.cpp tests/src/libirccd/command-server-invite/CMakeLists.txt tests/src/libirccd/command-server-invite/main.cpp tests/src/libirccd/command-server-join/CMakeLists.txt tests/src/libirccd/command-server-join/main.cpp tests/src/libirccd/command-server-kick/CMakeLists.txt tests/src/libirccd/command-server-kick/main.cpp tests/src/libirccd/command-server-list/CMakeLists.txt tests/src/libirccd/command-server-list/main.cpp tests/src/libirccd/command-server-me/CMakeLists.txt tests/src/libirccd/command-server-me/main.cpp tests/src/libirccd/command-server-message/CMakeLists.txt tests/src/libirccd/command-server-message/main.cpp tests/src/libirccd/command-server-mode/CMakeLists.txt tests/src/libirccd/command-server-mode/main.cpp tests/src/libirccd/command-server-nick/CMakeLists.txt tests/src/libirccd/command-server-nick/main.cpp tests/src/libirccd/command-server-notice/CMakeLists.txt tests/src/libirccd/command-server-notice/main.cpp tests/src/libirccd/command-server-part/CMakeLists.txt tests/src/libirccd/command-server-part/main.cpp tests/src/libirccd/command-server-reconnect/CMakeLists.txt tests/src/libirccd/command-server-reconnect/main.cpp tests/src/libirccd/command-server-topic/CMakeLists.txt tests/src/libirccd/command-server-topic/main.cpp tests/src/libirccd/dynlib-plugin/CMakeLists.txt tests/src/libirccd/dynlib-plugin/main.cpp tests/src/libirccd/dynlib-plugin/test_plugin.cpp tests/src/libirccd/irc/CMakeLists.txt tests/src/libirccd/irc/main.cpp tests/src/libirccd/logger/CMakeLists.txt tests/src/libirccd/logger/main.cpp tests/src/libirccd/rules/CMakeLists.txt tests/src/libirccd/rules/main.cpp tests/src/libirccd/util/CMakeLists.txt tests/src/libirccd/util/main.cpp tests/src/logger-jsapi/CMakeLists.txt tests/src/logger-jsapi/empty.js tests/src/logger-jsapi/main.cpp tests/src/logger/CMakeLists.txt tests/src/logger/main.cpp tests/src/plugin-ask/CMakeLists.txt tests/src/plugin-ask/answers.conf tests/src/plugin-ask/main.cpp tests/src/plugin-auth/CMakeLists.txt tests/src/plugin-auth/main.cpp tests/src/plugin-config-command/CMakeLists.txt tests/src/plugin-config-command/main.cpp tests/src/plugin-hangman/CMakeLists.txt tests/src/plugin-hangman/main.cpp tests/src/plugin-hangman/wordlist_fix_644.conf tests/src/plugin-hangman/words.conf tests/src/plugin-history/CMakeLists.txt tests/src/plugin-history/broken-conf.json tests/src/plugin-history/main.cpp tests/src/plugin-info-command/CMakeLists.txt tests/src/plugin-info-command/main.cpp tests/src/plugin-list-command/CMakeLists.txt tests/src/plugin-list-command/main.cpp tests/src/plugin-load-command/CMakeLists.txt tests/src/plugin-load-command/main.cpp tests/src/plugin-logger/CMakeLists.txt tests/src/plugin-logger/main.cpp tests/src/plugin-plugin/CMakeLists.txt tests/src/plugin-plugin/main.cpp tests/src/plugin-reload-command/CMakeLists.txt tests/src/plugin-reload-command/main.cpp tests/src/plugin-unload-command/CMakeLists.txt tests/src/plugin-unload-command/main.cpp tests/src/plugins/CMakeLists.txt tests/src/plugins/ask/CMakeLists.txt tests/src/plugins/ask/answers.conf tests/src/plugins/ask/main.cpp tests/src/plugins/auth/CMakeLists.txt tests/src/plugins/auth/main.cpp tests/src/plugins/hangman/CMakeLists.txt tests/src/plugins/hangman/main.cpp tests/src/plugins/hangman/wordlist_fix_644.conf tests/src/plugins/hangman/words.conf tests/src/plugins/history/CMakeLists.txt tests/src/plugins/history/broken-conf.json tests/src/plugins/history/main.cpp tests/src/plugins/logger/CMakeLists.txt tests/src/plugins/logger/main.cpp tests/src/plugins/plugin/CMakeLists.txt tests/src/plugins/plugin/main.cpp tests/src/rule-add-command/CMakeLists.txt tests/src/rule-add-command/main.cpp tests/src/rule-edit-command/CMakeLists.txt tests/src/rule-edit-command/main.cpp tests/src/rule-info-command/CMakeLists.txt tests/src/rule-info-command/main.cpp tests/src/rule-list-command/CMakeLists.txt tests/src/rule-list-command/main.cpp tests/src/rule-move-command/CMakeLists.txt tests/src/rule-move-command/main.cpp tests/src/rule-remove-command/CMakeLists.txt tests/src/rule-remove-command/main.cpp tests/src/rules/CMakeLists.txt tests/src/rules/main.cpp tests/src/server-connect-command/CMakeLists.txt tests/src/server-connect-command/main.cpp tests/src/server-disconnect-command/CMakeLists.txt tests/src/server-disconnect-command/main.cpp tests/src/server-info-command/CMakeLists.txt tests/src/server-info-command/main.cpp tests/src/server-invite-command/CMakeLists.txt tests/src/server-invite-command/main.cpp tests/src/server-join-command/CMakeLists.txt tests/src/server-join-command/main.cpp tests/src/server-kick-command/CMakeLists.txt tests/src/server-kick-command/main.cpp tests/src/server-list-command/CMakeLists.txt tests/src/server-list-command/main.cpp tests/src/server-me-command/CMakeLists.txt tests/src/server-me-command/main.cpp tests/src/server-message-command/CMakeLists.txt tests/src/server-message-command/main.cpp tests/src/server-mode-command/CMakeLists.txt tests/src/server-mode-command/main.cpp tests/src/server-nick-command/CMakeLists.txt tests/src/server-nick-command/main.cpp tests/src/server-notice-command/CMakeLists.txt tests/src/server-notice-command/main.cpp tests/src/server-part-command/CMakeLists.txt tests/src/server-part-command/main.cpp tests/src/server-reconnect-command/CMakeLists.txt tests/src/server-reconnect-command/main.cpp tests/src/server-topic-command/CMakeLists.txt tests/src/server-topic-command/main.cpp tests/src/system-jsapi/CMakeLists.txt tests/src/system-jsapi/empty.js tests/src/system-jsapi/main.cpp tests/src/timer-jsapi/CMakeLists.txt tests/src/timer-jsapi/main.cpp tests/src/timer-jsapi/timer.js tests/src/unicode-jsapi/CMakeLists.txt tests/src/unicode-jsapi/empty.js tests/src/unicode-jsapi/main.cpp tests/src/util-jsapi/CMakeLists.txt tests/src/util-jsapi/empty.js tests/src/util-jsapi/main.cpp tests/src/util/CMakeLists.txt tests/src/util/main.cpp
diffstat 214 files changed, 10401 insertions(+), 10346 deletions(-) [+]
line wrap: on
line diff
--- a/tests/CMakeLists.txt	Thu Dec 14 21:51:22 2017 +0100
+++ b/tests/CMakeLists.txt	Fri Dec 15 15:37:58 2017 +0100
@@ -19,60 +19,11 @@
 # Project
 project(tests)
 
-# Transport commands.
-add_subdirectory(src/plugin-config-command)
-add_subdirectory(src/plugin-info-command)
-add_subdirectory(src/plugin-list-command)
-add_subdirectory(src/plugin-load-command)
-add_subdirectory(src/plugin-reload-command)
-add_subdirectory(src/plugin-unload-command)
-add_subdirectory(src/rule-add-command)
-add_subdirectory(src/rule-edit-command)
-add_subdirectory(src/rule-info-command)
-add_subdirectory(src/rule-list-command)
-add_subdirectory(src/rule-move-command)
-add_subdirectory(src/rule-remove-command)
-add_subdirectory(src/server-connect-command)
-add_subdirectory(src/server-disconnect-command)
-add_subdirectory(src/server-info-command)
-add_subdirectory(src/server-invite-command)
-add_subdirectory(src/server-join-command)
-add_subdirectory(src/server-kick-command)
-add_subdirectory(src/server-list-command)
-add_subdirectory(src/server-me-command)
-add_subdirectory(src/server-message-command)
-add_subdirectory(src/server-mode-command)
-add_subdirectory(src/server-nick-command)
-add_subdirectory(src/server-notice-command)
-add_subdirectory(src/server-part-command)
-add_subdirectory(src/server-reconnect-command)
-add_subdirectory(src/server-topic-command)
-
-# Miscellaneous functions.
-add_subdirectory(src/dynlib-plugin)
-add_subdirectory(src/irc)
-add_subdirectory(src/logger)
-add_subdirectory(src/rules)
-add_subdirectory(src/util)
+# libirccd
+add_subdirectory(src/libirccd)
 
 # Javascript API and plugins.
 if (HAVE_JS)
-    add_subdirectory(src/js-plugin)
-
-    add_subdirectory(src/elapsedtimer-jsapi)
-    add_subdirectory(src/directory-jsapi)
-    add_subdirectory(src/file-jsapi)
-    add_subdirectory(src/irccd-jsapi)
-    add_subdirectory(src/logger-jsapi)
-    add_subdirectory(src/system-jsapi)
-    add_subdirectory(src/timer-jsapi)
-    add_subdirectory(src/unicode-jsapi)
-    add_subdirectory(src/util-jsapi)
-
-    add_subdirectory(src/plugin-ask)
-    add_subdirectory(src/plugin-auth)
-    add_subdirectory(src/plugin-hangman)
-    add_subdirectory(src/plugin-history)
-    add_subdirectory(src/plugin-logger)
-    add_subdirectory(src/plugin-plugin)
+    add_subdirectory(src/plugins)
+    add_subdirectory(src/libirccd-js)
 endif ()
--- a/tests/src/directory-jsapi/CMakeLists.txt	Thu Dec 14 21:51:22 2017 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,23 +0,0 @@
-#
-# CMakeLists.txt -- CMake build system for irccd
-#
-# Copyright (c) 2013-2017 David Demelier <markand@malikania.fr>
-#
-# Permission to use, copy, modify, and/or distribute this software for any
-# purpose with or without fee is hereby granted, provided that the above
-# copyright notice and this permission notice appear in all copies.
-#
-# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
-# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
-# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
-# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
-# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
-# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
-# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
-#
-
-irccd_define_test(
-    NAME directory-jsapi
-    SOURCES main.cpp
-    LIBRARIES libirccd-js
-)
--- a/tests/src/directory-jsapi/main.cpp	Thu Dec 14 21:51:22 2017 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,47 +0,0 @@
-/*
- * main.cpp -- test Irccd.Directory API
- *
- * Copyright (c) 2013-2017 David Demelier <markand@malikania.fr>
- *
- * Permission to use, copy, modify, and/or distribute this software for any
- * purpose with or without fee is hereby granted, provided that the above
- * copyright notice and this permission notice appear in all copies.
- *
- * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
- * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
- * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
- * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
- * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
- * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
- * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
- */
-
-#define BOOST_TEST_MODULE "Directory Javascript API"
-#include <boost/test/unit_test.hpp>
-
-#include <irccd/js/directory_jsapi.hpp>
-
-#include <irccd/test/js_test.hpp>
-
-namespace irccd {
-
-BOOST_FIXTURE_TEST_SUITE(directory_jsapi_suite, js_test<directory_jsapi>)
-
-BOOST_AUTO_TEST_CASE(constructor)
-{
-    const std::string script(
-        "d = new Irccd.Directory(CMAKE_SOURCE_DIR + \"/tests/root\");"
-        "p = d.path;"
-        "l = d.entries.length;"
-    );
-
-    if (duk_peval_string(plugin_->context(), script.c_str()) != 0)
-        throw dukx_stack(plugin_->context(), -1);
-
-    duk_get_global_string(plugin_->context(), "l");
-    BOOST_TEST(duk_get_int(plugin_->context(), -1) == 3);
-}
-
-BOOST_AUTO_TEST_SUITE_END()
-
-} // !irccd
--- a/tests/src/dynlib-plugin/CMakeLists.txt	Thu Dec 14 21:51:22 2017 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,43 +0,0 @@
-#
-# CMakeLists.txt -- CMake build system for irccd
-#
-# Copyright (c) 2013-2017 David Demelier <markand@malikania.fr>
-#
-# Permission to use, copy, modify, and/or distribute this software for any
-# purpose with or without fee is hereby granted, provided that the above
-# copyright notice and this permission notice appear in all copies.
-#
-# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
-# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
-# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
-# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
-# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
-# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
-# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
-#
-
-find_package(Boost REQUIRED QUIET)
-
-add_library(test-plugin MODULE test_plugin.cpp)
-target_link_libraries(test-plugin libirccd Boost::boost)
-set_target_properties(
-    test-plugin
-    PROPERTIES
-        PREFIX ""
-        RUNTIME_OUTPUT_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}
-)
-
-foreach (c ${CMAKE_CONFIGURATION_TYPES})
-    string(TOUPPER ${c} c)
-    set_target_properties(
-        test-plugin
-        PROPERTIES
-            RUNTIME_OUTPUT_DIRECTORY_${c} ${CMAKE_CURRENT_BINARY_DIR}
-    )
-endforeach ()
-
-irccd_define_test(
-    NAME dynlib-plugin
-    SOURCES main.cpp
-    LIBRARIES libirccd libirccd-test
-)
--- a/tests/src/dynlib-plugin/main.cpp	Thu Dec 14 21:51:22 2017 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,200 +0,0 @@
-/*
- * main.cpp -- test dynlib_plugin
- *
- * Copyright (c) 2013-2017 David Demelier <markand@malikania.fr>
- *
- * Permission to use, copy, modify, and/or distribute this software for any
- * purpose with or without fee is hereby granted, provided that the above
- * copyright notice and this permission notice appear in all copies.
- *
- * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
- * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
- * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
- * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
- * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
- * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
- * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
- */
-
-#define BOOST_TEST_MODULE "dynlib_plugin"
-#include <boost/test/unit_test.hpp>
-
-/*
- * For this test, we open a plugin written in C++ and pass a journal_server
- * class for each of the plugin function.
- *
- * Then we verify that the appropriate function has been called correctly.
- *
- * Functions load, unload and reload can not be tested though.
- */
-
-#include <irccd/daemon/dynlib_plugin.hpp>
-#include <irccd/daemon/irccd.hpp>
-
-#include <irccd/test/journal_server.hpp>
-
-namespace irccd {
-
-class fixture {
-protected:
-    boost::asio::io_service service_;
-    std::shared_ptr<journal_server> server_;
-    std::shared_ptr<plugin> plugin_;
-    irccd irccd_;
-
-    inline fixture()
-        : server_(std::make_shared<journal_server>(service_, "test"))
-        , irccd_(service_)
-    {
-        plugin_ = dynlib_plugin_loader({CMAKE_CURRENT_BINARY_DIR}).find("test-plugin");
-
-        if (!plugin_)
-            throw std::runtime_error("test plugin not found");
-    }
-};
-
-BOOST_FIXTURE_TEST_SUITE(dynlib_plugin_suite, fixture)
-
-BOOST_AUTO_TEST_CASE(on_command)
-{
-    plugin_->on_command(irccd_, {server_, "", "", ""});
-
-    BOOST_TEST(server_->cqueue().size() == 1U);
-    BOOST_TEST(server_->cqueue()[0]["command"].get<std::string>() == "message");
-    BOOST_TEST(server_->cqueue()[0]["message"].get<std::string>() == "on_command");
-    BOOST_TEST(server_->cqueue()[0]["target"].get<std::string>() == "test");
-}
-
-BOOST_AUTO_TEST_CASE(on_connect)
-{
-    plugin_->on_connect(irccd_, {server_});
-
-    BOOST_TEST(server_->cqueue().size() == 1U);
-    BOOST_TEST(server_->cqueue()[0]["command"].get<std::string>() == "message");
-    BOOST_TEST(server_->cqueue()[0]["message"].get<std::string>() == "on_connect");
-    BOOST_TEST(server_->cqueue()[0]["target"].get<std::string>() == "test");
-}
-
-BOOST_AUTO_TEST_CASE(on_invite)
-{
-    plugin_->on_invite(irccd_, {server_, "", "", ""});
-
-    BOOST_TEST(server_->cqueue().size() == 1U);
-    BOOST_TEST(server_->cqueue()[0]["command"].get<std::string>() == "message");
-    BOOST_TEST(server_->cqueue()[0]["message"].get<std::string>() == "on_invite");
-    BOOST_TEST(server_->cqueue()[0]["target"].get<std::string>() == "test");
-}
-
-BOOST_AUTO_TEST_CASE(on_join)
-{
-    plugin_->on_join(irccd_, {server_, "", ""});
-
-    BOOST_TEST(server_->cqueue().size() == 1U);
-    BOOST_TEST(server_->cqueue()[0]["command"].get<std::string>() == "message");
-    BOOST_TEST(server_->cqueue()[0]["message"].get<std::string>() == "on_join");
-    BOOST_TEST(server_->cqueue()[0]["target"].get<std::string>() == "test");
-}
-
-BOOST_AUTO_TEST_CASE(on_kick)
-{
-    plugin_->on_kick(irccd_, {server_, "", "", "", ""});
-
-    BOOST_TEST(server_->cqueue().size() == 1U);
-    BOOST_TEST(server_->cqueue()[0]["command"].get<std::string>() == "message");
-    BOOST_TEST(server_->cqueue()[0]["message"].get<std::string>() == "on_kick");
-    BOOST_TEST(server_->cqueue()[0]["target"].get<std::string>() == "test");
-}
-
-BOOST_AUTO_TEST_CASE(on_message)
-{
-    plugin_->on_message(irccd_, {server_, "", "", ""});
-
-    BOOST_TEST(server_->cqueue().size() == 1U);
-    BOOST_TEST(server_->cqueue()[0]["command"].get<std::string>() == "message");
-    BOOST_TEST(server_->cqueue()[0]["message"].get<std::string>() == "on_message");
-    BOOST_TEST(server_->cqueue()[0]["target"].get<std::string>() == "test");
-}
-
-BOOST_AUTO_TEST_CASE(on_me)
-{
-    plugin_->on_me(irccd_, {server_, "", "", ""});
-
-    BOOST_TEST(server_->cqueue().size() == 1U);
-    BOOST_TEST(server_->cqueue()[0]["command"].get<std::string>() == "message");
-    BOOST_TEST(server_->cqueue()[0]["message"].get<std::string>() == "on_me");
-    BOOST_TEST(server_->cqueue()[0]["target"].get<std::string>() == "test");
-}
-
-BOOST_AUTO_TEST_CASE(on_mode)
-{
-    plugin_->on_mode(irccd_, {server_, "", "", "", "", "", ""});
-
-    BOOST_TEST(server_->cqueue().size() == 1U);
-    BOOST_TEST(server_->cqueue()[0]["command"].get<std::string>() == "message");
-    BOOST_TEST(server_->cqueue()[0]["message"].get<std::string>() == "on_mode");
-    BOOST_TEST(server_->cqueue()[0]["target"].get<std::string>() == "test");
-}
-
-BOOST_AUTO_TEST_CASE(on_names)
-{
-    plugin_->on_names(irccd_, {server_, "", {}});
-
-    BOOST_TEST(server_->cqueue().size() == 1U);
-    BOOST_TEST(server_->cqueue()[0]["command"].get<std::string>() == "message");
-    BOOST_TEST(server_->cqueue()[0]["message"].get<std::string>() == "on_names");
-    BOOST_TEST(server_->cqueue()[0]["target"].get<std::string>() == "test");
-}
-
-BOOST_AUTO_TEST_CASE(on_nick)
-{
-    plugin_->on_nick(irccd_, {server_, "", ""});
-
-    BOOST_TEST(server_->cqueue().size() == 1U);
-    BOOST_TEST(server_->cqueue()[0]["command"].get<std::string>() == "message");
-    BOOST_TEST(server_->cqueue()[0]["message"].get<std::string>() == "on_nick");
-    BOOST_TEST(server_->cqueue()[0]["target"].get<std::string>() == "test");
-}
-
-BOOST_AUTO_TEST_CASE(on_notice)
-{
-    plugin_->on_notice(irccd_, {server_, "", "", ""});
-
-    BOOST_TEST(server_->cqueue().size() == 1U);
-    BOOST_TEST(server_->cqueue()[0]["command"].get<std::string>() == "message");
-    BOOST_TEST(server_->cqueue()[0]["message"].get<std::string>() == "on_notice");
-    BOOST_TEST(server_->cqueue()[0]["target"].get<std::string>() == "test");
-}
-
-BOOST_AUTO_TEST_CASE(on_part)
-{
-    plugin_->on_part(irccd_, {server_, "", "", ""});
-
-    BOOST_TEST(server_->cqueue().size() == 1U);
-    BOOST_TEST(server_->cqueue()[0]["command"].get<std::string>() == "message");
-    BOOST_TEST(server_->cqueue()[0]["message"].get<std::string>() == "on_part");
-    BOOST_TEST(server_->cqueue()[0]["target"].get<std::string>() == "test");
-}
-
-BOOST_AUTO_TEST_CASE(on_topic)
-{
-    plugin_->on_topic(irccd_, {server_, "", "", ""});
-
-    BOOST_TEST(server_->cqueue().size() == 1U);
-    BOOST_TEST(server_->cqueue()[0]["command"].get<std::string>() == "message");
-    BOOST_TEST(server_->cqueue()[0]["message"].get<std::string>() == "on_topic");
-    BOOST_TEST(server_->cqueue()[0]["target"].get<std::string>() == "test");
-}
-
-BOOST_AUTO_TEST_CASE(on_whois)
-{
-    plugin_->on_whois(irccd_, {server_, {"", "", "", "", {}}});
-
-    BOOST_TEST(server_->cqueue().size() == 1U);
-    BOOST_TEST(server_->cqueue()[0]["command"].get<std::string>() == "message");
-    BOOST_TEST(server_->cqueue()[0]["message"].get<std::string>() == "on_whois");
-    BOOST_TEST(server_->cqueue()[0]["target"].get<std::string>() == "test");
-}
-
-BOOST_AUTO_TEST_SUITE_END()
-
-} // !irccd
--- a/tests/src/dynlib-plugin/test_plugin.cpp	Thu Dec 14 21:51:22 2017 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,110 +0,0 @@
-/*
- * test_plugin.cpp -- basic exported plugin test
- *
- * Copyright (c) 2013-2017 David Demelier <markand@malikania.fr>
- *
- * Permission to use, copy, modify, and/or distribute this software for any
- * purpose with or without fee is hereby granted, provided that the above
- * copyright notice and this permission notice appear in all copies.
- *
- * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
- * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
- * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
- * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
- * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
- * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
- * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
- */
-
-#include <boost/dll.hpp>
-
-#include <irccd/daemon/plugin.hpp>
-
-namespace irccd {
-
-class test_plugin : public plugin {
-public:
-    using plugin::plugin;
-
-    void on_command(irccd&, const message_event& event) override
-    {
-        event.server->message("test", "on_command");
-    }
-
-    void on_connect(irccd&, const connect_event& event) override
-    {
-        event.server->message("test", "on_connect");
-    }
-
-    void on_invite(irccd&, const invite_event& event) override
-    {
-        event.server->message("test", "on_invite");
-    }
-
-    void on_join(irccd&, const join_event& event) override
-    {
-        event.server->message("test", "on_join");
-    }
-
-    void on_kick(irccd&, const kick_event& event) override
-    {
-        event.server->message("test", "on_kick");
-    }
-
-    void on_message(irccd&, const message_event& event) override
-    {
-        event.server->message("test", "on_message");
-    }
-
-    void on_me(irccd&, const me_event& event) override
-    {
-        event.server->message("test", "on_me");
-    }
-
-    void on_mode(irccd&, const mode_event& event) override
-    {
-        event.server->message("test", "on_mode");
-    }
-
-    void on_names(irccd&, const names_event& event) override
-    {
-        event.server->message("test", "on_names");
-    }
-
-    void on_nick(irccd&, const nick_event& event) override
-    {
-        event.server->message("test", "on_nick");
-    }
-
-    void on_notice(irccd&, const notice_event& event) override
-    {
-        event.server->message("test", "on_notice");
-    }
-
-    void on_part(irccd&, const part_event& event) override
-    {
-        event.server->message("test", "on_part");
-    }
-
-    void on_topic(irccd&, const topic_event& event) override
-    {
-        event.server->message("test", "on_topic");
-    }
-
-    void on_whois(irccd&, const whois_event& event) override
-    {
-        event.server->message("test", "on_whois");
-    }
-};
-
-} // !irccd
-
-extern "C" {
-
-BOOST_SYMBOL_EXPORT
-std::unique_ptr<irccd::plugin> irccd_testplugin_load(std::string name, std::string path)
-{
-    return std::make_unique<irccd::test_plugin>(name, path);
-}
-
-} // !C
--- a/tests/src/elapsedtimer-jsapi/CMakeLists.txt	Thu Dec 14 21:51:22 2017 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,23 +0,0 @@
-#
-# CMakeLists.txt -- CMake build system for irccd
-#
-# Copyright (c) 2013-2017 David Demelier <markand@malikania.fr>
-#
-# Permission to use, copy, modify, and/or distribute this software for any
-# purpose with or without fee is hereby granted, provided that the above
-# copyright notice and this permission notice appear in all copies.
-#
-# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
-# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
-# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
-# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
-# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
-# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
-# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
-#
-
-irccd_define_test(
-    NAME elapsedtimer-jsapi
-    SOURCES main.cpp
-    LIBRARIES libirccd-js
-)
--- a/tests/src/elapsedtimer-jsapi/main.cpp	Thu Dec 14 21:51:22 2017 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,51 +0,0 @@
-/*
- * main.cpp -- test Irccd.ElapsedTimer API
- *
- * Copyright (c) 2013-2017 David Demelier <markand@malikania.fr>
- *
- * Permission to use, copy, modify, and/or distribute this software for any
- * purpose with or without fee is hereby granted, provided that the above
- * copyright notice and this permission notice appear in all copies.
- *
- * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
- * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
- * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
- * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
- * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
- * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
- * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
- */
-
-#define BOOST_TEST_MODULE "ElapsedTimer Javascript API"
-#include <boost/test/unit_test.hpp>
-
-#include <thread>
-
-#include <irccd/js/elapsed_timer_jsapi.hpp>
-
-#include <irccd/test/js_test.hpp>
-
-using namespace std::chrono_literals;
-
-namespace irccd {
-
-BOOST_FIXTURE_TEST_SUITE(elapsed_timer_jsapi_suite, js_test<elapsed_timer_jsapi>)
-
-BOOST_AUTO_TEST_CASE(standard)
-{
-    if (duk_peval_string(plugin_->context(), "timer = new Irccd.ElapsedTimer();") != 0)
-        throw dukx_stack(plugin_->context(), -1);
-
-    std::this_thread::sleep_for(300ms);
-
-    if (duk_peval_string(plugin_->context(), "result = timer.elapsed();") != 0)
-        throw dukx_stack(plugin_->context(), -1);
-
-    BOOST_REQUIRE(duk_get_global_string(plugin_->context(), "result"));
-    BOOST_REQUIRE_GE(duk_get_int(plugin_->context(), -1), 250);
-    BOOST_REQUIRE_LE(duk_get_int(plugin_->context(), -1), 350);
-}
-
-} // !irccd
-
-BOOST_AUTO_TEST_SUITE_END()
--- a/tests/src/file-jsapi/CMakeLists.txt	Thu Dec 14 21:51:22 2017 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,23 +0,0 @@
-#
-# CMakeLists.txt -- CMake build system for irccd
-#
-# Copyright (c) 2013-2017 David Demelier <markand@malikania.fr>
-#
-# Permission to use, copy, modify, and/or distribute this software for any
-# purpose with or without fee is hereby granted, provided that the above
-# copyright notice and this permission notice appear in all copies.
-#
-# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
-# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
-# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
-# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
-# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
-# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
-# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
-#
-
-irccd_define_test(
-    NAME file-jsapi
-    SOURCES main.cpp
-    LIBRARIES libirccd-js
-)
--- a/tests/src/file-jsapi/main.cpp	Thu Dec 14 21:51:22 2017 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,308 +0,0 @@
-/*
- * main.cpp -- test Irccd.File API
- *
- * Copyright (c) 2013-2017 David Demelier <markand@malikania.fr>
- *
- * Permission to use, copy, modify, and/or distribute this software for any
- * purpose with or without fee is hereby granted, provided that the above
- * copyright notice and this permission notice appear in all copies.
- *
- * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
- * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
- * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
- * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
- * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
- * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
- * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
- */
-
-#define BOOST_TEST_MODULE "File Javascript API"
-#include <boost/test/unit_test.hpp>
-
-#include <fstream>
-
-#include <irccd/js/duktape_vector.hpp>
-#include <irccd/js/file_jsapi.hpp>
-
-#include <irccd/test/js_test.hpp>
-
-namespace irccd {
-
-BOOST_FIXTURE_TEST_SUITE(file_jsapi_suite, js_test<file_jsapi>)
-
-BOOST_AUTO_TEST_CASE(function_basename)
-{
-    if (duk_peval_string(plugin_->context(), "result = Irccd.File.basename('/usr/local/etc/irccd.conf');"))
-        throw dukx_stack(plugin_->context(), -1);
-
-    BOOST_TEST(duk_get_global_string(plugin_->context(), "result"));
-    BOOST_TEST("irccd.conf" == duk_get_string(plugin_->context(), -1));
-}
-
-BOOST_AUTO_TEST_CASE(function_dirname)
-{
-    if (duk_peval_string(plugin_->context(), "result = Irccd.File.dirname('/usr/local/etc/irccd.conf');"))
-        throw dukx_stack(plugin_->context(), -1);
-
-    BOOST_TEST(duk_get_global_string(plugin_->context(), "result"));
-    BOOST_TEST("/usr/local/etc" == duk_get_string(plugin_->context(), -1));
-}
-
-BOOST_AUTO_TEST_CASE(function_exists)
-{
-    if (duk_peval_string(plugin_->context(), "result = Irccd.File.exists(CMAKE_SOURCE_DIR + '/tests/root/file-1.txt')"))
-        throw dukx_stack(plugin_->context(), -1);
-
-    BOOST_TEST(duk_get_global_string(plugin_->context(), "result"));
-    BOOST_TEST(duk_get_boolean(plugin_->context(), -1));
-}
-
-BOOST_AUTO_TEST_CASE(function_exists2)
-{
-    if (duk_peval_string(plugin_->context(), "result = Irccd.File.exists('file_which_does_not_exist.txt')"))
-        throw dukx_stack(plugin_->context(), -1);
-
-    BOOST_TEST(duk_get_global_string(plugin_->context(), "result"));
-    BOOST_TEST(!duk_get_boolean(plugin_->context(), -1));
-}
-
-BOOST_AUTO_TEST_CASE(function_remove)
-{
-    // First create a dummy file
-    std::ofstream("test-js-fs.remove");
-
-    if (duk_peval_string(plugin_->context(), "Irccd.File.remove('test-js-fs.remove');") != 0)
-        throw dukx_stack(plugin_->context(), -1);
-
-    std::ifstream in("test-js-fs.remove");
-
-    BOOST_TEST(!in.is_open());
-}
-
-BOOST_AUTO_TEST_CASE(method_basename)
-{
-    auto ret = duk_peval_string(plugin_->context(),
-        "f = new Irccd.File(CMAKE_SOURCE_DIR + '/tests/root/file-1.txt', 'r');"
-        "result = f.basename();"
-    );
-
-    if (ret != 0)
-        throw dukx_stack(plugin_->context(), -1);
-
-    BOOST_TEST(duk_get_global_string(plugin_->context(), "result"));
-    BOOST_TEST("file-1.txt" == duk_get_string(plugin_->context(), -1));
-}
-
-BOOST_AUTO_TEST_CASE(method_basename_closed)
-{
-    auto ret = duk_peval_string(plugin_->context(),
-        "f = new Irccd.File(CMAKE_SOURCE_DIR + '/tests/root/file-1.txt', 'r');"
-        "f.close();"
-        "result = f.basename();"
-    );
-
-    if (ret != 0)
-        throw dukx_stack(plugin_->context(), -1);
-
-    BOOST_TEST(duk_get_global_string(plugin_->context(), "result"));
-    BOOST_TEST("file-1.txt" == duk_get_string(plugin_->context(), -1));
-}
-
-BOOST_AUTO_TEST_CASE(method_dirname)
-{
-    auto ret = duk_peval_string(plugin_->context(),
-        "f = new Irccd.File(CMAKE_SOURCE_DIR + '/tests/root/file-1.txt', 'r');"
-        "result = f.dirname();"
-    );
-
-    if (ret != 0)
-        throw dukx_stack(plugin_->context(), -1);
-
-    BOOST_TEST(duk_get_global_string(plugin_->context(), "result"));
-    BOOST_TEST(CMAKE_SOURCE_DIR "/tests/root" == duk_get_string(plugin_->context(), -1));
-}
-
-BOOST_AUTO_TEST_CASE(method_dirname_closed)
-{
-    auto ret = duk_peval_string(plugin_->context(),
-        "f = new Irccd.File(CMAKE_SOURCE_DIR + '/tests/root/file-1.txt', 'r');"
-        "f.close();"
-        "result = f.dirname();"
-    );
-
-    if (ret != 0)
-        throw dukx_stack(plugin_->context(), -1);
-
-    BOOST_TEST(duk_get_global_string(plugin_->context(), "result"));
-    BOOST_TEST(CMAKE_SOURCE_DIR "/tests/root" == duk_get_string(plugin_->context(), -1));
-}
-
-BOOST_AUTO_TEST_CASE(method_lines)
-{
-    auto ret = duk_peval_string(plugin_->context(),
-        "result = new Irccd.File(CMAKE_SOURCE_DIR + '/tests/root/lines.txt', 'r').lines();"
-    );
-
-    if (ret != 0)
-        throw dukx_stack(plugin_->context(), -1);
-
-    std::vector<std::string> expected{"a", "b", "c"};
-
-    BOOST_TEST(duk_get_global_string(plugin_->context(), "result"));
-    BOOST_TEST(expected == dukx_get<std::vector<std::string>>(plugin_->context(), -1));
-}
-
-BOOST_AUTO_TEST_CASE(method_seek1)
-{
-    auto ret = duk_peval_string(plugin_->context(),
-        "f = new Irccd.File(CMAKE_SOURCE_DIR + '/tests/root/file-1.txt', 'r');"
-        "f.seek(Irccd.File.SeekSet, 6);"
-        "result = f.read(1);"
-    );
-
-    if (ret != 0)
-        throw dukx_stack(plugin_->context(), -1);
-
-    BOOST_TEST(duk_get_global_string(plugin_->context(), "result"));
-    BOOST_TEST(".", dukx_get<std::string>(plugin_->context(), -1));
-}
-
-BOOST_AUTO_TEST_CASE(method_seek1_closed)
-{
-    auto ret = duk_peval_string(plugin_->context(),
-        "f = new Irccd.File(CMAKE_SOURCE_DIR + '/tests/root/file-1.txt', 'r');"
-        "f.close();"
-        "f.seek(Irccd.File.SeekSet, 4);"
-        "result = f.read(1);"
-        "result = typeof (result) === \"undefined\";"
-    );
-
-    if (ret != 0)
-        throw dukx_stack(plugin_->context(), -1);
-
-    BOOST_TEST(duk_get_global_string(plugin_->context(), "result"));
-    BOOST_TEST(duk_get_boolean(plugin_->context(), -1));
-}
-
-BOOST_AUTO_TEST_CASE(method_seek2)
-{
-    auto ret = duk_peval_string(plugin_->context(),
-        "f = new Irccd.File(CMAKE_SOURCE_DIR + '/tests/root/file-1.txt', 'r');"
-        "f.seek(Irccd.File.SeekSet, 2);"
-        "f.seek(Irccd.File.SeekCur, 4);"
-        "result = f.read(1);"
-    );
-
-    if (ret != 0)
-        throw dukx_stack(plugin_->context(), -1);
-
-    BOOST_TEST(duk_get_global_string(plugin_->context(), "result"));
-    BOOST_TEST("." == dukx_get<std::string>(plugin_->context(), -1));
-}
-
-BOOST_AUTO_TEST_CASE(method_seek2c_losed)
-{
-    auto ret = duk_peval_string(plugin_->context(),
-        "f = new Irccd.File(CMAKE_SOURCE_DIR + '/tests/root/file-1.txt', 'r');"
-        "f.close();"
-        "f.seek(Irccd.File.SeekSet, 2);"
-        "f.seek(Irccd.File.SeekCur, 2);"
-        "result = f.read(1);"
-        "result = typeof (result) === \"undefined\";"
-    );
-
-    if (ret != 0)
-        throw dukx_stack(plugin_->context(), -1);
-
-    BOOST_TEST(duk_get_global_string(plugin_->context(), "result"));
-    BOOST_TEST(duk_get_boolean(plugin_->context(), -1));
-}
-
-BOOST_AUTO_TEST_CASE(method_seek3)
-{
-    auto ret = duk_peval_string(plugin_->context(),
-        "f = new Irccd.File(CMAKE_SOURCE_DIR + '/tests/root/file-1.txt', 'r');"
-        "f.seek(Irccd.File.SeekEnd, -2);"
-        "result = f.read(1);"
-    );
-
-    if (ret != 0)
-        throw dukx_stack(plugin_->context(), -1);
-
-    BOOST_TEST(duk_get_global_string(plugin_->context(), "result"));
-    BOOST_TEST("t" == duk_get_string(plugin_->context(), -1));
-}
-
-BOOST_AUTO_TEST_CASE(method_seek3_closed)
-{
-    auto ret = duk_peval_string(plugin_->context(),
-        "f = new Irccd.File(CMAKE_SOURCE_DIR + '/tests/root/file-1.txt', 'r');"
-        "f.close();"
-        "f.seek(Irccd.File.SeekEnd, -2);"
-        "result = f.read(1);"
-        "result = typeof (result) === \"undefined\";"
-    );
-
-    if (ret != 0)
-        throw dukx_stack(plugin_->context(), -1);
-
-    BOOST_TEST(duk_get_global_string(plugin_->context(), "result"));
-    BOOST_TEST(duk_get_boolean(plugin_->context(), -1));
-}
-
-BOOST_AUTO_TEST_CASE(method_read1)
-{
-    auto ret = duk_peval_string(plugin_->context(),
-        "f = new Irccd.File(CMAKE_SOURCE_DIR + '/tests/root/file-1.txt', 'r');"
-        "result = f.read();"
-    );
-
-    if (ret != 0)
-        throw dukx_stack(plugin_->context(), -1);
-
-    BOOST_TEST(duk_get_global_string(plugin_->context(), "result"));
-    BOOST_TEST("file-1.txt\n" == duk_get_string(plugin_->context(), -1));
-}
-
-BOOST_AUTO_TEST_CASE(method_readline)
-{
-    auto ret = duk_peval_string(plugin_->context(),
-        "result = [];"
-        "f = new Irccd.File(CMAKE_SOURCE_DIR + '/tests/root/lines.txt', 'r');"
-        "for (var s; s = f.readline(); ) {"
-        "  result.push(s);"
-        "}"
-    );
-
-    if (ret != 0)
-        throw dukx_stack(plugin_->context(), -1);
-
-    std::vector<std::string> expected{"a", "b", "c"};
-
-    BOOST_TEST(duk_get_global_string(plugin_->context(), "result"));
-    BOOST_TEST(expected == dukx_get<std::vector<std::string>>(plugin_->context(), -1));
-}
-
-BOOST_AUTO_TEST_CASE(method_readline_closed)
-{
-    auto ret = duk_peval_string(plugin_->context(),
-        "result = [];"
-        "f = new Irccd.File(CMAKE_SOURCE_DIR + '/tests/root/lines.txt', 'r');"
-        "f.close();"
-        "for (var s; s = f.readline(); ) {"
-        "  result.push(s);"
-        "}"
-    );
-
-    if (ret != 0)
-        throw dukx_stack(plugin_->context(), -1);
-
-    std::vector<std::string> expected;
-
-    BOOST_TEST(duk_get_global_string(plugin_->context(), "result"));
-    BOOST_TEST(expected == dukx_get<std::vector<std::string>>(plugin_->context(), -1));
-}
-
-BOOST_AUTO_TEST_SUITE_END()
-
-} // !irccd
--- a/tests/src/irc/CMakeLists.txt	Thu Dec 14 21:51:22 2017 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,23 +0,0 @@
-#
-# CMakeLists.txt -- CMake build system for irccd
-#
-# Copyright (c) 2013-2017 David Demelier <markand@malikania.fr>
-#
-# Permission to use, copy, modify, and/or distribute this software for any
-# purpose with or without fee is hereby granted, provided that the above
-# copyright notice and this permission notice appear in all copies.
-#
-# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
-# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
-# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
-# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
-# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
-# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
-# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
-#
-
-irccd_define_test(
-    NAME irc
-    SOURCES main.cpp
-    LIBRARIES libirccd
-)
--- a/tests/src/irc/main.cpp	Thu Dec 14 21:51:22 2017 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,85 +0,0 @@
-/*
- * main.cpp -- test irc functions
- *
- * Copyright (c) 2013-2017 David Demelier <markand@malikania.fr>
- *
- * Permission to use, copy, modify, and/or distribute this software for any
- * purpose with or without fee is hereby granted, provided that the above
- * copyright notice and this permission notice appear in all copies.
- *
- * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
- * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
- * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
- * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
- * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
- * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
- * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
- */
-
-#define BOOST_TEST_MODULE "irc"
-#include <boost/test/unit_test.hpp>
-
-#include <irccd/daemon/irc.hpp>
-
-namespace irccd {
-
-BOOST_AUTO_TEST_SUITE(message_parse)
-
-BOOST_AUTO_TEST_CASE(no_prefix)
-{
-    irc::message m;
-
-    BOOST_TEST(!m);
-
-    m = irc::message::parse("PRIVMSG jean :bonjour à toi");
-    BOOST_TEST(m);
-    BOOST_TEST(m.prefix().empty());
-    BOOST_TEST(m.command() == "PRIVMSG");
-    BOOST_TEST(m.args().size() == 2U);
-    BOOST_TEST(m.args()[0] == "jean");
-    BOOST_TEST(m.args()[1] == "bonjour à toi");
-}
-
-BOOST_AUTO_TEST_CASE(prefix)
-{
-    irc::message m;
-
-    BOOST_TEST(!m);
-
-    m = irc::message::parse(":127.0.0.1 PRIVMSG jean :bonjour à toi");
-    BOOST_TEST(m);
-    BOOST_TEST(m.prefix() == "127.0.0.1");
-    BOOST_TEST(m.command() == "PRIVMSG");
-    BOOST_TEST(m.args().size() == 2U);
-    BOOST_TEST(m.args()[0] == "jean");
-    BOOST_TEST(m.args()[1] == "bonjour à toi");
-}
-
-BOOST_AUTO_TEST_SUITE_END()
-
-BOOST_AUTO_TEST_SUITE(user_parse)
-
-BOOST_AUTO_TEST_CASE(basics)
-{
-    auto user = irc::user::parse("jean!~jean@127.0.0.1");
-
-    BOOST_TEST(user.nick() == "jean");
-    BOOST_TEST(user.host() == "~jean@127.0.0.1");
-
-    auto usersimple = irc::user::parse("jean");
-
-    BOOST_TEST(usersimple.nick() == "jean");
-    BOOST_TEST(usersimple.host().empty());
-}
-
-BOOST_AUTO_TEST_CASE(empty)
-{
-    auto user = irc::user::parse("");
-
-    BOOST_TEST(user.nick().empty());
-    BOOST_TEST(user.host().empty());
-}
-
-BOOST_AUTO_TEST_SUITE_END()
-
-} // !irccd
--- a/tests/src/irccd-jsapi/CMakeLists.txt	Thu Dec 14 21:51:22 2017 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,23 +0,0 @@
-#
-# CMakeLists.txt -- CMake build system for irccd
-#
-# Copyright (c) 2013-2017 David Demelier <markand@malikania.fr>
-#
-# Permission to use, copy, modify, and/or distribute this software for any
-# purpose with or without fee is hereby granted, provided that the above
-# copyright notice and this permission notice appear in all copies.
-#
-# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
-# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
-# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
-# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
-# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
-# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
-# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
-#
-
-irccd_define_test(
-    NAME irccd-jsapi
-    SOURCES main.cpp
-    LIBRARIES libirccd-js
-)
--- a/tests/src/irccd-jsapi/main.cpp	Thu Dec 14 21:51:22 2017 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,115 +0,0 @@
-/*
- * main.cpp -- test Irccd API
- *
- * Copyright (c) 2013-2017 David Demelier <markand@malikania.fr>
- *
- * Permission to use, copy, modify, and/or distribute this software for any
- * purpose with or without fee is hereby granted, provided that the above
- * copyright notice and this permission notice appear in all copies.
- *
- * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
- * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
- * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
- * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
- * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
- * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
- * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
- */
-
-#define BOOST_TEST_MODULE "Irccd Javascript API"
-#include <boost/test/unit_test.hpp>
-
-#include <irccd/test/js_test.hpp>
-
-namespace irccd {
-
-BOOST_FIXTURE_TEST_SUITE(irccd_jsapi_suite, js_test<irccd_jsapi>)
-
-BOOST_AUTO_TEST_CASE(version)
-{
-    auto ret = duk_peval_string(plugin_->context(),
-        "major = Irccd.version.major;"
-        "minor = Irccd.version.minor;"
-        "patch = Irccd.version.patch;"
-    );
-
-    if (ret != 0)
-        throw dukx_stack(plugin_->context(), -1);
-
-    BOOST_TEST(duk_get_global_string(plugin_->context(), "major"));
-    BOOST_TEST(IRCCD_VERSION_MAJOR == duk_get_int(plugin_->context(), -1));
-    BOOST_TEST(duk_get_global_string(plugin_->context(), "minor"));
-    BOOST_TEST(IRCCD_VERSION_MINOR == duk_get_int(plugin_->context(), -1));
-    BOOST_TEST(duk_get_global_string(plugin_->context(), "patch"));
-    BOOST_TEST(IRCCD_VERSION_PATCH == duk_get_int(plugin_->context(), -1));
-}
-
-BOOST_AUTO_TEST_CASE(from_javascript)
-{
-    auto ret = duk_peval_string(plugin_->context(),
-        "try {"
-        "  throw new Irccd.SystemError(1, 'test');"
-        "} catch (e) {"
-        "  errno = e.errno;"
-        "  name = e.name;"
-        "  message = e.message;"
-        "  v1 = (e instanceof Error);"
-        "  v2 = (e instanceof Irccd.SystemError);"
-        "}"
-    );
-
-    if (ret != 0)
-        throw dukx_stack(plugin_->context(), -1);
-
-    BOOST_TEST(duk_get_global_string(plugin_->context(), "errno"));
-    BOOST_TEST(1 == duk_get_int(plugin_->context(), -1));
-    BOOST_TEST(duk_get_global_string(plugin_->context(), "name"));
-    BOOST_TEST("SystemError" == duk_get_string(plugin_->context(), -1));
-    BOOST_TEST(duk_get_global_string(plugin_->context(), "message"));
-    BOOST_TEST("test" == duk_get_string(plugin_->context(), -1));
-    BOOST_TEST(duk_get_global_string(plugin_->context(), "v1"));
-    BOOST_TEST(duk_get_boolean(plugin_->context(), -1));
-    BOOST_TEST(duk_get_global_string(plugin_->context(), "v2"));
-    BOOST_TEST(duk_get_boolean(plugin_->context(), -1));
-}
-
-BOOST_AUTO_TEST_CASE(from_native)
-{
-    duk_push_c_function(plugin_->context(), [] (duk_context *ctx) -> duk_ret_t {
-        dukx_throw(ctx, system_error(EINVAL, "hey"));
-
-        return 0;
-    }, 0);
-
-    duk_put_global_string(plugin_->context(), "f");
-
-    auto ret = duk_peval_string(plugin_->context(),
-        "try {"
-        "  f();"
-        "} catch (e) {"
-        "  errno = e.errno;"
-        "  name = e.name;"
-        "  message = e.message;"
-        "  v1 = (e instanceof Error);"
-        "  v2 = (e instanceof Irccd.SystemError);"
-        "}"
-    );
-
-    if (ret != 0)
-        throw dukx_stack(plugin_->context(), -1);
-
-    BOOST_TEST(duk_get_global_string(plugin_->context(), "errno"));
-    BOOST_TEST(EINVAL == duk_get_int(plugin_->context(), -1));
-    BOOST_TEST(duk_get_global_string(plugin_->context(), "name"));
-    BOOST_TEST("SystemError" == duk_get_string(plugin_->context(), -1));
-    BOOST_TEST(duk_get_global_string(plugin_->context(), "message"));
-    BOOST_TEST("hey" == duk_get_string(plugin_->context(), -1));
-    BOOST_TEST(duk_get_global_string(plugin_->context(), "v1"));
-    BOOST_TEST(duk_get_boolean(plugin_->context(), -1));
-    BOOST_TEST(duk_get_global_string(plugin_->context(), "v2"));
-    BOOST_TEST(duk_get_boolean(plugin_->context(), -1));
-}
-
-BOOST_AUTO_TEST_SUITE_END()
-
-} // !irccd
--- a/tests/src/js-plugin/CMakeLists.txt	Thu Dec 14 21:51:22 2017 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,23 +0,0 @@
-#
-# CMakeLists.txt -- CMake build system for irccd
-#
-# Copyright (c) 2013-2017 David Demelier <markand@malikania.fr>
-#
-# Permission to use, copy, modify, and/or distribute this software for any
-# purpose with or without fee is hereby granted, provided that the above
-# copyright notice and this permission notice appear in all copies.
-#
-# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
-# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
-# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
-# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
-# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
-# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
-# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
-#
-
-irccd_define_test(
-    NAME js-plugin
-    SOURCES main.cpp
-    LIBRARIES libirccd
-)
--- a/tests/src/js-plugin/config-assign.js	Thu Dec 14 21:51:22 2017 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,3 +0,0 @@
-Irccd.Plugin.config = {
-    "hard": "true"
-};
--- a/tests/src/js-plugin/config-fill.js	Thu Dec 14 21:51:22 2017 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,1 +0,0 @@
-Irccd.Plugin.config["hard"] = "true";
--- a/tests/src/js-plugin/irccd.conf	Thu Dec 14 21:51:22 2017 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,3 +0,0 @@
-[plugin.test]
-path = "none"
-verbose = "false"
--- a/tests/src/js-plugin/main.cpp	Thu Dec 14 21:51:22 2017 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,154 +0,0 @@
-/*
- * main.cpp -- test js_plugin object
- *
- * Copyright (c) 2013-2017 David Demelier <markand@malikania.fr>
- *
- * Permission to use, copy, modify, and/or distribute this software for any
- * purpose with or without fee is hereby granted, provided that the above
- * copyright notice and this permission notice appear in all copies.
- *
- * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
- * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
- * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
- * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
- * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
- * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
- * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
- */
-
-#define BOOST_TEST_MODULE "Javascript plugin object"
-#include <boost/asio.hpp>
-#include <boost/test/unit_test.hpp>
-
-#include <irccd/daemon/irccd.hpp>
-#include <irccd/daemon/plugin_service.hpp>
-
-#include <irccd/js/irccd_jsapi.hpp>
-#include <irccd/js/js_plugin.hpp>
-#include <irccd/js/plugin_jsapi.hpp>
-
-namespace irccd {
-
-class js_plugin_test {
-protected:
-    boost::asio::io_service service_;
-    irccd irccd_{service_};
-    std::shared_ptr<js_plugin> plugin_;
-
-    void load(std::string name, std::string path)
-    {
-        plugin_ = std::make_unique<js_plugin>(std::move(name), std::move(path));
-
-        irccd_jsapi().load(irccd_, plugin_);
-        plugin_jsapi().load(irccd_, plugin_);
-
-        plugin_->open();
-    }
-};
-
-BOOST_FIXTURE_TEST_SUITE(js_plugin_test_suite, js_plugin_test)
-
-BOOST_AUTO_TEST_CASE(assign)
-{
-    load("test", CMAKE_CURRENT_SOURCE_DIR "/config-assign.js");
-
-    plugin_->set_config({
-        { "path",       "none"  },
-        { "verbose",    "false" }
-    });
-    plugin_->on_load(irccd_);
-
-    BOOST_TEST(plugin_->config().at("path") == "none");
-    BOOST_TEST(plugin_->config().at("verbose") == "false");
-    BOOST_TEST(plugin_->config().at("hard") == "true");
-}
-
-BOOST_AUTO_TEST_CASE(fill)
-{
-    load("test", CMAKE_CURRENT_SOURCE_DIR "/config-fill.js");
-
-    plugin_->set_config({
-        { "path",       "none"  },
-        { "verbose",    "false" }
-    });
-    plugin_->on_load(irccd_);
-
-    BOOST_TEST(plugin_->config().at("path") == "none");
-    BOOST_TEST(plugin_->config().at("verbose") == "false");
-    BOOST_TEST(plugin_->config().at("hard") == "true");
-}
-
-BOOST_AUTO_TEST_CASE(merge_after)
-{
-    load("test", CMAKE_CURRENT_SOURCE_DIR "/config-fill.js");
-
-    plugin_->on_load(irccd_);
-    plugin_->set_config({
-        { "path",       "none"  },
-        { "verbose",    "false" }
-    });
-
-    BOOST_TEST(plugin_->config().at("path") == "none");
-    BOOST_TEST(plugin_->config().at("verbose") == "false");
-    BOOST_TEST(plugin_->config().at("hard") == "true");
-}
-
-BOOST_AUTO_TEST_SUITE_END()
-
-class js_plugin_loader_test {
-protected:
-    boost::asio::io_service service_;
-    irccd irccd_{service_};
-    std::shared_ptr<plugin> plugin_;
-
-    js_plugin_loader_test()
-    {
-        irccd_.set_config(config(CMAKE_CURRENT_SOURCE_DIR "/irccd.conf"));
-
-        auto loader = std::make_unique<js_plugin_loader>(irccd_);
-
-        loader->modules().push_back(std::make_unique<irccd_jsapi>());
-        loader->modules().push_back(std::make_unique<plugin_jsapi>());
-
-        irccd_.plugins().add_loader(std::move(loader));
-    }
-
-    void load(std::string name, std::string path)
-    {
-        irccd_.plugins().load(name, path);
-        plugin_ = irccd_.plugins().require(name);
-    }
-};
-
-BOOST_FIXTURE_TEST_SUITE(js_plugin_loader_test_suite, js_plugin_loader_test)
-
-BOOST_AUTO_TEST_CASE(assign)
-{
-    load("test", CMAKE_CURRENT_SOURCE_DIR "/config-assign.js");
-
-    BOOST_TEST(plugin_->config().at("path") == "none");
-    BOOST_TEST(plugin_->config().at("verbose") == "false");
-    BOOST_TEST(plugin_->config().at("hard") == "true");
-}
-
-BOOST_AUTO_TEST_CASE(fill)
-{
-    load("test", CMAKE_CURRENT_SOURCE_DIR "/config-fill.js");
-
-    BOOST_TEST(plugin_->config().at("path") == "none");
-    BOOST_TEST(plugin_->config().at("verbose") == "false");
-    BOOST_TEST(plugin_->config().at("hard") == "true");
-}
-
-BOOST_AUTO_TEST_CASE(merge_after)
-{
-    load("test", CMAKE_CURRENT_SOURCE_DIR "/config-fill.js");
-
-    BOOST_TEST(plugin_->config().at("path") == "none");
-    BOOST_TEST(plugin_->config().at("verbose") == "false");
-    BOOST_TEST(plugin_->config().at("hard") == "true");
-}
-
-BOOST_AUTO_TEST_SUITE_END()
-
-} // !irccd
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/src/libirccd-js/CMakeLists.txt	Fri Dec 15 15:37:58 2017 +0100
@@ -0,0 +1,29 @@
+#
+# CMakeLists.txt -- CMake build system for irccd
+#
+# Copyright (c) 2013-2017 David Demelier <markand@malikania.fr>
+#
+# Permission to use, copy, modify, and/or distribute this software for any
+# purpose with or without fee is hereby granted, provided that the above
+# copyright notice and this permission notice appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+#
+
+add_subdirectory(js-plugin)
+
+add_subdirectory(jsapi-elapsedtimer)
+add_subdirectory(jsapi-directory)
+add_subdirectory(jsapi-file)
+add_subdirectory(jsapi-irccd)
+add_subdirectory(jsapi-logger)
+add_subdirectory(jsapi-system)
+add_subdirectory(jsapi-timer)
+add_subdirectory(jsapi-unicode)
+add_subdirectory(jsapi-util)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/src/libirccd-js/js-plugin/CMakeLists.txt	Fri Dec 15 15:37:58 2017 +0100
@@ -0,0 +1,23 @@
+#
+# CMakeLists.txt -- CMake build system for irccd
+#
+# Copyright (c) 2013-2017 David Demelier <markand@malikania.fr>
+#
+# Permission to use, copy, modify, and/or distribute this software for any
+# purpose with or without fee is hereby granted, provided that the above
+# copyright notice and this permission notice appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+#
+
+irccd_define_test(
+    NAME js-plugin
+    SOURCES main.cpp
+    LIBRARIES libirccd
+)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/src/libirccd-js/js-plugin/config-assign.js	Fri Dec 15 15:37:58 2017 +0100
@@ -0,0 +1,3 @@
+Irccd.Plugin.config = {
+    "hard": "true"
+};
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/src/libirccd-js/js-plugin/config-fill.js	Fri Dec 15 15:37:58 2017 +0100
@@ -0,0 +1,1 @@
+Irccd.Plugin.config["hard"] = "true";
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/src/libirccd-js/js-plugin/irccd.conf	Fri Dec 15 15:37:58 2017 +0100
@@ -0,0 +1,3 @@
+[plugin.test]
+path = "none"
+verbose = "false"
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/src/libirccd-js/js-plugin/main.cpp	Fri Dec 15 15:37:58 2017 +0100
@@ -0,0 +1,154 @@
+/*
+ * main.cpp -- test js_plugin object
+ *
+ * Copyright (c) 2013-2017 David Demelier <markand@malikania.fr>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#define BOOST_TEST_MODULE "Javascript plugin object"
+#include <boost/asio.hpp>
+#include <boost/test/unit_test.hpp>
+
+#include <irccd/daemon/irccd.hpp>
+#include <irccd/daemon/plugin_service.hpp>
+
+#include <irccd/js/irccd_jsapi.hpp>
+#include <irccd/js/js_plugin.hpp>
+#include <irccd/js/plugin_jsapi.hpp>
+
+namespace irccd {
+
+class js_plugin_test {
+protected:
+    boost::asio::io_service service_;
+    irccd irccd_{service_};
+    std::shared_ptr<js_plugin> plugin_;
+
+    void load(std::string name, std::string path)
+    {
+        plugin_ = std::make_unique<js_plugin>(std::move(name), std::move(path));
+
+        irccd_jsapi().load(irccd_, plugin_);
+        plugin_jsapi().load(irccd_, plugin_);
+
+        plugin_->open();
+    }
+};
+
+BOOST_FIXTURE_TEST_SUITE(js_plugin_test_suite, js_plugin_test)
+
+BOOST_AUTO_TEST_CASE(assign)
+{
+    load("test", CMAKE_CURRENT_SOURCE_DIR "/config-assign.js");
+
+    plugin_->set_config({
+        { "path",       "none"  },
+        { "verbose",    "false" }
+    });
+    plugin_->on_load(irccd_);
+
+    BOOST_TEST(plugin_->config().at("path") == "none");
+    BOOST_TEST(plugin_->config().at("verbose") == "false");
+    BOOST_TEST(plugin_->config().at("hard") == "true");
+}
+
+BOOST_AUTO_TEST_CASE(fill)
+{
+    load("test", CMAKE_CURRENT_SOURCE_DIR "/config-fill.js");
+
+    plugin_->set_config({
+        { "path",       "none"  },
+        { "verbose",    "false" }
+    });
+    plugin_->on_load(irccd_);
+
+    BOOST_TEST(plugin_->config().at("path") == "none");
+    BOOST_TEST(plugin_->config().at("verbose") == "false");
+    BOOST_TEST(plugin_->config().at("hard") == "true");
+}
+
+BOOST_AUTO_TEST_CASE(merge_after)
+{
+    load("test", CMAKE_CURRENT_SOURCE_DIR "/config-fill.js");
+
+    plugin_->on_load(irccd_);
+    plugin_->set_config({
+        { "path",       "none"  },
+        { "verbose",    "false" }
+    });
+
+    BOOST_TEST(plugin_->config().at("path") == "none");
+    BOOST_TEST(plugin_->config().at("verbose") == "false");
+    BOOST_TEST(plugin_->config().at("hard") == "true");
+}
+
+BOOST_AUTO_TEST_SUITE_END()
+
+class js_plugin_loader_test {
+protected:
+    boost::asio::io_service service_;
+    irccd irccd_{service_};
+    std::shared_ptr<plugin> plugin_;
+
+    js_plugin_loader_test()
+    {
+        irccd_.set_config(config(CMAKE_CURRENT_SOURCE_DIR "/irccd.conf"));
+
+        auto loader = std::make_unique<js_plugin_loader>(irccd_);
+
+        loader->modules().push_back(std::make_unique<irccd_jsapi>());
+        loader->modules().push_back(std::make_unique<plugin_jsapi>());
+
+        irccd_.plugins().add_loader(std::move(loader));
+    }
+
+    void load(std::string name, std::string path)
+    {
+        irccd_.plugins().load(name, path);
+        plugin_ = irccd_.plugins().require(name);
+    }
+};
+
+BOOST_FIXTURE_TEST_SUITE(js_plugin_loader_test_suite, js_plugin_loader_test)
+
+BOOST_AUTO_TEST_CASE(assign)
+{
+    load("test", CMAKE_CURRENT_SOURCE_DIR "/config-assign.js");
+
+    BOOST_TEST(plugin_->config().at("path") == "none");
+    BOOST_TEST(plugin_->config().at("verbose") == "false");
+    BOOST_TEST(plugin_->config().at("hard") == "true");
+}
+
+BOOST_AUTO_TEST_CASE(fill)
+{
+    load("test", CMAKE_CURRENT_SOURCE_DIR "/config-fill.js");
+
+    BOOST_TEST(plugin_->config().at("path") == "none");
+    BOOST_TEST(plugin_->config().at("verbose") == "false");
+    BOOST_TEST(plugin_->config().at("hard") == "true");
+}
+
+BOOST_AUTO_TEST_CASE(merge_after)
+{
+    load("test", CMAKE_CURRENT_SOURCE_DIR "/config-fill.js");
+
+    BOOST_TEST(plugin_->config().at("path") == "none");
+    BOOST_TEST(plugin_->config().at("verbose") == "false");
+    BOOST_TEST(plugin_->config().at("hard") == "true");
+}
+
+BOOST_AUTO_TEST_SUITE_END()
+
+} // !irccd
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/src/libirccd-js/jsapi-directory/CMakeLists.txt	Fri Dec 15 15:37:58 2017 +0100
@@ -0,0 +1,23 @@
+#
+# CMakeLists.txt -- CMake build system for irccd
+#
+# Copyright (c) 2013-2017 David Demelier <markand@malikania.fr>
+#
+# Permission to use, copy, modify, and/or distribute this software for any
+# purpose with or without fee is hereby granted, provided that the above
+# copyright notice and this permission notice appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+#
+
+irccd_define_test(
+    NAME jsapi-directory
+    SOURCES main.cpp
+    LIBRARIES libirccd-js
+)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/src/libirccd-js/jsapi-directory/main.cpp	Fri Dec 15 15:37:58 2017 +0100
@@ -0,0 +1,47 @@
+/*
+ * main.cpp -- test Irccd.Directory API
+ *
+ * Copyright (c) 2013-2017 David Demelier <markand@malikania.fr>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#define BOOST_TEST_MODULE "Directory Javascript API"
+#include <boost/test/unit_test.hpp>
+
+#include <irccd/js/directory_jsapi.hpp>
+
+#include <irccd/test/js_test.hpp>
+
+namespace irccd {
+
+BOOST_FIXTURE_TEST_SUITE(directory_jsapi_suite, js_test<directory_jsapi>)
+
+BOOST_AUTO_TEST_CASE(constructor)
+{
+    const std::string script(
+        "d = new Irccd.Directory(CMAKE_SOURCE_DIR + \"/tests/root\");"
+        "p = d.path;"
+        "l = d.entries.length;"
+    );
+
+    if (duk_peval_string(plugin_->context(), script.c_str()) != 0)
+        throw dukx_stack(plugin_->context(), -1);
+
+    duk_get_global_string(plugin_->context(), "l");
+    BOOST_TEST(duk_get_int(plugin_->context(), -1) == 3);
+}
+
+BOOST_AUTO_TEST_SUITE_END()
+
+} // !irccd
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/src/libirccd-js/jsapi-elapsedtimer/CMakeLists.txt	Fri Dec 15 15:37:58 2017 +0100
@@ -0,0 +1,23 @@
+#
+# CMakeLists.txt -- CMake build system for irccd
+#
+# Copyright (c) 2013-2017 David Demelier <markand@malikania.fr>
+#
+# Permission to use, copy, modify, and/or distribute this software for any
+# purpose with or without fee is hereby granted, provided that the above
+# copyright notice and this permission notice appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+#
+
+irccd_define_test(
+    NAME jsapi-elapsedtimer
+    SOURCES main.cpp
+    LIBRARIES libirccd-js
+)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/src/libirccd-js/jsapi-elapsedtimer/main.cpp	Fri Dec 15 15:37:58 2017 +0100
@@ -0,0 +1,51 @@
+/*
+ * main.cpp -- test Irccd.ElapsedTimer API
+ *
+ * Copyright (c) 2013-2017 David Demelier <markand@malikania.fr>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#define BOOST_TEST_MODULE "ElapsedTimer Javascript API"
+#include <boost/test/unit_test.hpp>
+
+#include <thread>
+
+#include <irccd/js/elapsed_timer_jsapi.hpp>
+
+#include <irccd/test/js_test.hpp>
+
+using namespace std::chrono_literals;
+
+namespace irccd {
+
+BOOST_FIXTURE_TEST_SUITE(elapsed_timer_jsapi_suite, js_test<elapsed_timer_jsapi>)
+
+BOOST_AUTO_TEST_CASE(standard)
+{
+    if (duk_peval_string(plugin_->context(), "timer = new Irccd.ElapsedTimer();") != 0)
+        throw dukx_stack(plugin_->context(), -1);
+
+    std::this_thread::sleep_for(300ms);
+
+    if (duk_peval_string(plugin_->context(), "result = timer.elapsed();") != 0)
+        throw dukx_stack(plugin_->context(), -1);
+
+    BOOST_REQUIRE(duk_get_global_string(plugin_->context(), "result"));
+    BOOST_REQUIRE_GE(duk_get_int(plugin_->context(), -1), 250);
+    BOOST_REQUIRE_LE(duk_get_int(plugin_->context(), -1), 350);
+}
+
+} // !irccd
+
+BOOST_AUTO_TEST_SUITE_END()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/src/libirccd-js/jsapi-file/CMakeLists.txt	Fri Dec 15 15:37:58 2017 +0100
@@ -0,0 +1,23 @@
+#
+# CMakeLists.txt -- CMake build system for irccd
+#
+# Copyright (c) 2013-2017 David Demelier <markand@malikania.fr>
+#
+# Permission to use, copy, modify, and/or distribute this software for any
+# purpose with or without fee is hereby granted, provided that the above
+# copyright notice and this permission notice appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+#
+
+irccd_define_test(
+    NAME jsapi-file
+    SOURCES main.cpp
+    LIBRARIES libirccd-js
+)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/src/libirccd-js/jsapi-file/main.cpp	Fri Dec 15 15:37:58 2017 +0100
@@ -0,0 +1,308 @@
+/*
+ * main.cpp -- test Irccd.File API
+ *
+ * Copyright (c) 2013-2017 David Demelier <markand@malikania.fr>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#define BOOST_TEST_MODULE "File Javascript API"
+#include <boost/test/unit_test.hpp>
+
+#include <fstream>
+
+#include <irccd/js/duktape_vector.hpp>
+#include <irccd/js/file_jsapi.hpp>
+
+#include <irccd/test/js_test.hpp>
+
+namespace irccd {
+
+BOOST_FIXTURE_TEST_SUITE(file_jsapi_suite, js_test<file_jsapi>)
+
+BOOST_AUTO_TEST_CASE(function_basename)
+{
+    if (duk_peval_string(plugin_->context(), "result = Irccd.File.basename('/usr/local/etc/irccd.conf');"))
+        throw dukx_stack(plugin_->context(), -1);
+
+    BOOST_TEST(duk_get_global_string(plugin_->context(), "result"));
+    BOOST_TEST("irccd.conf" == duk_get_string(plugin_->context(), -1));
+}
+
+BOOST_AUTO_TEST_CASE(function_dirname)
+{
+    if (duk_peval_string(plugin_->context(), "result = Irccd.File.dirname('/usr/local/etc/irccd.conf');"))
+        throw dukx_stack(plugin_->context(), -1);
+
+    BOOST_TEST(duk_get_global_string(plugin_->context(), "result"));
+    BOOST_TEST("/usr/local/etc" == duk_get_string(plugin_->context(), -1));
+}
+
+BOOST_AUTO_TEST_CASE(function_exists)
+{
+    if (duk_peval_string(plugin_->context(), "result = Irccd.File.exists(CMAKE_SOURCE_DIR + '/tests/root/file-1.txt')"))
+        throw dukx_stack(plugin_->context(), -1);
+
+    BOOST_TEST(duk_get_global_string(plugin_->context(), "result"));
+    BOOST_TEST(duk_get_boolean(plugin_->context(), -1));
+}
+
+BOOST_AUTO_TEST_CASE(function_exists2)
+{
+    if (duk_peval_string(plugin_->context(), "result = Irccd.File.exists('file_which_does_not_exist.txt')"))
+        throw dukx_stack(plugin_->context(), -1);
+
+    BOOST_TEST(duk_get_global_string(plugin_->context(), "result"));
+    BOOST_TEST(!duk_get_boolean(plugin_->context(), -1));
+}
+
+BOOST_AUTO_TEST_CASE(function_remove)
+{
+    // First create a dummy file
+    std::ofstream("test-js-fs.remove");
+
+    if (duk_peval_string(plugin_->context(), "Irccd.File.remove('test-js-fs.remove');") != 0)
+        throw dukx_stack(plugin_->context(), -1);
+
+    std::ifstream in("test-js-fs.remove");
+
+    BOOST_TEST(!in.is_open());
+}
+
+BOOST_AUTO_TEST_CASE(method_basename)
+{
+    auto ret = duk_peval_string(plugin_->context(),
+        "f = new Irccd.File(CMAKE_SOURCE_DIR + '/tests/root/file-1.txt', 'r');"
+        "result = f.basename();"
+    );
+
+    if (ret != 0)
+        throw dukx_stack(plugin_->context(), -1);
+
+    BOOST_TEST(duk_get_global_string(plugin_->context(), "result"));
+    BOOST_TEST("file-1.txt" == duk_get_string(plugin_->context(), -1));
+}
+
+BOOST_AUTO_TEST_CASE(method_basename_closed)
+{
+    auto ret = duk_peval_string(plugin_->context(),
+        "f = new Irccd.File(CMAKE_SOURCE_DIR + '/tests/root/file-1.txt', 'r');"
+        "f.close();"
+        "result = f.basename();"
+    );
+
+    if (ret != 0)
+        throw dukx_stack(plugin_->context(), -1);
+
+    BOOST_TEST(duk_get_global_string(plugin_->context(), "result"));
+    BOOST_TEST("file-1.txt" == duk_get_string(plugin_->context(), -1));
+}
+
+BOOST_AUTO_TEST_CASE(method_dirname)
+{
+    auto ret = duk_peval_string(plugin_->context(),
+        "f = new Irccd.File(CMAKE_SOURCE_DIR + '/tests/root/file-1.txt', 'r');"
+        "result = f.dirname();"
+    );
+
+    if (ret != 0)
+        throw dukx_stack(plugin_->context(), -1);
+
+    BOOST_TEST(duk_get_global_string(plugin_->context(), "result"));
+    BOOST_TEST(CMAKE_SOURCE_DIR "/tests/root" == duk_get_string(plugin_->context(), -1));
+}
+
+BOOST_AUTO_TEST_CASE(method_dirname_closed)
+{
+    auto ret = duk_peval_string(plugin_->context(),
+        "f = new Irccd.File(CMAKE_SOURCE_DIR + '/tests/root/file-1.txt', 'r');"
+        "f.close();"
+        "result = f.dirname();"
+    );
+
+    if (ret != 0)
+        throw dukx_stack(plugin_->context(), -1);
+
+    BOOST_TEST(duk_get_global_string(plugin_->context(), "result"));
+    BOOST_TEST(CMAKE_SOURCE_DIR "/tests/root" == duk_get_string(plugin_->context(), -1));
+}
+
+BOOST_AUTO_TEST_CASE(method_lines)
+{
+    auto ret = duk_peval_string(plugin_->context(),
+        "result = new Irccd.File(CMAKE_SOURCE_DIR + '/tests/root/lines.txt', 'r').lines();"
+    );
+
+    if (ret != 0)
+        throw dukx_stack(plugin_->context(), -1);
+
+    std::vector<std::string> expected{"a", "b", "c"};
+
+    BOOST_TEST(duk_get_global_string(plugin_->context(), "result"));
+    BOOST_TEST(expected == dukx_get<std::vector<std::string>>(plugin_->context(), -1));
+}
+
+BOOST_AUTO_TEST_CASE(method_seek1)
+{
+    auto ret = duk_peval_string(plugin_->context(),
+        "f = new Irccd.File(CMAKE_SOURCE_DIR + '/tests/root/file-1.txt', 'r');"
+        "f.seek(Irccd.File.SeekSet, 6);"
+        "result = f.read(1);"
+    );
+
+    if (ret != 0)
+        throw dukx_stack(plugin_->context(), -1);
+
+    BOOST_TEST(duk_get_global_string(plugin_->context(), "result"));
+    BOOST_TEST(".", dukx_get<std::string>(plugin_->context(), -1));
+}
+
+BOOST_AUTO_TEST_CASE(method_seek1_closed)
+{
+    auto ret = duk_peval_string(plugin_->context(),
+        "f = new Irccd.File(CMAKE_SOURCE_DIR + '/tests/root/file-1.txt', 'r');"
+        "f.close();"
+        "f.seek(Irccd.File.SeekSet, 4);"
+        "result = f.read(1);"
+        "result = typeof (result) === \"undefined\";"
+    );
+
+    if (ret != 0)
+        throw dukx_stack(plugin_->context(), -1);
+
+    BOOST_TEST(duk_get_global_string(plugin_->context(), "result"));
+    BOOST_TEST(duk_get_boolean(plugin_->context(), -1));
+}
+
+BOOST_AUTO_TEST_CASE(method_seek2)
+{
+    auto ret = duk_peval_string(plugin_->context(),
+        "f = new Irccd.File(CMAKE_SOURCE_DIR + '/tests/root/file-1.txt', 'r');"
+        "f.seek(Irccd.File.SeekSet, 2);"
+        "f.seek(Irccd.File.SeekCur, 4);"
+        "result = f.read(1);"
+    );
+
+    if (ret != 0)
+        throw dukx_stack(plugin_->context(), -1);
+
+    BOOST_TEST(duk_get_global_string(plugin_->context(), "result"));
+    BOOST_TEST("." == dukx_get<std::string>(plugin_->context(), -1));
+}
+
+BOOST_AUTO_TEST_CASE(method_seek2c_losed)
+{
+    auto ret = duk_peval_string(plugin_->context(),
+        "f = new Irccd.File(CMAKE_SOURCE_DIR + '/tests/root/file-1.txt', 'r');"
+        "f.close();"
+        "f.seek(Irccd.File.SeekSet, 2);"
+        "f.seek(Irccd.File.SeekCur, 2);"
+        "result = f.read(1);"
+        "result = typeof (result) === \"undefined\";"
+    );
+
+    if (ret != 0)
+        throw dukx_stack(plugin_->context(), -1);
+
+    BOOST_TEST(duk_get_global_string(plugin_->context(), "result"));
+    BOOST_TEST(duk_get_boolean(plugin_->context(), -1));
+}
+
+BOOST_AUTO_TEST_CASE(method_seek3)
+{
+    auto ret = duk_peval_string(plugin_->context(),
+        "f = new Irccd.File(CMAKE_SOURCE_DIR + '/tests/root/file-1.txt', 'r');"
+        "f.seek(Irccd.File.SeekEnd, -2);"
+        "result = f.read(1);"
+    );
+
+    if (ret != 0)
+        throw dukx_stack(plugin_->context(), -1);
+
+    BOOST_TEST(duk_get_global_string(plugin_->context(), "result"));
+    BOOST_TEST("t" == duk_get_string(plugin_->context(), -1));
+}
+
+BOOST_AUTO_TEST_CASE(method_seek3_closed)
+{
+    auto ret = duk_peval_string(plugin_->context(),
+        "f = new Irccd.File(CMAKE_SOURCE_DIR + '/tests/root/file-1.txt', 'r');"
+        "f.close();"
+        "f.seek(Irccd.File.SeekEnd, -2);"
+        "result = f.read(1);"
+        "result = typeof (result) === \"undefined\";"
+    );
+
+    if (ret != 0)
+        throw dukx_stack(plugin_->context(), -1);
+
+    BOOST_TEST(duk_get_global_string(plugin_->context(), "result"));
+    BOOST_TEST(duk_get_boolean(plugin_->context(), -1));
+}
+
+BOOST_AUTO_TEST_CASE(method_read1)
+{
+    auto ret = duk_peval_string(plugin_->context(),
+        "f = new Irccd.File(CMAKE_SOURCE_DIR + '/tests/root/file-1.txt', 'r');"
+        "result = f.read();"
+    );
+
+    if (ret != 0)
+        throw dukx_stack(plugin_->context(), -1);
+
+    BOOST_TEST(duk_get_global_string(plugin_->context(), "result"));
+    BOOST_TEST("file-1.txt\n" == duk_get_string(plugin_->context(), -1));
+}
+
+BOOST_AUTO_TEST_CASE(method_readline)
+{
+    auto ret = duk_peval_string(plugin_->context(),
+        "result = [];"
+        "f = new Irccd.File(CMAKE_SOURCE_DIR + '/tests/root/lines.txt', 'r');"
+        "for (var s; s = f.readline(); ) {"
+        "  result.push(s);"
+        "}"
+    );
+
+    if (ret != 0)
+        throw dukx_stack(plugin_->context(), -1);
+
+    std::vector<std::string> expected{"a", "b", "c"};
+
+    BOOST_TEST(duk_get_global_string(plugin_->context(), "result"));
+    BOOST_TEST(expected == dukx_get<std::vector<std::string>>(plugin_->context(), -1));
+}
+
+BOOST_AUTO_TEST_CASE(method_readline_closed)
+{
+    auto ret = duk_peval_string(plugin_->context(),
+        "result = [];"
+        "f = new Irccd.File(CMAKE_SOURCE_DIR + '/tests/root/lines.txt', 'r');"
+        "f.close();"
+        "for (var s; s = f.readline(); ) {"
+        "  result.push(s);"
+        "}"
+    );
+
+    if (ret != 0)
+        throw dukx_stack(plugin_->context(), -1);
+
+    std::vector<std::string> expected;
+
+    BOOST_TEST(duk_get_global_string(plugin_->context(), "result"));
+    BOOST_TEST(expected == dukx_get<std::vector<std::string>>(plugin_->context(), -1));
+}
+
+BOOST_AUTO_TEST_SUITE_END()
+
+} // !irccd
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/src/libirccd-js/jsapi-irccd/CMakeLists.txt	Fri Dec 15 15:37:58 2017 +0100
@@ -0,0 +1,23 @@
+#
+# CMakeLists.txt -- CMake build system for irccd
+#
+# Copyright (c) 2013-2017 David Demelier <markand@malikania.fr>
+#
+# Permission to use, copy, modify, and/or distribute this software for any
+# purpose with or without fee is hereby granted, provided that the above
+# copyright notice and this permission notice appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+#
+
+irccd_define_test(
+    NAME jsapi-irccd
+    SOURCES main.cpp
+    LIBRARIES libirccd-js
+)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/src/libirccd-js/jsapi-irccd/main.cpp	Fri Dec 15 15:37:58 2017 +0100
@@ -0,0 +1,115 @@
+/*
+ * main.cpp -- test Irccd API
+ *
+ * Copyright (c) 2013-2017 David Demelier <markand@malikania.fr>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#define BOOST_TEST_MODULE "Irccd Javascript API"
+#include <boost/test/unit_test.hpp>
+
+#include <irccd/test/js_test.hpp>
+
+namespace irccd {
+
+BOOST_FIXTURE_TEST_SUITE(irccd_jsapi_suite, js_test<irccd_jsapi>)
+
+BOOST_AUTO_TEST_CASE(version)
+{
+    auto ret = duk_peval_string(plugin_->context(),
+        "major = Irccd.version.major;"
+        "minor = Irccd.version.minor;"
+        "patch = Irccd.version.patch;"
+    );
+
+    if (ret != 0)
+        throw dukx_stack(plugin_->context(), -1);
+
+    BOOST_TEST(duk_get_global_string(plugin_->context(), "major"));
+    BOOST_TEST(IRCCD_VERSION_MAJOR == duk_get_int(plugin_->context(), -1));
+    BOOST_TEST(duk_get_global_string(plugin_->context(), "minor"));
+    BOOST_TEST(IRCCD_VERSION_MINOR == duk_get_int(plugin_->context(), -1));
+    BOOST_TEST(duk_get_global_string(plugin_->context(), "patch"));
+    BOOST_TEST(IRCCD_VERSION_PATCH == duk_get_int(plugin_->context(), -1));
+}
+
+BOOST_AUTO_TEST_CASE(from_javascript)
+{
+    auto ret = duk_peval_string(plugin_->context(),
+        "try {"
+        "  throw new Irccd.SystemError(1, 'test');"
+        "} catch (e) {"
+        "  errno = e.errno;"
+        "  name = e.name;"
+        "  message = e.message;"
+        "  v1 = (e instanceof Error);"
+        "  v2 = (e instanceof Irccd.SystemError);"
+        "}"
+    );
+
+    if (ret != 0)
+        throw dukx_stack(plugin_->context(), -1);
+
+    BOOST_TEST(duk_get_global_string(plugin_->context(), "errno"));
+    BOOST_TEST(1 == duk_get_int(plugin_->context(), -1));
+    BOOST_TEST(duk_get_global_string(plugin_->context(), "name"));
+    BOOST_TEST("SystemError" == duk_get_string(plugin_->context(), -1));
+    BOOST_TEST(duk_get_global_string(plugin_->context(), "message"));
+    BOOST_TEST("test" == duk_get_string(plugin_->context(), -1));
+    BOOST_TEST(duk_get_global_string(plugin_->context(), "v1"));
+    BOOST_TEST(duk_get_boolean(plugin_->context(), -1));
+    BOOST_TEST(duk_get_global_string(plugin_->context(), "v2"));
+    BOOST_TEST(duk_get_boolean(plugin_->context(), -1));
+}
+
+BOOST_AUTO_TEST_CASE(from_native)
+{
+    duk_push_c_function(plugin_->context(), [] (duk_context *ctx) -> duk_ret_t {
+        dukx_throw(ctx, system_error(EINVAL, "hey"));
+
+        return 0;
+    }, 0);
+
+    duk_put_global_string(plugin_->context(), "f");
+
+    auto ret = duk_peval_string(plugin_->context(),
+        "try {"
+        "  f();"
+        "} catch (e) {"
+        "  errno = e.errno;"
+        "  name = e.name;"
+        "  message = e.message;"
+        "  v1 = (e instanceof Error);"
+        "  v2 = (e instanceof Irccd.SystemError);"
+        "}"
+    );
+
+    if (ret != 0)
+        throw dukx_stack(plugin_->context(), -1);
+
+    BOOST_TEST(duk_get_global_string(plugin_->context(), "errno"));
+    BOOST_TEST(EINVAL == duk_get_int(plugin_->context(), -1));
+    BOOST_TEST(duk_get_global_string(plugin_->context(), "name"));
+    BOOST_TEST("SystemError" == duk_get_string(plugin_->context(), -1));
+    BOOST_TEST(duk_get_global_string(plugin_->context(), "message"));
+    BOOST_TEST("hey" == duk_get_string(plugin_->context(), -1));
+    BOOST_TEST(duk_get_global_string(plugin_->context(), "v1"));
+    BOOST_TEST(duk_get_boolean(plugin_->context(), -1));
+    BOOST_TEST(duk_get_global_string(plugin_->context(), "v2"));
+    BOOST_TEST(duk_get_boolean(plugin_->context(), -1));
+}
+
+BOOST_AUTO_TEST_SUITE_END()
+
+} // !irccd
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/src/libirccd-js/jsapi-logger/CMakeLists.txt	Fri Dec 15 15:37:58 2017 +0100
@@ -0,0 +1,23 @@
+#
+# CMakeLists.txt -- CMake build system for irccd
+#
+# Copyright (c) 2013-2017 David Demelier <markand@malikania.fr>
+#
+# Permission to use, copy, modify, and/or distribute this software for any
+# purpose with or without fee is hereby granted, provided that the above
+# copyright notice and this permission notice appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+#
+
+irccd_define_test(
+    NAME jsapi-logger
+    SOURCES main.cpp
+    LIBRARIES libirccd-js
+)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/src/libirccd-js/jsapi-logger/main.cpp	Fri Dec 15 15:37:58 2017 +0100
@@ -0,0 +1,102 @@
+/*
+ * main.cpp -- test Irccd.Logger API
+ *
+ * Copyright (c) 2013-2017 David Demelier <markand@malikania.fr>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#define BOOST_TEST_MODULE "Logger Javascript API"
+#include <boost/test/unit_test.hpp>
+
+#include <irccd/daemon/logger.hpp>
+
+#include <irccd/js/logger_jsapi.hpp>
+#include <irccd/js/plugin_jsapi.hpp>
+
+#include <irccd/test/js_test.hpp>
+
+namespace irccd {
+
+class logger_test : public js_test<logger_jsapi, plugin_jsapi> {
+protected:
+    std::string line_info;
+    std::string line_warning;
+    std::string line_debug;
+
+    class my_logger : public logger {
+    private:
+        logger_test& test_;
+
+    public:
+        inline my_logger(logger_test& test) noexcept
+            : test_(test)
+        {
+        }
+
+        void write_info(const std::string& line) override
+        {
+            test_.line_info = line;
+        }
+
+        void write_warning(const std::string& line) override
+        {
+            test_.line_warning = line;
+        }
+
+        void write_debug(const std::string& line) override
+        {
+            test_.line_debug = line;
+        }
+    };
+
+    logger_test()
+    {
+        irccd_.set_log(std::make_unique<my_logger>(*this));
+        irccd_.log().set_verbose(true);
+    }
+};
+
+BOOST_FIXTURE_TEST_SUITE(logger_jsapi_suite, logger_test)
+
+BOOST_AUTO_TEST_CASE(info)
+{
+    if (duk_peval_string(plugin_->context(), "Irccd.Logger.info(\"hello!\");") != 0)
+        throw dukx_stack(plugin_->context(), -1);
+
+    BOOST_TEST("plugin test: hello!" == line_info);
+}
+
+BOOST_AUTO_TEST_CASE(warning)
+{
+    if (duk_peval_string(plugin_->context(), "Irccd.Logger.warning(\"FAIL!\");") != 0)
+        throw dukx_stack(plugin_->context(), -1);
+
+    BOOST_TEST("plugin test: FAIL!" == line_warning);
+}
+
+#if !defined(NDEBUG)
+
+BOOST_AUTO_TEST_CASE(debug)
+{
+    if (duk_peval_string(plugin_->context(), "Irccd.Logger.debug(\"starting\");") != 0)
+        throw dukx_stack(plugin_->context(), -1);
+
+    BOOST_TEST("plugin test: starting" == line_debug);
+}
+
+#endif
+
+BOOST_AUTO_TEST_SUITE_END()
+
+} // !irccd
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/src/libirccd-js/jsapi-system/CMakeLists.txt	Fri Dec 15 15:37:58 2017 +0100
@@ -0,0 +1,24 @@
+#
+# CMakeLists.txt -- CMake build system for irccd
+#
+# Copyright (c) 2013-2017 David Demelier <markand@malikania.fr>
+#
+# Permission to use, copy, modify, and/or distribute this software for any
+# purpose with or without fee is hereby granted, provided that the above
+# copyright notice and this permission notice appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+#
+
+irccd_define_test(
+    NAME jsapi-system
+    SOURCES main.cpp
+    LIBRARIES libirccd-js
+    FLAGS IRCCD_EXECUTABLE=\"$<TARGET_FILE:irccd>\"
+)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/src/libirccd-js/jsapi-system/main.cpp	Fri Dec 15 15:37:58 2017 +0100
@@ -0,0 +1,65 @@
+/*
+ * main.cpp -- test Irccd.System API
+ *
+ * Copyright (c) 2013-2017 David Demelier <markand@malikania.fr>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#define BOOST_TEST_MODULE "System Javascript API"
+#include <boost/test/unit_test.hpp>
+
+#include <irccd/system.hpp>
+
+#include <irccd/daemon/irccd.hpp>
+
+#include <irccd/js/file_jsapi.hpp>
+#include <irccd/js/system_jsapi.hpp>
+
+#include <irccd/test/js_test.hpp>
+
+namespace irccd {
+
+using fixture = js_test<file_jsapi, system_jsapi>;
+
+BOOST_FIXTURE_TEST_SUITE(system_jsapi_suite, fixture)
+
+BOOST_AUTO_TEST_CASE(home)
+{
+    duk_peval_string_noresult(plugin_->context(), "result = Irccd.System.home();");
+
+    BOOST_TEST(duk_get_global_string(plugin_->context(),"result"));
+    BOOST_TEST(duk_get_string(plugin_->context(), -1) == sys::home());
+}
+
+#if defined(HAVE_POPEN)
+
+BOOST_AUTO_TEST_CASE(popen)
+{
+    auto ret = duk_peval_string(plugin_->context(),
+        "f = Irccd.System.popen(\"" IRCCD_EXECUTABLE " --version\", \"r\");"
+        "r = f.readline();"
+    );
+
+    if (ret != 0)
+        throw dukx_stack(plugin_->context(), -1);
+
+    BOOST_TEST(duk_get_global_string(plugin_->context(), "r"));
+    BOOST_TEST(duk_get_string(plugin_->context(), -1) == IRCCD_VERSION);
+}
+
+#endif
+
+BOOST_AUTO_TEST_SUITE_END()
+
+} // !irccd
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/src/libirccd-js/jsapi-timer/CMakeLists.txt	Fri Dec 15 15:37:58 2017 +0100
@@ -0,0 +1,23 @@
+#
+# CMakeLists.txt -- CMake build system for irccd
+#
+# Copyright (c) 2013-2017 David Demelier <markand@malikania.fr>
+#
+# Permission to use, copy, modify, and/or distribute this software for any
+# purpose with or without fee is hereby granted, provided that the above
+# copyright notice and this permission notice appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+#
+
+irccd_define_test(
+    NAME jsapi-timer
+    SOURCES main.cpp
+    LIBRARIES libirccd-js
+)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/src/libirccd-js/jsapi-timer/main.cpp	Fri Dec 15 15:37:58 2017 +0100
@@ -0,0 +1,90 @@
+/*
+ * main.cpp -- test Irccd.Timer API
+ *
+ * Copyright (c) 2013-2017 David Demelier <markand@malikania.fr>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#define BOOST_TEST_MODULE "Timer Javascript API"
+#include <boost/test/unit_test.hpp>
+#include <boost/timer/timer.hpp>
+
+#include <irccd/js/plugin_jsapi.hpp>
+#include <irccd/js/timer_jsapi.hpp>
+
+#include <irccd/test/js_test.hpp>
+
+#include <irccd/js/logger_jsapi.hpp>
+
+namespace irccd {
+
+namespace {
+
+class js_timer_test : public js_test<plugin_jsapi, timer_jsapi> {
+public:
+    js_timer_test()
+        : js_test(CMAKE_CURRENT_SOURCE_DIR "/timer.js")
+    {
+    }
+
+    void set_type(const std::string& name)
+    {
+        duk_get_global_string(plugin_->context(), "Irccd");
+        duk_get_prop_string(plugin_->context(), -1, "Timer");
+        duk_get_prop_string(plugin_->context(), -1, name.c_str());
+        duk_put_global_string(plugin_->context(), "type");
+        duk_pop_n(plugin_->context(), 2);
+
+        plugin_->open();
+        plugin_->on_load(irccd_);
+    }
+};
+
+} // !namespace
+
+BOOST_FIXTURE_TEST_SUITE(js_timer_test_suite, js_timer_test)
+
+BOOST_AUTO_TEST_CASE(single)
+{
+    boost::timer::cpu_timer timer;
+
+    set_type("Single");
+
+    while (timer.elapsed().wall / 1000000LL < 3000) {
+        service_.reset();
+        service_.poll();
+    }
+
+    BOOST_TEST(duk_get_global_string(plugin_->context(), "count"));
+    BOOST_TEST(duk_get_int(plugin_->context(), -1) == 1);
+}
+
+BOOST_AUTO_TEST_CASE(repeat)
+{
+    boost::timer::cpu_timer timer;
+
+    set_type("Repeat");
+
+    while (timer.elapsed().wall / 1000000LL < 3000) {
+        service_.reset();
+        service_.poll();
+    }
+
+    BOOST_TEST(duk_get_global_string(plugin_->context(), "count"));
+    BOOST_TEST(duk_get_int(plugin_->context(), -1) >= 5);
+}
+
+BOOST_AUTO_TEST_SUITE_END()
+
+} // !irccd
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/src/libirccd-js/jsapi-timer/timer.js	Fri Dec 15 15:37:58 2017 +0100
@@ -0,0 +1,13 @@
+count = 0;
+
+function onLoad()
+{
+    if (typeof (type) === "undefined")
+        throw Error("global timer type not defined");
+
+    t = new Irccd.Timer(type, 50, function () {
+        count += 1;
+    });
+
+    t.start();
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/src/libirccd-js/jsapi-unicode/CMakeLists.txt	Fri Dec 15 15:37:58 2017 +0100
@@ -0,0 +1,23 @@
+#
+# CMakeLists.txt -- CMake build system for irccd
+#
+# Copyright (c) 2013-2017 David Demelier <markand@malikania.fr>
+#
+# Permission to use, copy, modify, and/or distribute this software for any
+# purpose with or without fee is hereby granted, provided that the above
+# copyright notice and this permission notice appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+#
+
+irccd_define_test(
+    NAME jsapi-unicode
+    SOURCES main.cpp
+    LIBRARIES libirccd-js
+)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/src/libirccd-js/jsapi-unicode/main.cpp	Fri Dec 15 15:37:58 2017 +0100
@@ -0,0 +1,69 @@
+/*
+ * main.cpp -- test Irccd.Unicode API
+ *
+ * Copyright (c) 2013-2017 David Demelier <markand@malikania.fr>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+/*
+ * /!\ Be sure that this file is kept saved in UTF-8 /!\
+ */
+
+#define BOOST_TEST_MODULE "Unicode Javascript API"
+#include <boost/test/unit_test.hpp>
+
+#include <irccd/js/unicode_jsapi.hpp>
+
+#include <irccd/test/js_test.hpp>
+
+namespace irccd {
+
+BOOST_FIXTURE_TEST_SUITE(unicode_jsapi_suite, js_test<unicode_jsapi>)
+
+BOOST_AUTO_TEST_CASE(is_letter)
+{
+    duk_peval_string_noresult(plugin_->context(), "result = Irccd.Unicode.isLetter(String('é').charCodeAt(0));");
+    BOOST_TEST(duk_get_global_string(plugin_->context(), "result"));
+    BOOST_TEST(duk_get_boolean(plugin_->context(), -1));
+
+    duk_peval_string_noresult(plugin_->context(), "result = Irccd.Unicode.isLetter(String('€').charCodeAt(0));");
+    BOOST_TEST(duk_get_global_string(plugin_->context(), "result"));
+    BOOST_TEST(!duk_get_boolean(plugin_->context(), -1));
+}
+
+BOOST_AUTO_TEST_CASE(is_lower)
+{
+    duk_peval_string_noresult(plugin_->context(), "result = Irccd.Unicode.isLower(String('é').charCodeAt(0));");
+    BOOST_TEST(duk_get_global_string(plugin_->context(), "result"));
+    BOOST_TEST(duk_get_boolean(plugin_->context(), -1));
+
+    duk_peval_string_noresult(plugin_->context(), "result = Irccd.Unicode.isLower(String('É').charCodeAt(0));");
+    BOOST_TEST(duk_get_global_string(plugin_->context(), "result"));
+    BOOST_TEST(!duk_get_boolean(plugin_->context(), -1));
+}
+
+BOOST_AUTO_TEST_CASE(is_upper)
+{
+    duk_peval_string_noresult(plugin_->context(), "result = Irccd.Unicode.isUpper(String('É').charCodeAt(0));");
+    BOOST_TEST(duk_get_global_string(plugin_->context(), "result"));
+    BOOST_TEST(duk_get_boolean(plugin_->context(), -1));
+
+    duk_peval_string_noresult(plugin_->context(), "result = Irccd.Unicode.isUpper(String('é').charCodeAt(0));");
+    BOOST_TEST(duk_get_global_string(plugin_->context(), "result"));
+    BOOST_TEST(!duk_get_boolean(plugin_->context(), -1));
+}
+
+BOOST_AUTO_TEST_SUITE_END()
+
+} // !irccd
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/src/libirccd-js/jsapi-util/CMakeLists.txt	Fri Dec 15 15:37:58 2017 +0100
@@ -0,0 +1,23 @@
+#
+# CMakeLists.txt -- CMake build system for irccd
+#
+# Copyright (c) 2013-2017 David Demelier <markand@malikania.fr>
+#
+# Permission to use, copy, modify, and/or distribute this software for any
+# purpose with or without fee is hereby granted, provided that the above
+# copyright notice and this permission notice appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+#
+
+irccd_define_test(
+    NAME jsapi-util
+    SOURCES main.cpp
+    LIBRARIES libirccd-js
+)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/src/libirccd-js/jsapi-util/main.cpp	Fri Dec 15 15:37:58 2017 +0100
@@ -0,0 +1,260 @@
+/*
+ * main.cpp -- test Irccd.Util API
+ *
+ * Copyright (c) 2013-2017 David Demelier <markand@malikania.fr>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#define BOOST_TEST_MODULE "Unicode Javascript API"
+#include <boost/test/unit_test.hpp>
+
+#include <irccd/js/util_jsapi.hpp>
+
+#include <irccd/test/js_test.hpp>
+
+namespace irccd {
+
+BOOST_FIXTURE_TEST_SUITE(util_jsapi_suite, js_test<util_jsapi>)
+
+/*
+ * Irccd.Util misc.
+ * ------------------------------------------------------------------
+ */
+
+BOOST_AUTO_TEST_CASE(format_simple)
+{
+    auto ret = duk_peval_string(plugin_->context(),
+        "result = Irccd.Util.format(\"#{target}\", { target: \"markand\" })"
+    );
+
+    if (ret != 0)
+        throw dukx_stack(plugin_->context(), -1);
+
+    BOOST_TEST(duk_get_global_string(plugin_->context(), "result"));
+    BOOST_TEST(duk_get_string(plugin_->context(), -1) == "markand");
+}
+
+BOOST_AUTO_TEST_CASE(splituser)
+{
+    if (duk_peval_string(plugin_->context(), "result = Irccd.Util.splituser(\"user!~user@hyper/super/host\");") != 0)
+        throw dukx_stack(plugin_->context(), -1);
+
+    BOOST_TEST(duk_get_global_string(plugin_->context(), "result"));
+    BOOST_TEST(duk_get_string(plugin_->context(), -1) == "user");
+}
+
+BOOST_AUTO_TEST_CASE(splithost)
+{
+    if (duk_peval_string(plugin_->context(), "result = Irccd.Util.splithost(\"user!~user@hyper/super/host\");") != 0)
+        throw dukx_stack(plugin_->context(), -1);
+
+    BOOST_TEST(duk_get_global_string(plugin_->context(), "result"));
+    BOOST_TEST(duk_get_string(plugin_->context(), -1) == "~user@hyper/super/host");
+}
+
+/*
+ * Irccd.Util.cut.
+ * ------------------------------------------------------------------
+ */
+
+BOOST_AUTO_TEST_CASE(cut_string_simple)
+{
+    auto ret = duk_peval_string(plugin_->context(),
+        "lines = Irccd.Util.cut('hello world');\n"
+        "line0 = lines[0];\n"
+    );
+
+    if (ret != 0)
+        throw dukx_stack(plugin_->context(), -1);
+
+    BOOST_TEST(duk_get_global_string(plugin_->context(), "line0"));
+    BOOST_TEST(duk_get_string(plugin_->context(), -1) == "hello world");
+}
+
+BOOST_AUTO_TEST_CASE(cut_string_double)
+{
+    auto ret = duk_peval_string(plugin_->context(),
+        "lines = Irccd.Util.cut('hello world', 5);\n"
+        "line0 = lines[0];\n"
+        "line1 = lines[1];\n"
+    );
+
+    if (ret != 0)
+        throw dukx_stack(plugin_->context(), -1);
+
+    BOOST_TEST(duk_get_global_string(plugin_->context(), "line0"));
+    BOOST_TEST(duk_get_string(plugin_->context(), -1) == "hello");
+    BOOST_TEST(duk_get_global_string(plugin_->context(), "line1"));
+    BOOST_TEST(duk_get_string(plugin_->context(), -1) == "world");
+}
+
+BOOST_AUTO_TEST_CASE(cut_string_dirty)
+{
+    auto ret = duk_peval_string(plugin_->context(),
+        "lines = Irccd.Util.cut('     hello    world     ', 5);\n"
+        "line0 = lines[0];\n"
+        "line1 = lines[1];\n"
+    );
+
+    if (ret != 0)
+        throw dukx_stack(plugin_->context(), -1);
+
+    BOOST_TEST(duk_get_global_string(plugin_->context(), "line0"));
+    BOOST_TEST(duk_get_string(plugin_->context(), -1) == "hello");
+    BOOST_TEST(duk_get_global_string(plugin_->context(), "line1"));
+    BOOST_TEST(duk_get_string(plugin_->context(), -1) == "world");
+}
+
+BOOST_AUTO_TEST_CASE(cut_string_too_much_lines)
+{
+    auto ret = duk_peval_string(plugin_->context(),
+        "lines = Irccd.Util.cut('abc def ghi jkl', 3, 3);\n"
+    );
+
+    if (ret != 0)
+        throw dukx_stack(plugin_->context(), -1);
+
+    BOOST_TEST(duk_get_global_string(plugin_->context(), "lines"));
+    BOOST_TEST(duk_is_undefined(plugin_->context(), -1));
+}
+
+BOOST_AUTO_TEST_CASE(cut_string_token_too_big)
+{
+    auto ret = duk_peval_string(plugin_->context(),
+        "try {\n"
+        "  lines = Irccd.Util.cut('hello world', 3);\n"
+        "} catch (e) {\n"
+        "  name = e.name;\n"
+        "  message = e.message;\n"
+        "}\n"
+    );
+
+    if (ret != 0)
+        throw dukx_stack(plugin_->context(), -1);
+
+    BOOST_TEST(duk_get_global_string(plugin_->context(), "name"));
+    BOOST_TEST(duk_get_string(plugin_->context(), -1) == "RangeError");
+    BOOST_TEST(duk_get_global_string(plugin_->context(), "message"));
+    BOOST_TEST(duk_get_string(plugin_->context(), -1) == "word 'hello' could not fit in maxc limit (3)");
+}
+
+BOOST_AUTO_TEST_CASE(cut_string_negative_maxc)
+{
+    auto ret = duk_peval_string(plugin_->context(),
+        "try {\n"
+        "  lines = Irccd.Util.cut('hello world', -3);\n"
+        "} catch (e) {\n"
+        "  name = e.name;\n"
+        "  message = e.message;\n"
+        "}\n"
+    );
+
+    if (ret != 0)
+        throw dukx_stack(plugin_->context(), -1);
+
+    BOOST_TEST(duk_get_global_string(plugin_->context(), "name"));
+    BOOST_TEST(duk_get_string(plugin_->context(), -1) == "RangeError");
+    BOOST_TEST(duk_get_global_string(plugin_->context(), "message"));
+    BOOST_TEST(duk_get_string(plugin_->context(), -1) == "argument 1 (maxc) must be positive");
+}
+
+BOOST_AUTO_TEST_CASE(cut_string_negative_maxl)
+{
+    auto ret = duk_peval_string(plugin_->context(),
+        "try {\n"
+        "  lines = Irccd.Util.cut('hello world', undefined, -1);\n"
+        "} catch (e) {\n"
+        "  name = e.name;\n"
+        "  message = e.message;\n"
+        "}\n"
+    );
+
+    if (ret != 0)
+        throw dukx_stack(plugin_->context(), -1);
+
+    BOOST_TEST(duk_get_global_string(plugin_->context(), "name"));
+    BOOST_TEST(duk_get_string(plugin_->context(), -1) == "RangeError");
+    BOOST_TEST(duk_get_global_string(plugin_->context(), "message"));
+    BOOST_TEST(duk_get_string(plugin_->context(), -1) == "argument 2 (maxl) must be positive");
+}
+
+BOOST_AUTO_TEST_CASE(cut_array_simple)
+{
+    auto ret = duk_peval_string(plugin_->context(),
+        "lines = Irccd.Util.cut([ 'hello', 'world' ]);\n"
+        "line0 = lines[0];\n"
+    );
+
+    if (ret != 0)
+        throw dukx_stack(plugin_->context(), -1);
+
+    BOOST_TEST(duk_get_global_string(plugin_->context(), "line0"));
+    BOOST_TEST(duk_get_string(plugin_->context(), -1) == "hello world");
+}
+
+BOOST_AUTO_TEST_CASE(cut_array_double)
+{
+    auto ret = duk_peval_string(plugin_->context(),
+        "lines = Irccd.Util.cut([ 'hello', 'world' ], 5);\n"
+        "line0 = lines[0];\n"
+        "line1 = lines[1];\n"
+    );
+
+    if (ret != 0)
+        throw dukx_stack(plugin_->context(), -1);
+
+    BOOST_TEST(duk_get_global_string(plugin_->context(), "line0"));
+    BOOST_TEST(duk_get_string(plugin_->context(), -1) == "hello");
+    BOOST_TEST(duk_get_global_string(plugin_->context(), "line1"));
+    BOOST_TEST(duk_get_string(plugin_->context(), -1) == "world");
+}
+
+BOOST_AUTO_TEST_CASE(cut_array_dirty)
+{
+    auto ret = duk_peval_string(plugin_->context(),
+        "lines = Irccd.Util.cut([ '   ', ' hello  ', '  world ', '    '], 5);\n"
+        "line0 = lines[0];\n"
+        "line1 = lines[1];\n"
+    );
+
+    if (ret != 0)
+        throw dukx_stack(plugin_->context(), -1);
+
+    BOOST_TEST(duk_get_global_string(plugin_->context(), "line0"));
+    BOOST_TEST(duk_get_string(plugin_->context(), -1) == "hello");
+    BOOST_TEST(duk_get_global_string(plugin_->context(), "line1"));
+    BOOST_TEST(duk_get_string(plugin_->context(), -1) == "world");
+}
+
+BOOST_AUTO_TEST_CASE(cut_invalid_data)
+{
+    auto ret = duk_peval_string(plugin_->context(),
+        "try {\n"
+        "  lines = Irccd.Util.cut(123);\n"
+        "} catch (e) {\n"
+        "  name = e.name;\n"
+        "  message = e.message;\n"
+        "}\n"
+    );
+
+    if (ret != 0)
+        throw dukx_stack(plugin_->context(), -1);
+
+    BOOST_TEST(duk_get_global_string(plugin_->context(), "name"));
+    BOOST_TEST(duk_get_string(plugin_->context(), -1) == "TypeError");
+}
+
+BOOST_AUTO_TEST_SUITE_END()
+
+} // !irccd
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/src/libirccd/CMakeLists.txt	Fri Dec 15 15:37:58 2017 +0100
@@ -0,0 +1,51 @@
+#
+# CMakeLists.txt -- CMake build system for irccd
+#
+# Copyright (c) 2013-2017 David Demelier <markand@malikania.fr>
+#
+# Permission to use, copy, modify, and/or distribute this software for any
+# purpose with or without fee is hereby granted, provided that the above
+# copyright notice and this permission notice appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+#
+
+add_subdirectory(command-plugin-config)
+add_subdirectory(command-plugin-info)
+add_subdirectory(command-plugin-list)
+add_subdirectory(command-plugin-load)
+add_subdirectory(command-plugin-reload)
+add_subdirectory(command-plugin-unload)
+add_subdirectory(command-rule-add)
+add_subdirectory(command-rule-edit)
+add_subdirectory(command-rule-info)
+add_subdirectory(command-rule-list)
+add_subdirectory(command-rule-move)
+add_subdirectory(command-rule-remove)
+add_subdirectory(command-server-connect)
+add_subdirectory(command-server-disconnect)
+add_subdirectory(command-server-info)
+add_subdirectory(command-server-invite)
+add_subdirectory(command-server-join)
+add_subdirectory(command-server-kick)
+add_subdirectory(command-server-list)
+add_subdirectory(command-server-me)
+add_subdirectory(command-server-message)
+add_subdirectory(command-server-mode)
+add_subdirectory(command-server-nick)
+add_subdirectory(command-server-notice)
+add_subdirectory(command-server-part)
+add_subdirectory(command-server-reconnect)
+add_subdirectory(command-server-topic)
+
+add_subdirectory(dynlib-plugin)
+add_subdirectory(irc)
+add_subdirectory(logger)
+add_subdirectory(rules)
+add_subdirectory(util)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/src/libirccd/command-plugin-config/CMakeLists.txt	Fri Dec 15 15:37:58 2017 +0100
@@ -0,0 +1,23 @@
+#
+# CMakeLists.txt -- CMake build system for irccd
+#
+# Copyright (c) 2013-2017 David Demelier <markand@malikania.fr>
+#
+# Permission to use, copy, modify, and/or distribute this software for any
+# purpose with or without fee is hereby granted, provided that the above
+# copyright notice and this permission notice appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+#
+
+irccd_define_test(
+    NAME command-plugin-config
+    SOURCES main.cpp
+    LIBRARIES libirccd libirccdctl
+)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/src/libirccd/command-plugin-config/main.cpp	Fri Dec 15 15:37:58 2017 +0100
@@ -0,0 +1,161 @@
+/*
+ * main.cpp -- test plugin-config remote command
+ *
+ * Copyright (c) 2013-2017 David Demelier <markand@malikania.fr>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#define BOOST_TEST_MODULE "plugin-config"
+#include <boost/test/unit_test.hpp>
+
+#include <irccd/daemon/command.hpp>
+#include <irccd/daemon/plugin_service.hpp>
+
+#include <irccd/test/command_test.hpp>
+
+namespace irccd {
+
+namespace {
+
+class custom_plugin : public plugin {
+public:
+    plugin_config config_;
+
+    custom_plugin(std::string name = "test")
+        : plugin(std::move(name), "")
+    {
+    }
+
+    plugin_config config() override
+    {
+        return config_;
+    }
+
+    void set_config(plugin_config config) override
+    {
+        config_ = std::move(config);
+    }
+};
+
+} // !namespace
+
+BOOST_FIXTURE_TEST_SUITE(plugin_config_test_suite, command_test<plugin_config_command>)
+
+BOOST_AUTO_TEST_CASE(set)
+{
+    daemon_->plugins().add(std::make_unique<custom_plugin>("test"));
+    ctl_->send({
+        { "command",    "plugin-config" },
+        { "plugin",     "test"          },
+        { "variable",   "verbosy"       },
+        { "value",      "falsy"         }
+    });
+
+    wait_for([&] {
+        return !daemon_->plugins().require("test")->config().empty();
+    });
+
+    auto config = daemon_->plugins().require("test")->config();
+
+    BOOST_TEST(!config.empty());
+    BOOST_TEST(config["verbosy"] == "falsy");
+}
+
+BOOST_AUTO_TEST_CASE(get)
+{
+    auto plugin = std::make_unique<custom_plugin>("test");
+    auto json = nlohmann::json();
+
+    plugin->set_config({
+        { "x1", "10" },
+        { "x2", "20" }
+    });
+
+    daemon_->plugins().add(std::move(plugin));
+    ctl_->send({
+        { "command", "plugin-config" },
+        { "plugin", "test" },
+        { "variable", "x1" }
+    });
+    ctl_->recv([&] (auto, auto message) {
+        json = std::move(message);
+    });
+
+    wait_for([&] {
+        return json.is_object();
+    });
+
+    BOOST_TEST(json.is_object());
+    BOOST_TEST(json["variables"]["x1"].get<std::string>() == "10");
+    BOOST_TEST(json["variables"]["x2"].is_null());
+}
+
+BOOST_AUTO_TEST_CASE(getall)
+{
+    auto plugin = std::make_unique<custom_plugin>("test");
+    auto json = nlohmann::json();
+
+    plugin->set_config({
+        { "x1", "10" },
+        { "x2", "20" }
+    });
+
+    daemon_->plugins().add(std::move(plugin));
+    ctl_->send({
+        { "command", "plugin-config" },
+        { "plugin", "test" }
+    });
+    ctl_->recv([&] (auto, auto message) {
+        json = std::move(message);
+    });
+
+    wait_for([&] {
+        return json.is_object();
+    });
+
+    BOOST_TEST(json.is_object());
+    BOOST_TEST(json["variables"]["x1"].get<std::string>() == "10");
+    BOOST_TEST(json["variables"]["x2"].get<std::string>() == "20");
+}
+
+BOOST_AUTO_TEST_SUITE(errors)
+
+BOOST_AUTO_TEST_CASE(not_found)
+{
+    boost::system::error_code result;
+    nlohmann::json message;
+
+    ctl_->send({
+        { "command",    "plugin-config" },
+        { "plugin",     "unknown"       }
+    });
+    ctl_->recv([&] (auto rresult, auto rmessage) {
+        result = rresult;
+        message = rmessage;
+    });
+
+    wait_for([&] {
+        return result;
+    });
+
+    BOOST_TEST(result == plugin_error::not_found);
+    BOOST_TEST(message["error"].template get<int>() == plugin_error::not_found);
+    BOOST_TEST(message["errorCategory"].template get<std::string>() == "plugin");
+}
+
+BOOST_AUTO_TEST_SUITE_END()
+
+BOOST_AUTO_TEST_SUITE_END()
+
+} // !irccd
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/src/libirccd/command-plugin-info/CMakeLists.txt	Fri Dec 15 15:37:58 2017 +0100
@@ -0,0 +1,23 @@
+#
+# CMakeLists.txt -- CMake build system for irccd
+#
+# Copyright (c) 2013-2017 David Demelier <markand@malikania.fr>
+#
+# Permission to use, copy, modify, and/or distribute this software for any
+# purpose with or without fee is hereby granted, provided that the above
+# copyright notice and this permission notice appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+#
+
+irccd_define_test(
+    NAME command-plugin-info
+    SOURCES main.cpp
+    LIBRARIES libirccd libirccdctl
+)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/src/libirccd/command-plugin-info/main.cpp	Fri Dec 15 15:37:58 2017 +0100
@@ -0,0 +1,90 @@
+/*
+ * main.cpp -- test plugin-info remote command
+ *
+ * Copyright (c) 2013-2017 David Demelier <markand@malikania.fr>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#define BOOST_TEST_MODULE "plugin-info"
+#include <boost/test/unit_test.hpp>
+
+#include <irccd/daemon/command.hpp>
+#include <irccd/daemon/plugin_service.hpp>
+
+#include <irccd/test/command_test.hpp>
+
+namespace irccd {
+
+BOOST_FIXTURE_TEST_SUITE(plugin_info_test_suite, command_test<plugin_info_command>)
+
+BOOST_AUTO_TEST_CASE(basic)
+{
+    auto plg = std::make_unique<plugin>("test", "");
+    auto response = nlohmann::json();
+
+    plg->set_author("Francis Beaugrand");
+    plg->set_license("GPL");
+    plg->set_summary("Completely useless plugin");
+    plg->set_version("0.0.0.0.0.0.0.0.1-beta5");
+
+    daemon_->plugins().add(std::move(plg));
+    ctl_->recv([&] (auto, auto msg) {
+        response = std::move(msg);
+    });
+    ctl_->send({
+        { "command",    "plugin-info"       },
+        { "plugin",     "test"              },
+    });
+
+    wait_for([&] () {
+        return response.is_object();
+    });
+
+    BOOST_TEST(response.is_object());
+    BOOST_TEST(response["author"].get<std::string>() == "Francis Beaugrand");
+    BOOST_TEST(response["license"].get<std::string>() == "GPL");
+    BOOST_TEST(response["summary"].get<std::string>() == "Completely useless plugin");
+    BOOST_TEST(response["version"].get<std::string>() == "0.0.0.0.0.0.0.0.1-beta5");
+}
+
+BOOST_AUTO_TEST_SUITE(errors)
+
+BOOST_AUTO_TEST_CASE(not_found)
+{
+    boost::system::error_code result;
+    nlohmann::json message;
+
+    ctl_->send({
+        { "command",    "plugin-info"   },
+        { "plugin",     "unknown"       }
+    });
+    ctl_->recv([&] (auto rresult, auto rmessage) {
+        result = rresult;
+        message = rmessage;
+    });
+
+    wait_for([&] {
+        return result;
+    });
+
+    BOOST_TEST(result == plugin_error::not_found);
+    BOOST_TEST(message["error"].template get<int>() == plugin_error::not_found);
+    BOOST_TEST(message["errorCategory"].template get<std::string>() == "plugin");
+}
+
+BOOST_AUTO_TEST_SUITE_END()
+
+BOOST_AUTO_TEST_SUITE_END()
+
+} // !irccd
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/src/libirccd/command-plugin-list/CMakeLists.txt	Fri Dec 15 15:37:58 2017 +0100
@@ -0,0 +1,23 @@
+#
+# CMakeLists.txt -- CMake build system for irccd
+#
+# Copyright (c) 2013-2017 David Demelier <markand@malikania.fr>
+#
+# Permission to use, copy, modify, and/or distribute this software for any
+# purpose with or without fee is hereby granted, provided that the above
+# copyright notice and this permission notice appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+#
+
+irccd_define_test(
+    NAME command-plugin-list
+    SOURCES main.cpp
+    LIBRARIES libirccd libirccdctl
+)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/src/libirccd/command-plugin-list/main.cpp	Fri Dec 15 15:37:58 2017 +0100
@@ -0,0 +1,60 @@
+/*
+ * main.cpp -- test plugin-list remote command
+ *
+ * Copyright (c) 2013-2017 David Demelier <markand@malikania.fr>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#define BOOST_TEST_MODULE "plugin-list"
+#include <boost/test/unit_test.hpp>
+
+#include <irccd/daemon/command.hpp>
+#include <irccd/daemon/plugin_service.hpp>
+
+#include <irccd/test/command_test.hpp>
+
+namespace irccd {
+
+class plugin_list_test : public command_test<plugin_list_command> {
+public:
+    plugin_list_test()
+    {
+        daemon_->plugins().add(std::make_unique<plugin>("t1", ""));
+        daemon_->plugins().add(std::make_unique<plugin>("t2", ""));
+    }
+};
+
+BOOST_FIXTURE_TEST_SUITE(plugin_list_test_suite, plugin_list_test)
+
+BOOST_AUTO_TEST_CASE(basic)
+{
+    auto response = nlohmann::json();
+
+    ctl_->send({{"command", "plugin-list"}});
+    ctl_->recv([&] (auto, auto message) {
+        response = message;
+    });
+
+    wait_for([&] () {
+        return response.is_object();
+    });
+
+    BOOST_TEST(response.is_object());
+    BOOST_TEST("t1", response["list"][0]);
+    BOOST_TEST("t2", response["list"][1]);
+}
+
+BOOST_AUTO_TEST_SUITE_END()
+
+} // !irccd
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/src/libirccd/command-plugin-load/CMakeLists.txt	Fri Dec 15 15:37:58 2017 +0100
@@ -0,0 +1,23 @@
+#
+# CMakeLists.txt -- CMake build system for irccd
+#
+# Copyright (c) 2013-2017 David Demelier <markand@malikania.fr>
+#
+# Permission to use, copy, modify, and/or distribute this software for any
+# purpose with or without fee is hereby granted, provided that the above
+# copyright notice and this permission notice appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+#
+
+irccd_define_test(
+    NAME command-plugin-load
+    SOURCES main.cpp
+    LIBRARIES libirccd libirccdctl
+)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/src/libirccd/command-plugin-load/main.cpp	Fri Dec 15 15:37:58 2017 +0100
@@ -0,0 +1,168 @@
+/*
+ * main.cpp -- test plugin-load remote command
+ *
+ * Copyright (c) 2013-2017 David Demelier <markand@malikania.fr>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#define BOOST_TEST_MODULE "plugin-load"
+#include <boost/test/unit_test.hpp>
+
+#include <irccd/daemon/command.hpp>
+#include <irccd/daemon/plugin_service.hpp>
+
+#include <irccd/test/command_test.hpp>
+
+namespace irccd {
+
+namespace {
+
+class custom_loader : public plugin_loader {
+public:
+    custom_loader()
+        : plugin_loader({}, {".none"})
+    {
+    }
+
+    std::shared_ptr<plugin> open(const std::string&,
+                                 const std::string&) noexcept override
+    {
+        return nullptr;
+    }
+
+    std::shared_ptr<plugin> find(const std::string& id) noexcept override
+    {
+        class broken : public plugin {
+        public:
+            using plugin::plugin;
+
+            void on_load(irccd&) override
+            {
+                throw std::runtime_error("broken");
+            }
+        };
+
+        /*
+         * The 'magic' plugin will be created for the unit tests, all other
+         * plugins will return null.
+         */
+        if (id == "magic")
+            return std::make_unique<plugin>(id, "");
+        if (id == "broken")
+            return std::make_unique<broken>(id, "");
+
+        return nullptr;
+    }
+};
+
+class plugin_load_test : public command_test<plugin_load_command> {
+public:
+    plugin_load_test()
+    {
+        daemon_->plugins().add_loader(std::make_unique<custom_loader>());
+        daemon_->plugins().add(std::make_unique<plugin>("already", ""));
+    }
+};
+
+} // !namespace
+
+BOOST_FIXTURE_TEST_SUITE(plugin_load_test_suite, plugin_load_test)
+
+BOOST_AUTO_TEST_CASE(basic)
+{
+    ctl_->send({
+        { "command",    "plugin-load"   },
+        { "plugin",     "magic"         }
+    });
+
+    wait_for([&] () {
+        return daemon_->plugins().has("magic");
+    });
+
+    BOOST_TEST(!daemon_->plugins().list().empty());
+    BOOST_TEST(daemon_->plugins().has("magic"));
+}
+
+BOOST_AUTO_TEST_SUITE(errors)
+
+BOOST_AUTO_TEST_CASE(not_found)
+{
+    boost::system::error_code result;
+
+    ctl_->send({
+        { "command",    "plugin-load"   },
+        { "plugin",     "unknown"       }
+    });
+    ctl_->recv([&] (auto code, auto) {
+        result = code;
+    });
+
+    wait_for([&] {
+        return result;
+    });
+
+    BOOST_TEST(result == plugin_error::not_found);
+}
+
+BOOST_AUTO_TEST_CASE(already_exists)
+{
+    boost::system::error_code result;
+    nlohmann::json message;
+
+    ctl_->send({
+        { "command",    "plugin-load"   },
+        { "plugin",     "already"       }
+    });
+    ctl_->recv([&] (auto rresult, auto rmessage) {
+        result = rresult;
+        message = rmessage;
+    });
+
+    wait_for([&] {
+        return result;
+    });
+
+    BOOST_TEST(result == plugin_error::already_exists);
+    BOOST_TEST(message["error"].template get<int>() == plugin_error::already_exists);
+    BOOST_TEST(message["errorCategory"].template get<std::string>() == "plugin");
+}
+
+BOOST_AUTO_TEST_CASE(exec_error)
+{
+    boost::system::error_code result;
+    nlohmann::json message;
+
+    ctl_->send({
+        { "command",    "plugin-load"   },
+        { "plugin",     "broken"        }
+    });
+    ctl_->recv([&] (auto rresult, auto rmessage) {
+        result = rresult;
+        message = rmessage;
+    });
+
+    wait_for([&] {
+        return result;
+    });
+
+    BOOST_TEST(result == plugin_error::exec_error);
+    BOOST_TEST(message["error"].template get<int>() == plugin_error::exec_error);
+    BOOST_TEST(message["errorCategory"].template get<std::string>() == "plugin");
+}
+
+BOOST_AUTO_TEST_SUITE_END()
+
+BOOST_AUTO_TEST_SUITE_END()
+
+} // !irccd
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/src/libirccd/command-plugin-reload/CMakeLists.txt	Fri Dec 15 15:37:58 2017 +0100
@@ -0,0 +1,23 @@
+#
+# CMakeLists.txt -- CMake build system for irccd
+#
+# Copyright (c) 2013-2017 David Demelier <markand@malikania.fr>
+#
+# Permission to use, copy, modify, and/or distribute this software for any
+# purpose with or without fee is hereby granted, provided that the above
+# copyright notice and this permission notice appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+#
+
+irccd_define_test(
+    NAME command-plugin-reload
+    SOURCES main.cpp
+    LIBRARIES libirccd libirccdctl
+)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/src/libirccd/command-plugin-reload/main.cpp	Fri Dec 15 15:37:58 2017 +0100
@@ -0,0 +1,138 @@
+/*
+ * main.cpp -- test plugin-reload remote command
+ *
+ * Copyright (c) 2013-2017 David Demelier <markand@malikania.fr>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#define BOOST_TEST_MODULE "plugin-reload"
+#include <boost/test/unit_test.hpp>
+
+#include <irccd/daemon/command.hpp>
+#include <irccd/daemon/plugin_service.hpp>
+
+#include <irccd/test/command_test.hpp>
+
+namespace irccd {
+
+namespace {
+
+class custom_plugin : public plugin {
+public:
+    bool reloaded{false};
+
+    custom_plugin()
+        : plugin("test", "")
+    {
+    }
+
+    void on_reload(irccd&) override
+    {
+        reloaded = true;
+    }
+};
+
+class broken_plugin : public plugin {
+public:
+    using plugin::plugin;
+
+    void on_reload(irccd&) override
+    {
+        throw std::runtime_error("broken");
+    }
+};
+
+class plugin_reload_test : public command_test<plugin_reload_command> {
+protected:
+    std::shared_ptr<custom_plugin> plugin_;
+
+    plugin_reload_test()
+        : plugin_(std::make_shared<custom_plugin>())
+    {
+        daemon_->plugins().add(plugin_);
+        daemon_->plugins().add(std::make_unique<broken_plugin>("broken", ""));
+    }
+};
+
+} // !namespace
+
+BOOST_FIXTURE_TEST_SUITE(plugin_reload_test_suite, plugin_reload_test)
+
+BOOST_AUTO_TEST_CASE(basic)
+{
+    ctl_->send({
+        { "command",    "plugin-reload" },
+        { "plugin",     "test"          }
+    });
+
+    wait_for([&] () {
+        return plugin_->reloaded;
+    });
+
+    BOOST_TEST(plugin_->reloaded);
+}
+
+BOOST_AUTO_TEST_SUITE(errors)
+
+BOOST_AUTO_TEST_CASE(not_found)
+{
+    boost::system::error_code result;
+    nlohmann::json message;
+
+    ctl_->send({
+        { "command",    "plugin-reload" },
+        { "plugin",     "unknown"       }
+    });
+    ctl_->recv([&] (auto rresult, auto rmessage) {
+        result = rresult;
+        message = rmessage;
+    });
+
+    wait_for([&] {
+        return result;
+    });
+
+    BOOST_TEST(result == plugin_error::not_found);
+    BOOST_TEST(message["error"].template get<int>() == plugin_error::not_found);
+    BOOST_TEST(message["errorCategory"].template get<std::string>() == "plugin");
+}
+
+BOOST_AUTO_TEST_CASE(exec_error)
+{
+    boost::system::error_code result;
+    nlohmann::json message;
+
+    ctl_->send({
+        { "command",    "plugin-reload" },
+        { "plugin",     "broken"        }
+    });
+    ctl_->recv([&] (auto rresult, auto rmessage) {
+        result = rresult;
+        message = rmessage;
+    });
+
+    wait_for([&] {
+        return result;
+    });
+
+    BOOST_TEST(result == plugin_error::exec_error);
+    BOOST_TEST(message["error"].template get<int>() == plugin_error::exec_error);
+    BOOST_TEST(message["errorCategory"].template get<std::string>() == "plugin");
+}
+
+BOOST_AUTO_TEST_SUITE_END()
+
+BOOST_AUTO_TEST_SUITE_END()
+
+} // !irccd
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/src/libirccd/command-plugin-unload/CMakeLists.txt	Fri Dec 15 15:37:58 2017 +0100
@@ -0,0 +1,23 @@
+#
+# CMakeLists.txt -- CMake build system for irccd
+#
+# Copyright (c) 2013-2017 David Demelier <markand@malikania.fr>
+#
+# Permission to use, copy, modify, and/or distribute this software for any
+# purpose with or without fee is hereby granted, provided that the above
+# copyright notice and this permission notice appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+#
+
+irccd_define_test(
+    NAME command-plugin-unload
+    SOURCES main.cpp
+    LIBRARIES libirccd libirccdctl
+)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/src/libirccd/command-plugin-unload/main.cpp	Fri Dec 15 15:37:58 2017 +0100
@@ -0,0 +1,139 @@
+/*
+ * main.cpp -- test plugin-unload remote command
+ *
+ * Copyright (c) 2013-2017 David Demelier <markand@malikania.fr>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#define BOOST_TEST_MODULE "plugin-unload"
+#include <boost/test/unit_test.hpp>
+
+#include <irccd/daemon/command.hpp>
+#include <irccd/daemon/plugin_service.hpp>
+
+#include <irccd/test/command_test.hpp>
+
+namespace irccd {
+
+namespace {
+
+class custom_plugin : public plugin {
+public:
+    bool unloaded{false};
+
+    custom_plugin()
+        : plugin("test", "")
+    {
+    }
+
+    void on_unload(irccd &) override
+    {
+        unloaded = true;
+    }
+};
+
+class broken_plugin : public plugin {
+public:
+    using plugin::plugin;
+
+    void on_unload(irccd&) override
+    {
+        throw std::runtime_error("broken");
+    }
+};
+
+class plugin_unload_test : public command_test<plugin_unload_command> {
+protected:
+    std::shared_ptr<custom_plugin> plugin_;
+
+    plugin_unload_test()
+        : plugin_(std::make_shared<custom_plugin>())
+    {
+        daemon_->plugins().add(plugin_);
+        daemon_->plugins().add(std::make_unique<broken_plugin>("broken", ""));
+    }
+};
+
+} // !namespace
+
+BOOST_FIXTURE_TEST_SUITE(plugin_unload_test_suite, plugin_unload_test)
+
+BOOST_AUTO_TEST_CASE(basic)
+{
+    ctl_->send({
+        { "command",    "plugin-unload" },
+        { "plugin",     "test"          }
+    });
+
+    wait_for([&] () {
+        return plugin_->unloaded;
+    });
+
+    BOOST_TEST(plugin_->unloaded);
+}
+
+BOOST_AUTO_TEST_SUITE(errors)
+
+BOOST_AUTO_TEST_CASE(not_found)
+{
+    boost::system::error_code result;
+    nlohmann::json message;
+
+    ctl_->send({
+        { "command",    "plugin-unload" },
+        { "plugin",     "unknown"       }
+    });
+    ctl_->recv([&] (auto rresult, auto rmessage) {
+        result = rresult;
+        message = rmessage;
+    });
+
+    wait_for([&] {
+        return result;
+    });
+
+    BOOST_TEST(result == plugin_error::not_found);
+    BOOST_TEST(message["error"].template get<int>() == plugin_error::not_found);
+    BOOST_TEST(message["errorCategory"].template get<std::string>() == "plugin");
+}
+
+BOOST_AUTO_TEST_CASE(exec_error)
+{
+    boost::system::error_code result;
+    nlohmann::json message;
+
+    ctl_->send({
+        { "command",    "plugin-unload" },
+        { "plugin",     "broken"        }
+    });
+    ctl_->recv([&] (auto rresult, auto rmessage) {
+        result = rresult;
+        message = rmessage;
+    });
+
+    wait_for([&] {
+        return result;
+    });
+
+    BOOST_TEST(result == plugin_error::exec_error);
+    BOOST_TEST(message["error"].template get<int>() == plugin_error::exec_error);
+    BOOST_TEST(message["errorCategory"].template get<std::string>() == "plugin");
+    BOOST_TEST(!daemon_->plugins().has("broken"));
+}
+
+BOOST_AUTO_TEST_SUITE_END()
+
+BOOST_AUTO_TEST_SUITE_END()
+
+} // !irccd
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/src/libirccd/command-rule-add/CMakeLists.txt	Fri Dec 15 15:37:58 2017 +0100
@@ -0,0 +1,23 @@
+#
+# CMakeLists.txt -- CMake build system for irccd
+#
+# Copyright (c) 2013-2017 David Demelier <markand@malikania.fr>
+#
+# Permission to use, copy, modify, and/or distribute this software for any
+# purpose with or without fee is hereby granted, provided that the above
+# copyright notice and this permission notice appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+#
+
+irccd_define_test(
+    NAME command-rule-add
+    SOURCES main.cpp
+    LIBRARIES libirccd libirccdctl
+)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/src/libirccd/command-rule-add/main.cpp	Fri Dec 15 15:37:58 2017 +0100
@@ -0,0 +1,203 @@
+/*
+ * main.cpp -- test rule-add remote command
+ *
+ * Copyright (c) 2013-2017 David Demelier <markand@malikania.fr>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#define BOOST_TEST_MODULE "rule-add"
+#include <boost/test/unit_test.hpp>
+
+#include <irccd/json_util.hpp>
+
+#include <irccd/daemon/command.hpp>
+#include <irccd/daemon/rule_service.hpp>
+
+#include <irccd/test/command_test.hpp>
+
+namespace irccd {
+
+namespace {
+
+using rule_add_test = command_test<rule_add_command, rule_list_command>;
+
+} // !namespace
+
+BOOST_FIXTURE_TEST_SUITE(rule_add_test_suite, rule_add_test)
+
+BOOST_AUTO_TEST_CASE(basic)
+{
+    nlohmann::json result;
+
+    ctl_->send({
+        { "command",    "rule-add"          },
+        { "servers",    { "s1", "s2" }      },
+        { "channels",   { "c1", "c2" }      },
+        { "plugins",    { "p1", "p2" }      },
+        { "events",     { "onMessage" }     },
+        { "action",     "accept"            },
+        { "index",      0                   }
+    });
+    ctl_->recv([&] (auto, auto msg) {
+        result = msg;
+    });
+
+    wait_for([&] () {
+        return result.is_object();
+    });
+
+    BOOST_TEST(result.is_object());
+
+    result = nullptr;
+    ctl_->send({{"command", "rule-list"}});
+    ctl_->recv([&] (auto, auto msg) {
+        result = msg;
+    });
+
+    wait_for([&] () {
+        return result.is_object();
+    });
+
+    BOOST_TEST(result.is_object());
+
+    auto servers = result["list"][0]["servers"];
+    auto channels = result["list"][0]["channels"];
+    auto plugins = result["list"][0]["plugins"];
+    auto events = result["list"][0]["events"];
+
+    BOOST_TEST(json_util::contains(servers, "s1"));
+    BOOST_TEST(json_util::contains(servers, "s2"));
+    BOOST_TEST(json_util::contains(channels, "c1"));
+    BOOST_TEST(json_util::contains(channels, "c2"));
+    BOOST_TEST(json_util::contains(plugins, "p1"));
+    BOOST_TEST(json_util::contains(plugins, "p2"));
+    BOOST_TEST(json_util::contains(events, "onMessage"));
+    BOOST_TEST(result["list"][0]["action"].get<std::string>() == "accept");
+}
+
+BOOST_AUTO_TEST_CASE(append)
+{
+    nlohmann::json result;
+
+    ctl_->send({
+        { "command",    "rule-add"          },
+        { "servers",    { "s1" }            },
+        { "channels",   { "c1" }            },
+        { "plugins",    { "p1" }            },
+        { "events",     { "onMessage" }     },
+        { "action",     "accept"            },
+        { "index",      0                   }
+    });
+    ctl_->recv([&] (auto, auto msg) {
+        result = msg;
+    });
+
+    wait_for([&] () {
+        return result.is_object();
+    });
+
+    BOOST_TEST(result.is_object());
+
+    result = nullptr;
+    ctl_->send({
+        { "command",    "rule-add"          },
+        { "servers",    { "s2" }            },
+        { "channels",   { "c2" }            },
+        { "plugins",    { "p2" }            },
+        { "events",     { "onMessage" }     },
+        { "action",     "drop"              },
+        { "index",      1                   }
+    });
+    ctl_->recv([&] (auto, auto msg) {
+        result = msg;
+    });
+
+    wait_for([&] () {
+        return result.is_object();
+    });
+
+    BOOST_TEST(result.is_object());
+
+    result = nullptr;
+    ctl_->send({{"command", "rule-list"}});
+    ctl_->recv([&] (auto, auto msg) {
+        result = msg;
+    });
+
+    wait_for([&] () {
+        return result.is_object();
+    });
+
+    BOOST_TEST(result.is_object());
+    BOOST_TEST(result["list"].size() == 2U);
+
+    // Rule 0.
+    {
+        auto servers = result["list"][0]["servers"];
+        auto channels = result["list"][0]["channels"];
+        auto plugins = result["list"][0]["plugins"];
+        auto events = result["list"][0]["events"];
+
+        BOOST_TEST(json_util::contains(servers, "s1"));
+        BOOST_TEST(json_util::contains(channels, "c1"));
+        BOOST_TEST(json_util::contains(plugins, "p1"));
+        BOOST_TEST(json_util::contains(events, "onMessage"));
+        BOOST_TEST(result["list"][0]["action"].get<std::string>() == "accept");
+    }
+
+    // Rule 1.
+    {
+        auto servers = result["list"][1]["servers"];
+        auto channels = result["list"][1]["channels"];
+        auto plugins = result["list"][1]["plugins"];
+        auto events = result["list"][1]["events"];
+
+        BOOST_TEST(json_util::contains(servers, "s2"));
+        BOOST_TEST(json_util::contains(channels, "c2"));
+        BOOST_TEST(json_util::contains(plugins, "p2"));
+        BOOST_TEST(json_util::contains(events, "onMessage"));
+        BOOST_TEST(result["list"][1]["action"].get<std::string>() == "drop");
+    }
+}
+
+BOOST_AUTO_TEST_SUITE(errors)
+
+BOOST_AUTO_TEST_CASE(invalid_action)
+{
+    boost::system::error_code result;
+    nlohmann::json message;
+
+    ctl_->send({
+        { "command",    "rule-add"  },
+        { "action",     "unknown"   }
+    });
+    ctl_->recv([&] (auto rresult, auto rmessage) {
+        result = rresult;
+        message = rmessage;
+    });
+
+    wait_for([&] {
+        return result;
+    });
+
+    BOOST_TEST(result == rule_error::invalid_action);
+    BOOST_TEST(message["error"].template get<int>() == rule_error::invalid_action);
+    BOOST_TEST(message["errorCategory"].template get<std::string>() == "rule");
+}
+
+BOOST_AUTO_TEST_SUITE_END()
+
+BOOST_AUTO_TEST_SUITE_END()
+
+} // !irccd
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/src/libirccd/command-rule-edit/CMakeLists.txt	Fri Dec 15 15:37:58 2017 +0100
@@ -0,0 +1,23 @@
+#
+# CMakeLists.txt -- CMake build system for irccd
+#
+# Copyright (c) 2013-2017 David Demelier <markand@malikania.fr>
+#
+# Permission to use, copy, modify, and/or distribute this software for any
+# purpose with or without fee is hereby granted, provided that the above
+# copyright notice and this permission notice appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+#
+
+irccd_define_test(
+    NAME command-rule-edit
+    SOURCES main.cpp
+    LIBRARIES libirccd libirccdctl
+)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/src/libirccd/command-rule-edit/main.cpp	Fri Dec 15 15:37:58 2017 +0100
@@ -0,0 +1,646 @@
+/*
+ * main.cpp -- test rule-edit remote command
+ *
+ * Copyright (c) 2013-2017 David Demelier <markand@malikania.fr>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#define BOOST_TEST_MODULE "rule-edit"
+#include <boost/test/unit_test.hpp>
+
+#include <irccd/json_util.hpp>
+
+#include <irccd/daemon/command.hpp>
+#include <irccd/daemon/rule_service.hpp>
+
+#include <irccd/test/command_test.hpp>
+
+namespace irccd {
+
+namespace {
+
+class rule_edit_test : public command_test<rule_edit_command, rule_info_command> {
+public:
+    rule_edit_test()
+    {
+        daemon_->rules().add(rule(
+            { "s1", "s2" },
+            { "c1", "c2" },
+            { "o1", "o2" },
+            { "p1", "p2" },
+            { "onMessage", "onCommand" },
+            rule::action_type::drop
+        ));
+    }
+};
+
+} // !namespace
+
+BOOST_FIXTURE_TEST_SUITE(rule_edit_test_suite, rule_edit_test)
+
+BOOST_AUTO_TEST_CASE(add_server)
+{
+    nlohmann::json result;
+
+    ctl_->send({
+        { "command",        "rule-edit"     },
+        { "add-servers",    { "new-s3" }    },
+        { "index",          0               }
+    });
+    ctl_->recv([&] (auto, auto msg) {
+        result = msg;
+    });
+
+    wait_for([&] () {
+        return result.is_object();
+    });
+
+    BOOST_TEST(result.is_object());
+
+    result = nullptr;
+    ctl_->send({
+        { "command",        "rule-info"     },
+        { "index",          0               }
+    });
+    ctl_->recv([&] (auto, auto msg) {
+        result = msg;
+    });
+
+    wait_for([&] () {
+        return result.is_object();
+    });
+
+    BOOST_TEST(result.is_object());
+    BOOST_TEST(json_util::contains(result["servers"], "s1"));
+    BOOST_TEST(json_util::contains(result["servers"], "s2"));
+    BOOST_TEST(json_util::contains(result["servers"], "new-s3"));
+    BOOST_TEST(json_util::contains(result["channels"], "c1"));
+    BOOST_TEST(json_util::contains(result["channels"], "c2"));
+    BOOST_TEST(json_util::contains(result["plugins"], "p1"));
+    BOOST_TEST(json_util::contains(result["plugins"], "p2"));
+    BOOST_TEST(json_util::contains(result["events"], "onMessage"));
+    BOOST_TEST(json_util::contains(result["events"], "onCommand"));
+    BOOST_TEST(result["action"].get<std::string>() == "drop");
+}
+
+BOOST_AUTO_TEST_CASE(add_channel)
+{
+    nlohmann::json result;
+
+    ctl_->send({
+        { "command",        "rule-edit"     },
+        { "add-channels",   { "new-c3" }    },
+        { "index",          0               }
+    });
+    ctl_->recv([&] (auto, auto msg) {
+        result = msg;
+    });
+
+    wait_for([&] () {
+        return result.is_object();
+    });
+
+    BOOST_TEST(result.is_object());
+
+    result = nullptr;
+    ctl_->send({
+        { "command",        "rule-info"     },
+        { "index",          0               }
+    });
+    ctl_->recv([&] (auto, auto msg) {
+        result = msg;
+    });
+
+    wait_for([&] () {
+        return result.is_object();
+    });
+
+    BOOST_TEST(result.is_object());
+    BOOST_TEST(json_util::contains(result["servers"], "s1"));
+    BOOST_TEST(json_util::contains(result["servers"], "s2"));
+    BOOST_TEST(json_util::contains(result["channels"], "c1"));
+    BOOST_TEST(json_util::contains(result["channels"], "c2"));
+    BOOST_TEST(json_util::contains(result["channels"], "new-c3"));
+    BOOST_TEST(json_util::contains(result["plugins"], "p1"));
+    BOOST_TEST(json_util::contains(result["plugins"], "p2"));
+    BOOST_TEST(json_util::contains(result["events"], "onMessage"));
+    BOOST_TEST(json_util::contains(result["events"], "onCommand"));
+    BOOST_TEST(result["action"].get<std::string>() == "drop");
+}
+
+BOOST_AUTO_TEST_CASE(add_plugin)
+{
+    nlohmann::json result;
+
+    ctl_->send({
+        { "command",        "rule-edit"     },
+        { "add-plugins",    { "new-p3" }    },
+        { "index",          0               }
+    });
+    ctl_->recv([&] (auto, auto msg) {
+        result = msg;
+    });
+
+    wait_for([&] () {
+        return result.is_object();
+    });
+
+    BOOST_TEST(result.is_object());
+
+    result = nullptr;
+    ctl_->send({
+        { "command",        "rule-info"     },
+        { "index",          0               }
+    });
+    ctl_->recv([&] (auto, auto msg) {
+        result = msg;
+    });
+
+    wait_for([&] () {
+        return result.is_object();
+    });
+
+    BOOST_TEST(result.is_object());
+    BOOST_TEST(json_util::contains(result["servers"], "s1"));
+    BOOST_TEST(json_util::contains(result["servers"], "s2"));
+    BOOST_TEST(json_util::contains(result["channels"], "c1"));
+    BOOST_TEST(json_util::contains(result["channels"], "c2"));
+    BOOST_TEST(json_util::contains(result["plugins"], "p1"));
+    BOOST_TEST(json_util::contains(result["plugins"], "p2"));
+    BOOST_TEST(json_util::contains(result["plugins"], "new-p3"));
+    BOOST_TEST(json_util::contains(result["events"], "onMessage"));
+    BOOST_TEST(json_util::contains(result["events"], "onCommand"));
+    BOOST_TEST(result["action"].get<std::string>() == "drop");
+}
+
+BOOST_AUTO_TEST_CASE(add_event)
+{
+    nlohmann::json result;
+
+    ctl_->send({
+        { "command",        "rule-edit"     },
+        { "add-events",     { "onQuery" }   },
+        { "index",          0               }
+    });
+    ctl_->recv([&] (auto, auto msg) {
+        result = msg;
+    });
+
+    wait_for([&] () {
+        return result.is_object();
+    });
+
+    BOOST_TEST(result.is_object());
+
+    result = nullptr;
+    ctl_->send({
+        { "command",        "rule-info"     },
+        { "index",          0               }
+    });
+    ctl_->recv([&] (auto, auto msg) {
+        result = msg;
+    });
+
+    wait_for([&] () {
+        return result.is_object();
+    });
+
+    BOOST_TEST(result.is_object());
+    BOOST_TEST(json_util::contains(result["servers"], "s1"));
+    BOOST_TEST(json_util::contains(result["servers"], "s2"));
+    BOOST_TEST(json_util::contains(result["channels"], "c1"));
+    BOOST_TEST(json_util::contains(result["channels"], "c2"));
+    BOOST_TEST(json_util::contains(result["plugins"], "p1"));
+    BOOST_TEST(json_util::contains(result["plugins"], "p2"));
+    BOOST_TEST(json_util::contains(result["events"], "onMessage"));
+    BOOST_TEST(json_util::contains(result["events"], "onCommand"));
+    BOOST_TEST(json_util::contains(result["events"], "onQuery"));
+    BOOST_TEST(result["action"].get<std::string>() == "drop");
+}
+
+BOOST_AUTO_TEST_CASE(add_event_and_server)
+{
+    nlohmann::json result;
+
+    ctl_->send({
+        { "command",        "rule-edit"     },
+        { "add-servers",    { "new-s3" }    },
+        { "add-events",     { "onQuery" }   },
+        { "index",          0               }
+    });
+    ctl_->recv([&] (auto, auto msg) {
+        result = msg;
+    });
+
+    wait_for([&] () {
+        return result.is_object();
+    });
+
+    BOOST_TEST(result.is_object());
+
+    result = nullptr;
+    ctl_->send({
+        { "command",        "rule-info"     },
+        { "index",          0               }
+    });
+    ctl_->recv([&] (auto, auto msg) {
+        result = msg;
+    });
+
+    wait_for([&] () {
+        return result.is_object();
+    });
+
+    BOOST_TEST(result.is_object());
+    BOOST_TEST(json_util::contains(result["servers"], "s1"));
+    BOOST_TEST(json_util::contains(result["servers"], "s2"));
+    BOOST_TEST(json_util::contains(result["servers"], "new-s3"));
+    BOOST_TEST(json_util::contains(result["channels"], "c1"));
+    BOOST_TEST(json_util::contains(result["channels"], "c2"));
+    BOOST_TEST(json_util::contains(result["plugins"], "p1"));
+    BOOST_TEST(json_util::contains(result["plugins"], "p2"));
+    BOOST_TEST(json_util::contains(result["events"], "onMessage"));
+    BOOST_TEST(json_util::contains(result["events"], "onCommand"));
+    BOOST_TEST(json_util::contains(result["events"], "onQuery"));
+    BOOST_TEST(result["action"].get<std::string>() == "drop");
+}
+
+BOOST_AUTO_TEST_CASE(change_action)
+{
+    nlohmann::json result;
+
+    ctl_->send({
+        { "command",        "rule-edit"     },
+        { "action",         "accept"        },
+        { "index",          0               }
+    });
+    ctl_->recv([&] (auto, auto msg) {
+        result = msg;
+    });
+
+    wait_for([&] () {
+        return result.is_object();
+    });
+
+    BOOST_TEST(result.is_object());
+
+    result = nullptr;
+    ctl_->send({
+        { "command",        "rule-info"     },
+        { "index",          0               }
+    });
+    ctl_->recv([&] (auto, auto msg) {
+        result = msg;
+    });
+
+    wait_for([&] () {
+        return result.is_object();
+    });
+
+    BOOST_TEST(result.is_object());
+    BOOST_TEST(json_util::contains(result["servers"], "s1"));
+    BOOST_TEST(json_util::contains(result["servers"], "s2"));
+    BOOST_TEST(json_util::contains(result["channels"], "c1"));
+    BOOST_TEST(json_util::contains(result["channels"], "c2"));
+    BOOST_TEST(json_util::contains(result["plugins"], "p1"));
+    BOOST_TEST(json_util::contains(result["plugins"], "p2"));
+    BOOST_TEST(json_util::contains(result["events"], "onMessage"));
+    BOOST_TEST(json_util::contains(result["events"], "onCommand"));
+    BOOST_TEST(result["action"].get<std::string>() == "accept");
+}
+
+BOOST_AUTO_TEST_CASE(remove_server)
+{
+    nlohmann::json result;
+
+    ctl_->send({
+        { "command",        "rule-edit"     },
+        { "remove-servers", { "s2" }        },
+        { "index",          0               }
+    });
+    ctl_->recv([&] (auto, auto msg) {
+        result = msg;
+    });
+
+    wait_for([&] () {
+        return result.is_object();
+    });
+
+    BOOST_TEST(result.is_object());
+
+    result = nullptr;
+    ctl_->send({
+        { "command",        "rule-info"     },
+        { "index",          0               }
+    });
+    ctl_->recv([&] (auto, auto msg) {
+        result = msg;
+    });
+
+    wait_for([&] () {
+        return result.is_object();
+    });
+
+    BOOST_TEST(result.is_object());
+    BOOST_TEST(json_util::contains(result["servers"], "s1"));
+    BOOST_TEST(!json_util::contains(result["servers"], "s2"));
+    BOOST_TEST(json_util::contains(result["channels"], "c1"));
+    BOOST_TEST(json_util::contains(result["channels"], "c2"));
+    BOOST_TEST(json_util::contains(result["plugins"], "p1"));
+    BOOST_TEST(json_util::contains(result["plugins"], "p2"));
+    BOOST_TEST(json_util::contains(result["events"], "onMessage"));
+    BOOST_TEST(json_util::contains(result["events"], "onCommand"));
+    BOOST_TEST(result["action"].get<std::string>() == "drop");
+}
+
+BOOST_AUTO_TEST_CASE(remove_channel)
+{
+    nlohmann::json result;
+
+    ctl_->send({
+        { "command",        "rule-edit"     },
+        { "remove-channels", { "c2" }       },
+        { "index",          0               }
+    });
+    ctl_->recv([&] (auto, auto msg) {
+        result = msg;
+    });
+
+    wait_for([&] () {
+        return result.is_object();
+    });
+
+    BOOST_TEST(result.is_object());
+
+    result = nullptr;
+    ctl_->send({
+        { "command",        "rule-info"     },
+        { "index",          0               }
+    });
+    ctl_->recv([&] (auto, auto msg) {
+        result = msg;
+    });
+
+    wait_for([&] () {
+        return result.is_object();
+    });
+
+    BOOST_TEST(result.is_object());
+    BOOST_TEST(json_util::contains(result["servers"], "s1"));
+    BOOST_TEST(json_util::contains(result["servers"], "s2"));
+    BOOST_TEST(json_util::contains(result["channels"], "c1"));
+    BOOST_TEST(!json_util::contains(result["channels"], "c2"));
+    BOOST_TEST(json_util::contains(result["plugins"], "p1"));
+    BOOST_TEST(json_util::contains(result["plugins"], "p2"));
+    BOOST_TEST(json_util::contains(result["events"], "onMessage"));
+    BOOST_TEST(json_util::contains(result["events"], "onCommand"));
+    BOOST_TEST(result["action"].get<std::string>() == "drop");
+}
+
+BOOST_AUTO_TEST_CASE(remove_plugin)
+{
+    nlohmann::json result;
+
+    ctl_->send({
+        { "command",        "rule-edit"     },
+        { "remove-plugins", { "p2" }        },
+        { "index",          0               }
+    });
+    ctl_->recv([&] (auto, auto msg) {
+        result = msg;
+    });
+
+    wait_for([&] () {
+        return result.is_object();
+    });
+
+    BOOST_TEST(result.is_object());
+
+    result = nullptr;
+    ctl_->send({
+        { "command",        "rule-info"     },
+        { "index",          0               }
+    });
+    ctl_->recv([&] (auto, auto msg) {
+        result = msg;
+    });
+
+    wait_for([&] () {
+        return result.is_object();
+    });
+
+    BOOST_TEST(result.is_object());
+    BOOST_TEST(json_util::contains(result["servers"], "s1"));
+    BOOST_TEST(json_util::contains(result["servers"], "s2"));
+    BOOST_TEST(json_util::contains(result["channels"], "c1"));
+    BOOST_TEST(json_util::contains(result["channels"], "c2"));
+    BOOST_TEST(json_util::contains(result["plugins"], "p1"));
+    BOOST_TEST(!json_util::contains(result["plugins"], "p2"));
+    BOOST_TEST(json_util::contains(result["events"], "onMessage"));
+    BOOST_TEST(json_util::contains(result["events"], "onCommand"));
+    BOOST_TEST(result["action"].get<std::string>() == "drop");
+}
+
+BOOST_AUTO_TEST_CASE(remove_event)
+{
+    nlohmann::json result;
+
+    ctl_->send({
+        { "command",        "rule-edit"     },
+        { "remove-events",  { "onCommand" } },
+        { "index",          0               }
+    });
+    ctl_->recv([&] (auto, auto msg) {
+        result = msg;
+    });
+
+    wait_for([&] () {
+        return result.is_object();
+    });
+
+    BOOST_TEST(result.is_object());
+
+    result = nullptr;
+    ctl_->send({
+        { "command",        "rule-info"     },
+        { "index",          0               }
+    });
+    ctl_->recv([&] (auto, auto msg) {
+        result = msg;
+    });
+
+    wait_for([&] () {
+        return result.is_object();
+    });
+
+    BOOST_TEST(result.is_object());
+    BOOST_TEST(json_util::contains(result["servers"], "s1"));
+    BOOST_TEST(json_util::contains(result["servers"], "s2"));
+    BOOST_TEST(json_util::contains(result["channels"], "c1"));
+    BOOST_TEST(json_util::contains(result["channels"], "c2"));
+    BOOST_TEST(json_util::contains(result["plugins"], "p1"));
+    BOOST_TEST(json_util::contains(result["plugins"], "p2"));
+    BOOST_TEST(json_util::contains(result["events"], "onMessage"));
+    BOOST_TEST(!json_util::contains(result["events"], "onCommand"));
+    BOOST_TEST(result["action"].get<std::string>() == "drop");
+}
+
+BOOST_AUTO_TEST_CASE(remove_event_and_server)
+{
+    nlohmann::json result;
+
+    ctl_->send({
+        { "command",        "rule-edit"     },
+        { "remove-servers", { "s2" }        },
+        { "remove-events",  { "onCommand" } },
+        { "index",          0               }
+    });
+    ctl_->recv([&] (auto, auto msg) {
+        result = msg;
+    });
+
+    wait_for([&] () {
+        return result.is_object();
+    });
+
+    BOOST_TEST(result.is_object());
+
+    result = nullptr;
+    ctl_->send({
+        { "command",        "rule-info"     },
+        { "index",          0               }
+    });
+    ctl_->recv([&] (auto, auto msg) {
+        result = msg;
+    });
+
+    wait_for([&] () {
+        return result.is_object();
+    });
+
+    BOOST_TEST(result.is_object());
+    BOOST_TEST(json_util::contains(result["servers"], "s1"));
+    BOOST_TEST(!json_util::contains(result["servers"], "s2"));
+    BOOST_TEST(json_util::contains(result["channels"], "c1"));
+    BOOST_TEST(json_util::contains(result["channels"], "c2"));
+    BOOST_TEST(json_util::contains(result["plugins"], "p1"));
+    BOOST_TEST(json_util::contains(result["plugins"], "p2"));
+    BOOST_TEST(json_util::contains(result["events"], "onMessage"));
+    BOOST_TEST(!json_util::contains(result["events"], "onCommand"));
+    BOOST_TEST(result["action"].get<std::string>() == "drop");
+}
+
+BOOST_AUTO_TEST_SUITE(errors)
+
+BOOST_AUTO_TEST_CASE(invalid_index_1)
+{
+    boost::system::error_code result;
+    nlohmann::json message;
+
+    ctl_->send({
+        { "command",    "rule-edit" },
+        { "index",      -100        },
+        { "action",     "drop"      }
+    });
+    ctl_->recv([&] (auto rresult, auto rmessage) {
+        result = rresult;
+        message = rmessage;
+    });
+
+    wait_for([&] {
+        return result;
+    });
+
+    BOOST_TEST(result == rule_error::invalid_index);
+    BOOST_TEST(message["error"].template get<int>() == rule_error::invalid_index);
+    BOOST_TEST(message["errorCategory"].template get<std::string>() == "rule");
+}
+
+BOOST_AUTO_TEST_CASE(invalid_index_2)
+{
+    boost::system::error_code result;
+    nlohmann::json message;
+
+    ctl_->send({
+        { "command",    "rule-edit" },
+        { "index",      100         },
+        { "action",     "drop"      }
+    });
+    ctl_->recv([&] (auto rresult, auto rmessage) {
+        result = rresult;
+        message = rmessage;
+    });
+
+    wait_for([&] {
+        return result;
+    });
+
+    BOOST_TEST(result == rule_error::invalid_index);
+    BOOST_TEST(message["error"].template get<int>() == rule_error::invalid_index);
+    BOOST_TEST(message["errorCategory"].template get<std::string>() == "rule");
+}
+
+BOOST_AUTO_TEST_CASE(invalid_index_3)
+{
+    boost::system::error_code result;
+    nlohmann::json message;
+
+    ctl_->send({
+        { "command",    "rule-edit" },
+        { "index",      "notaint"   },
+        { "action",     "drop"      }
+    });
+    ctl_->recv([&] (auto rresult, auto rmessage) {
+        result = rresult;
+        message = rmessage;
+    });
+
+    wait_for([&] {
+        return result;
+    });
+
+    BOOST_TEST(result == rule_error::invalid_index);
+    BOOST_TEST(message["error"].template get<int>() == rule_error::invalid_index);
+    BOOST_TEST(message["errorCategory"].template get<std::string>() == "rule");
+}
+
+BOOST_AUTO_TEST_CASE(invalid_action)
+{
+    boost::system::error_code result;
+    nlohmann::json message;
+
+    ctl_->send({
+        { "command",    "rule-edit" },
+        { "index",      0           },
+        { "action",     "unknown"   }
+    });
+    ctl_->recv([&] (auto rresult, auto rmessage) {
+        result = rresult;
+        message = rmessage;
+    });
+
+    wait_for([&] {
+        return result;
+    });
+
+    BOOST_TEST(result == rule_error::invalid_action);
+    BOOST_TEST(message["error"].template get<int>() == rule_error::invalid_action);
+    BOOST_TEST(message["errorCategory"].template get<std::string>() == "rule");
+}
+
+BOOST_AUTO_TEST_SUITE_END()
+
+BOOST_AUTO_TEST_SUITE_END()
+
+} // !irccd
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/src/libirccd/command-rule-info/CMakeLists.txt	Fri Dec 15 15:37:58 2017 +0100
@@ -0,0 +1,23 @@
+#
+# CMakeLists.txt -- CMake build system for irccd
+#
+# Copyright (c) 2013-2017 David Demelier <markand@malikania.fr>
+#
+# Permission to use, copy, modify, and/or distribute this software for any
+# purpose with or without fee is hereby granted, provided that the above
+# copyright notice and this permission notice appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+#
+
+irccd_define_test(
+    NAME command-rule-info
+    SOURCES main.cpp
+    LIBRARIES libirccd libirccdctl
+)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/src/libirccd/command-rule-info/main.cpp	Fri Dec 15 15:37:58 2017 +0100
@@ -0,0 +1,169 @@
+/*
+ * main.cpp -- test rule-info remote command
+ *
+ * Copyright (c) 2013-2017 David Demelier <markand@malikania.fr>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#define BOOST_TEST_MODULE "rule-info"
+#include <boost/test/unit_test.hpp>
+
+#include <irccd/json_util.hpp>
+
+#include <irccd/daemon/command.hpp>
+#include <irccd/daemon/rule_service.hpp>
+
+#include <irccd/test/command_test.hpp>
+
+namespace irccd {
+
+namespace {
+
+class rule_info_test : public command_test<rule_info_command> {
+public:
+    rule_info_test()
+    {
+        daemon_->rules().add(rule(
+            { "s1", "s2" },
+            { "c1", "c2" },
+            { "o1", "o2" },
+            { "p1", "p2" },
+            { "onMessage", "onCommand" },
+            rule::action_type::drop
+        ));
+        daemon_->rules().add(rule(
+            { "s1", },
+            { "c1", },
+            { "o1", },
+            { "p1", },
+            { "onMessage", },
+            rule::action_type::accept
+        ));
+    }
+};
+
+} // !namespace
+
+BOOST_FIXTURE_TEST_SUITE(rule_info_test_suite, rule_info_test)
+
+BOOST_AUTO_TEST_CASE(basic)
+{
+    nlohmann::json result;
+
+    ctl_->send({
+        { "command",    "rule-info" },
+        { "index",      0           }
+    });
+    ctl_->recv([&] (auto, auto msg) {
+        result = msg;
+    });
+
+    wait_for([&] () {
+        return result.is_object();
+    });
+
+    BOOST_TEST(result.is_object());
+
+    auto servers = result["servers"];
+    auto channels = result["channels"];
+    auto plugins = result["plugins"];
+    auto events = result["events"];
+
+    BOOST_TEST(json_util::contains(servers, "s1"));
+    BOOST_TEST(json_util::contains(servers, "s2"));
+    BOOST_TEST(json_util::contains(channels, "c1"));
+    BOOST_TEST(json_util::contains(channels, "c2"));
+    BOOST_TEST(json_util::contains(plugins, "p1"));
+    BOOST_TEST(json_util::contains(plugins, "p2"));
+    BOOST_TEST(json_util::contains(events, "onMessage"));
+    BOOST_TEST(json_util::contains(events, "onCommand"));
+    BOOST_TEST(result["action"].get<std::string>() == "drop");
+}
+
+BOOST_AUTO_TEST_SUITE(errors)
+
+BOOST_AUTO_TEST_CASE(invalid_index_1)
+{
+    boost::system::error_code result;
+    nlohmann::json message;
+
+    ctl_->send({
+        { "command",    "rule-info" },
+        { "index",      -100        }
+    });
+    ctl_->recv([&] (auto rresult, auto rmessage) {
+        result = rresult;
+        message = rmessage;
+    });
+
+    wait_for([&] {
+        return result;
+    });
+
+    BOOST_TEST(result == rule_error::invalid_index);
+    BOOST_TEST(message["error"].template get<int>() == rule_error::invalid_index);
+    BOOST_TEST(message["errorCategory"].template get<std::string>() == "rule");
+}
+
+BOOST_AUTO_TEST_CASE(invalid_index_2)
+{
+    boost::system::error_code result;
+    nlohmann::json message;
+
+    ctl_->send({
+        { "command",    "rule-info" },
+        { "index",      100         }
+    });
+    ctl_->recv([&] (auto rresult, auto rmessage) {
+        result = rresult;
+        message = rmessage;
+    });
+
+    wait_for([&] {
+        return result;
+    });
+
+    BOOST_TEST(result == rule_error::invalid_index);
+    BOOST_TEST(message["error"].template get<int>() == rule_error::invalid_index);
+    BOOST_TEST(message["errorCategory"].template get<std::string>() == "rule");
+}
+
+BOOST_AUTO_TEST_CASE(invalid_index_3)
+{
+    boost::system::error_code result;
+    nlohmann::json message;
+
+    ctl_->send({
+        { "command",    "rule-info" },
+        { "index",      "notaint"   }
+    });
+    ctl_->recv([&] (auto rresult, auto rmessage) {
+        result = rresult;
+        message = rmessage;
+    });
+
+    wait_for([&] {
+        return result;
+    });
+
+    BOOST_TEST(result == rule_error::invalid_index);
+    BOOST_TEST(message["error"].template get<int>() == rule_error::invalid_index);
+    BOOST_TEST(message["errorCategory"].template get<std::string>() == "rule");
+}
+
+BOOST_AUTO_TEST_SUITE_END()
+
+BOOST_AUTO_TEST_SUITE_END()
+
+} // !irccd
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/src/libirccd/command-rule-list/CMakeLists.txt	Fri Dec 15 15:37:58 2017 +0100
@@ -0,0 +1,23 @@
+#
+# CMakeLists.txt -- CMake build system for irccd
+#
+# Copyright (c) 2013-2017 David Demelier <markand@malikania.fr>
+#
+# Permission to use, copy, modify, and/or distribute this software for any
+# purpose with or without fee is hereby granted, provided that the above
+# copyright notice and this permission notice appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+#
+
+irccd_define_test(
+    NAME command-rule-list
+    SOURCES main.cpp
+    LIBRARIES libirccd libirccdctl
+)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/src/libirccd/command-rule-list/main.cpp	Fri Dec 15 15:37:58 2017 +0100
@@ -0,0 +1,131 @@
+/*
+ * main.cpp -- test rule-list remote command
+ *
+ * Copyright (c) 2013-2017 David Demelier <markand@malikania.fr>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#define BOOST_TEST_MODULE "rule-list"
+#include <boost/test/unit_test.hpp>
+
+#include <irccd/json_util.hpp>
+
+#include <irccd/daemon/command.hpp>
+#include <irccd/daemon/rule_service.hpp>
+
+#include <irccd/test/command_test.hpp>
+
+namespace irccd {
+
+namespace {
+
+class rule_list_test : public command_test<rule_list_command> {
+public:
+    rule_list_test()
+    {
+        daemon_->rules().add(rule(
+            { "s1", "s2" },
+            { "c1", "c2" },
+            { "o1", "o2" },
+            { "p1", "p2" },
+            { "onMessage", "onCommand" },
+            rule::action_type::drop
+        ));
+        daemon_->rules().add(rule(
+            { "s1", },
+            { "c1", },
+            { "o1", },
+            { "p1", },
+            { "onMessage", },
+            rule::action_type::accept
+        ));
+    }
+};
+
+} // !namespace
+
+BOOST_FIXTURE_TEST_SUITE(rule_list_test_suite, rule_list_test)
+
+BOOST_AUTO_TEST_CASE(basic)
+{
+    nlohmann::json result;
+
+    ctl_->send({{"command", "rule-list"}});
+    ctl_->recv([&] (auto, auto msg) {
+        result = msg;
+    });
+    wait_for([&] () {
+        return result.is_object();
+    });
+
+    BOOST_TEST(result.is_object());
+    BOOST_TEST(result["list"].is_array());
+    BOOST_TEST(result["list"].size() == 2U);
+
+    // Rule 0.
+    {
+        auto servers = result["list"][0]["servers"];
+        auto channels = result["list"][0]["channels"];
+        auto plugins = result["list"][0]["plugins"];
+        auto events = result["list"][0]["events"];
+
+        BOOST_TEST(json_util::contains(servers, "s1"));
+        BOOST_TEST(json_util::contains(servers, "s2"));
+        BOOST_TEST(json_util::contains(channels, "c1"));
+        BOOST_TEST(json_util::contains(channels, "c2"));
+        BOOST_TEST(json_util::contains(plugins, "p1"));
+        BOOST_TEST(json_util::contains(plugins, "p2"));
+        BOOST_TEST(json_util::contains(events, "onMessage"));
+        BOOST_TEST(json_util::contains(events, "onCommand"));
+        BOOST_TEST(result["list"][0]["action"].get<std::string>() == "drop");
+    }
+
+    // Rule 1.
+    {
+        auto servers = result["list"][1]["servers"];
+        auto channels = result["list"][1]["channels"];
+        auto plugins = result["list"][1]["plugins"];
+        auto events = result["list"][1]["events"];
+
+        BOOST_TEST(json_util::contains(servers, "s1"));
+        BOOST_TEST(json_util::contains(channels, "c1"));
+        BOOST_TEST(json_util::contains(plugins, "p1"));
+        BOOST_TEST(json_util::contains(events, "onMessage"));
+        BOOST_TEST(result["list"][1]["action"].get<std::string>() == "accept");
+    }
+}
+
+BOOST_AUTO_TEST_CASE(empty)
+{
+    nlohmann::json result;
+
+    daemon_->rules().remove(0);
+    daemon_->rules().remove(0);
+
+    ctl_->send({{"command", "rule-list"}});
+    ctl_->recv([&] (auto, auto msg) {
+        result = msg;
+    });
+    wait_for([&] () {
+        return result.is_object();
+    });
+
+    BOOST_TEST(result.is_object());
+    BOOST_TEST(result["list"].is_array());
+    BOOST_TEST(result["list"].empty());
+}
+
+BOOST_AUTO_TEST_SUITE_END()
+
+} // !irccd
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/src/libirccd/command-rule-move/CMakeLists.txt	Fri Dec 15 15:37:58 2017 +0100
@@ -0,0 +1,23 @@
+#
+# CMakeLists.txt -- CMake build system for irccd
+#
+# Copyright (c) 2013-2017 David Demelier <markand@malikania.fr>
+#
+# Permission to use, copy, modify, and/or distribute this software for any
+# purpose with or without fee is hereby granted, provided that the above
+# copyright notice and this permission notice appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+#
+
+irccd_define_test(
+    NAME command-rule-move
+    SOURCES main.cpp
+    LIBRARIES libirccd libirccdctl
+)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/src/libirccd/command-rule-move/main.cpp	Fri Dec 15 15:37:58 2017 +0100
@@ -0,0 +1,490 @@
+/*
+ * main.cpp -- test rule-move remote command
+ *
+ * Copyright (c) 2013-2017 David Demelier <markand@malikania.fr>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#define BOOST_TEST_MODULE "rule-move"
+#include <boost/test/unit_test.hpp>
+
+#include <irccd/json_util.hpp>
+
+#include <irccd/daemon/command.hpp>
+#include <irccd/daemon/rule_service.hpp>
+
+#include <irccd/test/command_test.hpp>
+
+namespace irccd {
+
+namespace {
+
+class rule_move_test : public command_test<rule_move_command, rule_list_command> {
+public:
+    rule_move_test()
+    {
+        daemon_->rules().add(rule(
+            { "s0" },
+            { "c0" },
+            { "o0" },
+            { "p0" },
+            { "onMessage" },
+            rule::action_type::drop
+        ));
+        daemon_->rules().add(rule(
+            { "s1", },
+            { "c1", },
+            { "o1", },
+            { "p1", },
+            { "onMessage", },
+            rule::action_type::accept
+        ));
+        daemon_->rules().add(rule(
+            { "s2", },
+            { "c2", },
+            { "o2", },
+            { "p2", },
+            { "onMessage", },
+            rule::action_type::accept
+        ));
+    }
+};
+
+} // !namespace
+
+BOOST_FIXTURE_TEST_SUITE(rule_move_test_suite, rule_move_test)
+
+BOOST_AUTO_TEST_CASE(backward)
+{
+    nlohmann::json result;
+
+    ctl_->send({
+        { "command",    "rule-move" },
+        { "from",       2           },
+        { "to",         0           }
+    });
+    ctl_->recv([&] (auto, auto msg) {
+        result = msg;
+    });
+
+    wait_for([&] () {
+        return result.is_object();
+    });
+
+    BOOST_TEST(result.is_object());
+
+    result = nullptr;
+    ctl_->send({{ "command", "rule-list" }});
+    ctl_->recv([&] (auto, auto msg) {
+        result = msg;
+    });
+
+    wait_for([&] () {
+        return result.is_object();
+    });
+
+    BOOST_TEST(result.is_object());
+
+    // Rule 2.
+    {
+        auto servers = result["list"][0]["servers"];
+        auto channels = result["list"][0]["channels"];
+        auto plugins = result["list"][0]["plugins"];
+        auto events = result["list"][0]["events"];
+
+        BOOST_TEST(json_util::contains(servers, "s2"));
+        BOOST_TEST(json_util::contains(channels, "c2"));
+        BOOST_TEST(json_util::contains(plugins, "p2"));
+        BOOST_TEST(json_util::contains(events, "onMessage"));
+        BOOST_TEST(result["list"][0]["action"].get<std::string>() == "accept");
+    }
+
+    // Rule 0.
+    {
+        auto servers = result["list"][1]["servers"];
+        auto channels = result["list"][1]["channels"];
+        auto plugins = result["list"][1]["plugins"];
+        auto events = result["list"][1]["events"];
+
+        BOOST_TEST(json_util::contains(servers, "s0"));
+        BOOST_TEST(json_util::contains(channels, "c0"));
+        BOOST_TEST(json_util::contains(plugins, "p0"));
+        BOOST_TEST(json_util::contains(events, "onMessage"));
+        BOOST_TEST(result["list"][1]["action"].get<std::string>() == "drop");
+    }
+
+    // Rule 1.
+    {
+        auto servers = result["list"][2]["servers"];
+        auto channels = result["list"][2]["channels"];
+        auto plugins = result["list"][2]["plugins"];
+        auto events = result["list"][2]["events"];
+
+        BOOST_TEST(json_util::contains(servers, "s1"));
+        BOOST_TEST(json_util::contains(channels, "c1"));
+        BOOST_TEST(json_util::contains(plugins, "p1"));
+        BOOST_TEST(json_util::contains(events, "onMessage"));
+        BOOST_TEST(result["list"][2]["action"].get<std::string>() == "accept");
+    }
+}
+
+BOOST_AUTO_TEST_CASE(upward)
+{
+    nlohmann::json result;
+
+    ctl_->send({
+        { "command",    "rule-move" },
+        { "from",       0           },
+        { "to",         2           }
+    });
+    ctl_->recv([&] (auto, auto msg) {
+        result = msg;
+    });
+
+    wait_for([&] () {
+        return result.is_object();
+    });
+
+    BOOST_TEST(result.is_object());
+
+    result = nullptr;
+    ctl_->send({{ "command", "rule-list" }});
+    ctl_->recv([&] (auto, auto msg) {
+        result = msg;
+    });
+
+    wait_for([&] () {
+        return result.is_object();
+    });
+
+    BOOST_TEST(result.is_object());
+
+    // Rule 1.
+    {
+        auto servers = result["list"][0]["servers"];
+        auto channels = result["list"][0]["channels"];
+        auto plugins = result["list"][0]["plugins"];
+        auto events = result["list"][0]["events"];
+
+        BOOST_TEST(json_util::contains(servers, "s1"));
+        BOOST_TEST(json_util::contains(channels, "c1"));
+        BOOST_TEST(json_util::contains(plugins, "p1"));
+        BOOST_TEST(json_util::contains(events, "onMessage"));
+        BOOST_TEST(result["list"][0]["action"].get<std::string>() == "accept");
+    }
+
+    // Rule 2.
+    {
+        auto servers = result["list"][1]["servers"];
+        auto channels = result["list"][1]["channels"];
+        auto plugins = result["list"][1]["plugins"];
+        auto events = result["list"][1]["events"];
+
+        BOOST_TEST(json_util::contains(servers, "s2"));
+        BOOST_TEST(json_util::contains(channels, "c2"));
+        BOOST_TEST(json_util::contains(plugins, "p2"));
+        BOOST_TEST(json_util::contains(events, "onMessage"));
+        BOOST_TEST(result["list"][1]["action"].get<std::string>() == "accept");
+    }
+
+    // Rule 0.
+    {
+        auto servers = result["list"][2]["servers"];
+        auto channels = result["list"][2]["channels"];
+        auto plugins = result["list"][2]["plugins"];
+        auto events = result["list"][2]["events"];
+
+        BOOST_TEST(json_util::contains(servers, "s0"));
+        BOOST_TEST(json_util::contains(channels, "c0"));
+        BOOST_TEST(json_util::contains(plugins, "p0"));
+        BOOST_TEST(json_util::contains(events, "onMessage"));
+        BOOST_TEST(result["list"][2]["action"].get<std::string>() == "drop");
+    }
+}
+
+BOOST_AUTO_TEST_CASE(same)
+{
+    nlohmann::json result;
+
+    ctl_->send({
+        { "command",    "rule-move" },
+        { "from",       1           },
+        { "to",         1           }
+    });
+    ctl_->recv([&] (auto, auto msg) {
+        result = msg;
+    });
+
+    wait_for([&] () {
+        return result.is_object();
+    });
+
+    BOOST_TEST(result.is_object());
+
+    result = nullptr;
+    ctl_->send({{ "command", "rule-list" }});
+    ctl_->recv([&] (auto, auto msg) {
+        result = msg;
+    });
+
+    wait_for([&] () {
+        return result.is_object();
+    });
+
+    BOOST_TEST(result.is_object());
+
+    // Rule 0.
+    {
+        auto servers = result["list"][0]["servers"];
+        auto channels = result["list"][0]["channels"];
+        auto plugins = result["list"][0]["plugins"];
+        auto events = result["list"][0]["events"];
+
+        BOOST_TEST(json_util::contains(servers, "s0"));
+        BOOST_TEST(json_util::contains(channels, "c0"));
+        BOOST_TEST(json_util::contains(plugins, "p0"));
+        BOOST_TEST(json_util::contains(events, "onMessage"));
+        BOOST_TEST(result["list"][0]["action"].get<std::string>() == "drop");
+    }
+
+    // Rule 1.
+    {
+        auto servers = result["list"][1]["servers"];
+        auto channels = result["list"][1]["channels"];
+        auto plugins = result["list"][1]["plugins"];
+        auto events = result["list"][1]["events"];
+
+        BOOST_TEST(json_util::contains(servers, "s1"));
+        BOOST_TEST(json_util::contains(channels, "c1"));
+        BOOST_TEST(json_util::contains(plugins, "p1"));
+        BOOST_TEST(json_util::contains(events, "onMessage"));
+        BOOST_TEST(result["list"][1]["action"].get<std::string>() == "accept");
+    }
+
+    // Rule 2.
+    {
+        auto servers = result["list"][2]["servers"];
+        auto channels = result["list"][2]["channels"];
+        auto plugins = result["list"][2]["plugins"];
+        auto events = result["list"][2]["events"];
+
+        BOOST_TEST(json_util::contains(servers, "s2"));
+        BOOST_TEST(json_util::contains(channels, "c2"));
+        BOOST_TEST(json_util::contains(plugins, "p2"));
+        BOOST_TEST(json_util::contains(events, "onMessage"));
+        BOOST_TEST(result["list"][2]["action"].get<std::string>() == "accept");
+    }
+}
+
+BOOST_AUTO_TEST_CASE(beyond)
+{
+    nlohmann::json result;
+
+    ctl_->send({
+        { "command",    "rule-move" },
+        { "from",       0           },
+        { "to",         123         }
+    });
+    ctl_->recv([&] (auto, auto msg) {
+        result = msg;
+    });
+
+    wait_for([&] () {
+        return result.is_object();
+    });
+
+    BOOST_TEST(result.is_object());
+
+    result = nullptr;
+    ctl_->send({{ "command", "rule-list" }});
+    ctl_->recv([&] (auto, auto msg) {
+        result = msg;
+    });
+
+    wait_for([&] () {
+        return result.is_object();
+    });
+
+    BOOST_TEST(result.is_object());
+
+    // Rule 1.
+    {
+        auto servers = result["list"][0]["servers"];
+        auto channels = result["list"][0]["channels"];
+        auto plugins = result["list"][0]["plugins"];
+        auto events = result["list"][0]["events"];
+
+        BOOST_TEST(json_util::contains(servers, "s1"));
+        BOOST_TEST(json_util::contains(channels, "c1"));
+        BOOST_TEST(json_util::contains(plugins, "p1"));
+        BOOST_TEST(json_util::contains(events, "onMessage"));
+        BOOST_TEST(result["list"][0]["action"].get<std::string>() == "accept");
+    }
+
+    // Rule 2.
+    {
+        auto servers = result["list"][1]["servers"];
+        auto channels = result["list"][1]["channels"];
+        auto plugins = result["list"][1]["plugins"];
+        auto events = result["list"][1]["events"];
+
+        BOOST_TEST(json_util::contains(servers, "s2"));
+        BOOST_TEST(json_util::contains(channels, "c2"));
+        BOOST_TEST(json_util::contains(plugins, "p2"));
+        BOOST_TEST(json_util::contains(events, "onMessage"));
+        BOOST_TEST(result["list"][1]["action"].get<std::string>() == "accept");
+    }
+
+    // Rule 0.
+    {
+        auto servers = result["list"][2]["servers"];
+        auto channels = result["list"][2]["channels"];
+        auto plugins = result["list"][2]["plugins"];
+        auto events = result["list"][2]["events"];
+
+        BOOST_TEST(json_util::contains(servers, "s0"));
+        BOOST_TEST(json_util::contains(channels, "c0"));
+        BOOST_TEST(json_util::contains(plugins, "p0"));
+        BOOST_TEST(json_util::contains(events, "onMessage"));
+        BOOST_TEST(result["list"][2]["action"].get<std::string>() == "drop");
+    }
+}
+
+BOOST_AUTO_TEST_SUITE(errors)
+
+BOOST_AUTO_TEST_CASE(invalid_index_1_from)
+{
+    boost::system::error_code result;
+    nlohmann::json message;
+
+    ctl_->send({
+        { "command",    "rule-move" },
+        { "from",       -100        },
+        { "to",         0           }
+    });
+    ctl_->recv([&] (auto rresult, auto rmessage) {
+        result = rresult;
+        message = rmessage;
+    });
+
+    wait_for([&] {
+        return result;
+    });
+
+    BOOST_TEST(result == rule_error::invalid_index);
+    BOOST_TEST(message["error"].template get<int>() == rule_error::invalid_index);
+    BOOST_TEST(message["errorCategory"].template get<std::string>() == "rule");
+}
+
+BOOST_AUTO_TEST_CASE(invalid_index_1_to)
+{
+    boost::system::error_code result;
+    nlohmann::json message;
+
+    ctl_->send({
+        { "command",    "rule-move" },
+        { "from",       0           },
+        { "to",         -100        }
+    });
+    ctl_->recv([&] (auto rresult, auto rmessage) {
+        result = rresult;
+        message = rmessage;
+    });
+
+    wait_for([&] {
+        return result;
+    });
+
+    BOOST_TEST(result == rule_error::invalid_index);
+    BOOST_TEST(message["error"].template get<int>() == rule_error::invalid_index);
+    BOOST_TEST(message["errorCategory"].template get<std::string>() == "rule");
+}
+
+BOOST_AUTO_TEST_CASE(invalid_index_2_from)
+{
+    boost::system::error_code result;
+    nlohmann::json message;
+
+    ctl_->send({
+        { "command",    "rule-move" },
+        { "from",       100         },
+        { "to",         0           }
+    });
+    ctl_->recv([&] (auto rresult, auto rmessage) {
+        result = rresult;
+        message = rmessage;
+    });
+
+    wait_for([&] {
+        return result;
+    });
+
+    BOOST_TEST(result == rule_error::invalid_index);
+    BOOST_TEST(message["error"].template get<int>() == rule_error::invalid_index);
+    BOOST_TEST(message["errorCategory"].template get<std::string>() == "rule");
+}
+
+BOOST_AUTO_TEST_CASE(invalid_index_3_from)
+{
+    boost::system::error_code result;
+    nlohmann::json message;
+
+    ctl_->send({
+        { "command",    "rule-move" },
+        { "from",       "notaint"   },
+        { "to",         0           }
+    });
+    ctl_->recv([&] (auto rresult, auto rmessage) {
+        result = rresult;
+        message = rmessage;
+    });
+
+    wait_for([&] {
+        return result;
+    });
+
+    BOOST_TEST(result == rule_error::invalid_index);
+    BOOST_TEST(message["error"].template get<int>() == rule_error::invalid_index);
+    BOOST_TEST(message["errorCategory"].template get<std::string>() == "rule");
+}
+
+BOOST_AUTO_TEST_CASE(invalid_index_3_to)
+{
+    boost::system::error_code result;
+    nlohmann::json message;
+
+    ctl_->send({
+        { "command",    "rule-move" },
+        { "from",       0           },
+        { "to",         "notaint"   }
+    });
+    ctl_->recv([&] (auto rresult, auto rmessage) {
+        result = rresult;
+        message = rmessage;
+    });
+
+    wait_for([&] {
+        return result;
+    });
+
+    BOOST_TEST(result == rule_error::invalid_index);
+    BOOST_TEST(message["error"].template get<int>() == rule_error::invalid_index);
+    BOOST_TEST(message["errorCategory"].template get<std::string>() == "rule");
+}
+
+BOOST_AUTO_TEST_SUITE_END()
+
+BOOST_AUTO_TEST_SUITE_END()
+
+} // !irccd
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/src/libirccd/command-rule-remove/CMakeLists.txt	Fri Dec 15 15:37:58 2017 +0100
@@ -0,0 +1,23 @@
+#
+# CMakeLists.txt -- CMake build system for irccd
+#
+# Copyright (c) 2013-2017 David Demelier <markand@malikania.fr>
+#
+# Permission to use, copy, modify, and/or distribute this software for any
+# purpose with or without fee is hereby granted, provided that the above
+# copyright notice and this permission notice appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+#
+
+irccd_define_test(
+    NAME command-rule-remove
+    SOURCES main.cpp
+    LIBRARIES libirccd libirccdctl
+)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/src/libirccd/command-rule-remove/main.cpp	Fri Dec 15 15:37:58 2017 +0100
@@ -0,0 +1,204 @@
+/*
+ * main.cpp -- test rule-remove remote command
+ *
+ * Copyright (c) 2013-2017 David Demelier <markand@malikania.fr>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#define BOOST_TEST_MODULE "rule-remove"
+#include <boost/test/unit_test.hpp>
+
+#include <irccd/json_util.hpp>
+
+#include <irccd/daemon/command.hpp>
+#include <irccd/daemon/rule_service.hpp>
+
+#include <irccd/test/command_test.hpp>
+
+namespace irccd {
+
+namespace {
+
+class rule_remove_test : public command_test<rule_remove_command, rule_list_command> {
+public:
+    rule_remove_test()
+    {
+        daemon_->rules().add(rule(
+            { "s1", "s2" },
+            { "c1", "c2" },
+            { "o1", "o2" },
+            { "p1", "p2" },
+            { "onMessage", "onCommand" },
+            rule::action_type::drop
+        ));
+        daemon_->rules().add(rule(
+            { "s1", },
+            { "c1", },
+            { "o1", },
+            { "p1", },
+            { "onMessage", },
+            rule::action_type::accept
+        ));
+    }
+};
+
+} // !namespace
+
+BOOST_FIXTURE_TEST_SUITE(rule_remove_test_suite, rule_remove_test)
+
+BOOST_AUTO_TEST_CASE(basic)
+{
+    nlohmann::json result;
+
+    ctl_->send({
+        { "command",    "rule-remove"   },
+        { "index",      1               }
+    });
+    ctl_->recv([&] (auto, auto msg) {
+        result = msg;
+    });
+
+    wait_for([&] () {
+        return result.is_object();
+    });
+
+    BOOST_TEST(result.is_object());
+
+    result = nullptr;
+    ctl_->send({{ "command", "rule-list" }});
+    ctl_->recv([&] (auto, auto msg) {
+        result = msg;
+    });
+
+    wait_for([&] () {
+        return result.is_object();
+    });
+
+    BOOST_TEST(result["list"].is_array());
+    BOOST_TEST(result["list"].size() == 1U);
+
+    auto servers = result["list"][0]["servers"];
+    auto channels = result["list"][0]["channels"];
+    auto plugins = result["list"][0]["plugins"];
+    auto events = result["list"][0]["events"];
+
+    BOOST_TEST(json_util::contains(servers, "s1"));
+    BOOST_TEST(json_util::contains(servers, "s2"));
+    BOOST_TEST(json_util::contains(channels, "c1"));
+    BOOST_TEST(json_util::contains(channels, "c2"));
+    BOOST_TEST(json_util::contains(plugins, "p1"));
+    BOOST_TEST(json_util::contains(plugins, "p2"));
+    BOOST_TEST(json_util::contains(events, "onMessage"));
+    BOOST_TEST(json_util::contains(events, "onCommand"));
+    BOOST_TEST(result["list"][0]["action"].get<std::string>() == "drop");
+}
+
+BOOST_AUTO_TEST_CASE(empty)
+{
+    nlohmann::json result;
+
+    daemon_->rules().remove(0);
+    daemon_->rules().remove(0);
+
+    ctl_->send({
+        { "command",    "rule-remove"   },
+        { "index",      1               }
+    });
+    ctl_->recv([&] (auto, auto msg) {
+        result = msg;
+    });
+
+    wait_for([&] () {
+        return result.is_object();
+    });
+
+    BOOST_TEST(result.is_object());
+}
+
+BOOST_AUTO_TEST_SUITE(errors)
+
+BOOST_AUTO_TEST_CASE(invalid_index_1)
+{
+    boost::system::error_code result;
+    nlohmann::json message;
+
+    ctl_->send({
+        { "command",    "rule-remove"   },
+        { "index",      -100            }
+    });
+    ctl_->recv([&] (auto rresult, auto rmessage) {
+        result = rresult;
+        message = rmessage;
+    });
+
+    wait_for([&] {
+        return result;
+    });
+
+    BOOST_TEST(result == rule_error::invalid_index);
+    BOOST_TEST(message["error"].template get<int>() == rule_error::invalid_index);
+    BOOST_TEST(message["errorCategory"].template get<std::string>() == "rule");
+}
+
+BOOST_AUTO_TEST_CASE(invalid_index_2)
+{
+    boost::system::error_code result;
+    nlohmann::json message;
+
+    ctl_->send({
+        { "command",    "rule-remove"   },
+        { "index",      100             }
+    });
+    ctl_->recv([&] (auto rresult, auto rmessage) {
+        result = rresult;
+        message = rmessage;
+    });
+
+    wait_for([&] {
+        return result;
+    });
+
+    BOOST_TEST(result == rule_error::invalid_index);
+    BOOST_TEST(message["error"].template get<int>() == rule_error::invalid_index);
+    BOOST_TEST(message["errorCategory"].template get<std::string>() == "rule");
+}
+
+BOOST_AUTO_TEST_CASE(invalid_index_3)
+{
+    boost::system::error_code result;
+    nlohmann::json message;
+
+    ctl_->send({
+        { "command",    "rule-remove"   },
+        { "index",      "notaint"       }
+    });
+    ctl_->recv([&] (auto rresult, auto rmessage) {
+        result = rresult;
+        message = rmessage;
+    });
+
+    wait_for([&] {
+        return result;
+    });
+
+    BOOST_TEST(result == rule_error::invalid_index);
+    BOOST_TEST(message["error"].template get<int>() == rule_error::invalid_index);
+    BOOST_TEST(message["errorCategory"].template get<std::string>() == "rule");
+}
+
+BOOST_AUTO_TEST_SUITE_END()
+
+BOOST_AUTO_TEST_SUITE_END()
+
+} // !irccd
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/src/libirccd/command-server-connect/CMakeLists.txt	Fri Dec 15 15:37:58 2017 +0100
@@ -0,0 +1,23 @@
+#
+# CMakeLists.txt -- CMake build system for irccd
+#
+# Copyright (c) 2013-2017 David Demelier <markand@malikania.fr>
+#
+# Permission to use, copy, modify, and/or distribute this software for any
+# purpose with or without fee is hereby granted, provided that the above
+# copyright notice and this permission notice appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+#
+
+irccd_define_test(
+    NAME command-server-connect
+    SOURCES main.cpp
+    LIBRARIES libirccd libirccdctl
+)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/src/libirccd/command-server-connect/main.cpp	Fri Dec 15 15:37:58 2017 +0100
@@ -0,0 +1,336 @@
+/*
+ * main.cpp -- test server-connect remote command
+ *
+ * Copyright (c) 2013-2017 David Demelier <markand@malikania.fr>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#define BOOST_TEST_MODULE "server-connect"
+#include <boost/test/unit_test.hpp>
+
+#include <irccd/daemon/server_service.hpp>
+
+#include <irccd/test/command_test.hpp>
+#include <irccd/test/journal_server.hpp>
+
+namespace irccd {
+
+BOOST_FIXTURE_TEST_SUITE(server_connect_test_suite, command_test<server_connect_command>)
+
+BOOST_AUTO_TEST_CASE(minimal)
+{
+    nlohmann::json result;
+
+    ctl_->send({
+        { "command",    "server-connect"    },
+        { "name",       "local"             },
+        { "host",       "irc.example.org"   }
+    });
+    ctl_->recv([&] (auto, auto msg) {
+        result = msg;
+    });
+
+    wait_for([&] () {
+        return result.is_object();
+    });
+
+    auto s = daemon_->servers().get("local");
+
+    BOOST_TEST(s);
+    BOOST_TEST(s->name() == "local");
+    BOOST_TEST(s->host() == "irc.example.org");
+    BOOST_TEST(s->port() == 6667U);
+}
+
+#if defined(HAVE_SSL)
+
+BOOST_AUTO_TEST_CASE(full)
+{
+    nlohmann::json result;
+
+    ctl_->send({
+        { "command",    "server-connect"    },
+        { "name",       "local2"            },
+        { "host",       "irc.example2.org"  },
+        { "password",   "nonono"            },
+        { "nickname",   "francis"           },
+        { "realname",   "the_francis"       },
+        { "username",   "frc"               },
+        { "ctcpVersion", "ultra bot"        },
+        { "commandChar", "::"               },
+        { "port",       18000               },
+        { "ssl",        true                },
+        { "sslVerify",  true                },
+        { "autoRejoin", true                },
+        { "joinInvite", true                }
+    });
+    ctl_->recv([&] (auto, auto msg) {
+        result = msg;
+    });
+
+    wait_for([&] () {
+        return result.is_object();
+    });
+
+    auto s = daemon_->servers().get("local2");
+
+    BOOST_TEST(s);
+    BOOST_TEST(s->name() == "local2");
+    BOOST_TEST(s->host() == "irc.example2.org");
+    BOOST_TEST(s->port() == 18000U);
+    BOOST_TEST(s->password() == "nonono");
+    BOOST_TEST(s->nickname() == "francis");
+    BOOST_TEST(s->realname() == "the_francis");
+    BOOST_TEST(s->username() == "frc");
+    BOOST_TEST(s->command_char() == "::");
+    BOOST_TEST(s->ctcp_version() == "ultra bot");
+    BOOST_TEST(s->flags() & server::ssl);
+    BOOST_TEST(s->flags() & server::ssl_verify);
+    BOOST_TEST(s->flags() & server::auto_rejoin);
+    BOOST_TEST(s->flags() & server::join_invite);
+}
+
+#endif // !HAVE_SSL
+
+BOOST_AUTO_TEST_SUITE(errors)
+
+BOOST_AUTO_TEST_CASE(already_exists)
+{
+    boost::system::error_code result;
+    nlohmann::json message;
+
+    daemon_->servers().add(std::make_unique<journal_server>(service_, "local"));
+    ctl_->send({
+        { "command",    "server-connect"    },
+        { "name",       "local"             },
+        { "host",       "127.0.0.1"         }
+    });
+    ctl_->recv([&] (auto rresult, auto rmessage) {
+        result = rresult;
+        message = rmessage;
+    });
+
+    wait_for([&] {
+        return result;
+    });
+
+    BOOST_TEST(result == server_error::already_exists);
+    BOOST_TEST(message["error"].template get<int>() == server_error::already_exists);
+    BOOST_TEST(message["errorCategory"].template get<std::string>() == "server");
+}
+
+BOOST_AUTO_TEST_CASE(invalid_hostname_1)
+{
+    boost::system::error_code result;
+    nlohmann::json message;
+
+    ctl_->send({
+        { "command",    "server-connect"    },
+        { "name",       "new"               },
+    });
+    ctl_->recv([&] (auto rresult, auto rmessage) {
+        result = rresult;
+        message = rmessage;
+    });
+
+    wait_for([&] {
+        return result;
+    });
+
+    BOOST_TEST(result == server_error::invalid_hostname);
+    BOOST_TEST(message["error"].template get<int>() == server_error::invalid_hostname);
+    BOOST_TEST(message["errorCategory"].template get<std::string>() == "server");
+}
+
+BOOST_AUTO_TEST_CASE(invalid_hostname_2)
+{
+    boost::system::error_code result;
+    nlohmann::json message;
+
+    ctl_->send({
+        { "command",    "server-connect"    },
+        { "name",       "new"               },
+        { "host",       123456              }
+    });
+    ctl_->recv([&] (auto rresult, auto rmessage) {
+        result = rresult;
+        message = rmessage;
+    });
+
+    wait_for([&] {
+        return result;
+    });
+
+    BOOST_TEST(result == server_error::invalid_hostname);
+    BOOST_TEST(message["error"].template get<int>() == server_error::invalid_hostname);
+    BOOST_TEST(message["errorCategory"].template get<std::string>() == "server");
+}
+
+BOOST_AUTO_TEST_CASE(invalid_identifier_1)
+{
+    boost::system::error_code result;
+    nlohmann::json message;
+
+    ctl_->send({
+        { "command",    "server-connect"    },
+        { "name",       ""                  },
+        { "host",       "127.0.0.1"         }
+    });
+    ctl_->recv([&] (auto rresult, auto rmessage) {
+        result = rresult;
+        message = rmessage;
+    });
+
+    wait_for([&] {
+        return result;
+    });
+
+    BOOST_TEST(result == server_error::invalid_identifier);
+    BOOST_TEST(message["error"].template get<int>() == server_error::invalid_identifier);
+    BOOST_TEST(message["errorCategory"].template get<std::string>() == "server");
+}
+
+BOOST_AUTO_TEST_CASE(invalid_identifier_2)
+{
+    boost::system::error_code result;
+    nlohmann::json message;
+
+    ctl_->send({
+        { "command",    "server-connect"    },
+        { "name",       123456              },
+        { "host",       "127.0.0.1"         }
+    });
+    ctl_->recv([&] (auto rresult, auto rmessage) {
+        result = rresult;
+        message = rmessage;
+    });
+
+    wait_for([&] {
+        return result;
+    });
+
+    BOOST_TEST(result == server_error::invalid_identifier);
+    BOOST_TEST(message["error"].template get<int>() == server_error::invalid_identifier);
+    BOOST_TEST(message["errorCategory"].template get<std::string>() == "server");
+}
+
+BOOST_AUTO_TEST_CASE(invalid_port_1)
+{
+    boost::system::error_code result;
+    nlohmann::json message;
+
+    ctl_->send({
+        { "command",    "server-connect"    },
+        { "name",       "new"               },
+        { "host",       "127.0.0.1"         },
+        { "port",       "notaint"           }
+    });
+    ctl_->recv([&] (auto rresult, auto rmessage) {
+        result = rresult;
+        message = rmessage;
+    });
+
+    wait_for([&] {
+        return result;
+    });
+
+    BOOST_TEST(result == server_error::invalid_port);
+    BOOST_TEST(message["error"].template get<int>() == server_error::invalid_port);
+    BOOST_TEST(message["errorCategory"].template get<std::string>() == "server");
+}
+
+BOOST_AUTO_TEST_CASE(invalid_port_2)
+{
+    boost::system::error_code result;
+    nlohmann::json message;
+
+    ctl_->send({
+        { "command",    "server-connect"    },
+        { "name",       "new"               },
+        { "host",       "127.0.0.1"         },
+        { "port",       -123                }
+    });
+    ctl_->recv([&] (auto rresult, auto rmessage) {
+        result = rresult;
+        message = rmessage;
+    });
+
+    wait_for([&] {
+        return result;
+    });
+
+    BOOST_TEST(result == server_error::invalid_port);
+    BOOST_TEST(message["error"].template get<int>() == server_error::invalid_port);
+    BOOST_TEST(message["errorCategory"].template get<std::string>() == "server");
+}
+
+BOOST_AUTO_TEST_CASE(invalid_port_3)
+{
+    boost::system::error_code result;
+    nlohmann::json message;
+
+    ctl_->send({
+        { "command",    "server-connect"    },
+        { "name",       "new"               },
+        { "host",       "127.0.0.1"         },
+        { "port",       1000000             }
+    });
+    ctl_->recv([&] (auto rresult, auto rmessage) {
+        result = rresult;
+        message = rmessage;
+    });
+
+    wait_for([&] {
+        return result;
+    });
+
+    BOOST_TEST(result == server_error::invalid_port);
+    BOOST_TEST(message["error"].template get<int>() == server_error::invalid_port);
+    BOOST_TEST(message["errorCategory"].template get<std::string>() == "server");
+}
+
+#if !defined(HAVE_SSL)
+
+BOOST_AUTO_TEST_CASE(ssl_disabled)
+{
+    boost::system::error_code result;
+    nlohmann::json message;
+
+    ctl_->send({
+        { "command",    "server-connect"    },
+        { "name",       "new"               },
+        { "host",       "127.0.0.1"         },
+        { "ssl",        true                }
+    });
+    ctl_->recv([&] (auto rresult, auto rmessage) {
+        result = rresult;
+        message = rmessage;
+    });
+
+    wait_for([&] {
+        return result;
+    });
+
+    BOOST_TEST(result == server_error::ssl_disabled);
+    BOOST_TEST(message["error"].template get<int>() == server_error::ssl_disabled);
+    BOOST_TEST(message["errorCategory"].template get<std::string>() == "server");
+}
+
+#endif
+
+BOOST_AUTO_TEST_SUITE_END()
+
+BOOST_AUTO_TEST_SUITE_END()
+
+} // !irccd
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/src/libirccd/command-server-disconnect/CMakeLists.txt	Fri Dec 15 15:37:58 2017 +0100
@@ -0,0 +1,23 @@
+#
+# CMakeLists.txt -- CMake build system for irccd
+#
+# Copyright (c) 2013-2017 David Demelier <markand@malikania.fr>
+#
+# Permission to use, copy, modify, and/or distribute this software for any
+# purpose with or without fee is hereby granted, provided that the above
+# copyright notice and this permission notice appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+#
+
+irccd_define_test(
+    NAME command-server-disconnect
+    SOURCES main.cpp
+    LIBRARIES libirccd libirccdctl
+)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/src/libirccd/command-server-disconnect/main.cpp	Fri Dec 15 15:37:58 2017 +0100
@@ -0,0 +1,135 @@
+/*
+ * main.cpp -- test server-disconnect remote command
+ *
+ * Copyright (c) 2013-2017 David Demelier <markand@malikania.fr>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#define BOOST_TEST_MODULE "server-disconnect"
+#include <boost/test/unit_test.hpp>
+
+#include <irccd/daemon/server_service.hpp>
+
+#include <irccd/test/journal_server.hpp>
+#include <irccd/test/command_test.hpp>
+
+namespace irccd {
+
+namespace {
+
+class server_disconnect_test : public command_test<server_disconnect_command> {
+protected:
+    server_disconnect_test()
+    {
+        daemon_->servers().add(std::make_unique<journal_server>(service_, "s1"));
+        daemon_->servers().add(std::make_unique<journal_server>(service_, "s2"));
+    }
+};
+
+} // !namespace
+
+BOOST_FIXTURE_TEST_SUITE(server_disconnect_test_suite, server_disconnect_test)
+
+BOOST_AUTO_TEST_CASE(one)
+{
+    nlohmann::json result;
+
+    ctl_->send({
+        { "command",    "server-disconnect" },
+        { "server",     "s1"                }
+    });
+    ctl_->recv([&] (auto, auto msg) {
+        result = msg;
+    });
+
+    wait_for([&] () {
+        return result.is_object();
+    });
+
+    BOOST_TEST(result["command"].get<std::string>() == "server-disconnect");
+    BOOST_TEST(!daemon_->servers().has("s1"));
+    BOOST_TEST(daemon_->servers().has("s2"));
+}
+
+BOOST_AUTO_TEST_CASE(all)
+{
+    nlohmann::json result;
+
+    ctl_->send({{"command", "server-disconnect"}});
+    ctl_->recv([&] (auto, auto msg) {
+        result = msg;
+    });
+
+    wait_for([&] () {
+        return result.is_object();
+    });
+
+    BOOST_TEST(result["command"].get<std::string>() == "server-disconnect");
+    BOOST_TEST(!daemon_->servers().has("s1"));
+    BOOST_TEST(!daemon_->servers().has("s2"));
+}
+
+BOOST_AUTO_TEST_SUITE(errors)
+
+BOOST_AUTO_TEST_CASE(invalid_identifier)
+{
+    boost::system::error_code result;
+    nlohmann::json message;
+
+    ctl_->send({
+        { "command",    "server-disconnect" },
+        { "server",     123456              }
+    });
+    ctl_->recv([&] (auto rresult, auto rmessage) {
+        result = rresult;
+        message = rmessage;
+    });
+
+    wait_for([&] {
+        return result;
+    });
+
+    BOOST_TEST(result == server_error::invalid_identifier);
+    BOOST_TEST(message["error"].template get<int>() == server_error::invalid_identifier);
+    BOOST_TEST(message["errorCategory"].template get<std::string>() == "server");
+}
+
+BOOST_AUTO_TEST_CASE(not_found)
+{
+    boost::system::error_code result;
+    nlohmann::json message;
+
+    ctl_->send({
+        { "command",    "server-disconnect" },
+        { "server",     "unknown"           }
+    });
+    ctl_->recv([&] (auto rresult, auto rmessage) {
+        result = rresult;
+        message = rmessage;
+    });
+
+    wait_for([&] {
+        return result;
+    });
+
+    BOOST_TEST(result == server_error::not_found);
+    BOOST_TEST(message["error"].template get<int>() == server_error::not_found);
+    BOOST_TEST(message["errorCategory"].template get<std::string>() == "server");
+}
+
+BOOST_AUTO_TEST_SUITE_END()
+
+BOOST_AUTO_TEST_SUITE_END()
+
+} // !irccd
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/src/libirccd/command-server-info/CMakeLists.txt	Fri Dec 15 15:37:58 2017 +0100
@@ -0,0 +1,23 @@
+#
+# CMakeLists.txt -- CMake build system for irccd
+#
+# Copyright (c) 2013-2017 David Demelier <markand@malikania.fr>
+#
+# Permission to use, copy, modify, and/or distribute this software for any
+# purpose with or without fee is hereby granted, provided that the above
+# copyright notice and this permission notice appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+#
+
+irccd_define_test(
+    NAME command-server-info
+    SOURCES main.cpp
+    LIBRARIES libirccd libirccdctl
+)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/src/libirccd/command-server-info/main.cpp	Fri Dec 15 15:37:58 2017 +0100
@@ -0,0 +1,145 @@
+/*
+ * main.cpp -- test server-info remote command
+ *
+ * Copyright (c) 2013-2017 David Demelier <markand@malikania.fr>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#define BOOST_TEST_MODULE "server-info"
+#include <boost/test/unit_test.hpp>
+
+#include <irccd/daemon/server_service.hpp>
+
+#include <irccd/test/command_test.hpp>
+#include <irccd/test/journal_server.hpp>
+
+namespace irccd {
+
+BOOST_FIXTURE_TEST_SUITE(server_info_test_suite, command_test<server_info_command>)
+
+BOOST_AUTO_TEST_CASE(basic)
+{
+    auto server = std::make_unique<journal_server>(service_, "test");
+
+    server->set_host("example.org");
+    server->set_port(8765);
+    server->set_password("none");
+    server->set_nickname("pascal");
+    server->set_username("psc");
+    server->set_realname("Pascal le grand frere");
+    server->set_ctcp_version("yeah");
+    server->set_command_char("@");
+    server->set_reconnect_tries(80);
+    server->set_ping_timeout(20000);
+
+    nlohmann::json result;
+
+    daemon_->servers().add(std::move(server));
+    ctl_->send({
+        { "command",    "server-info"       },
+        { "server",     "test"              },
+    });
+    ctl_->recv([&] (auto, auto msg) {
+        result = msg;
+    });
+
+    wait_for([&] () {
+        return result.is_object();
+    });
+
+    BOOST_TEST(result.is_object());
+    BOOST_TEST(result["host"].get<std::string>() == "example.org");
+    BOOST_TEST(result["name"].get<std::string>() == "test");
+    BOOST_TEST(result["nickname"].get<std::string>() == "pascal");
+    BOOST_TEST(result["port"].get<int>() == 8765);
+    BOOST_TEST(result["realname"].get<std::string>() == "Pascal le grand frere");
+    BOOST_TEST(result["username"].get<std::string>() == "psc");
+}
+
+BOOST_AUTO_TEST_SUITE(errors)
+
+BOOST_AUTO_TEST_CASE(invalid_identifier_1)
+{
+    boost::system::error_code result;
+    nlohmann::json message;
+
+    ctl_->send({
+        { "command",    "server-info"   },
+        { "server",     123456          }
+    });
+    ctl_->recv([&] (auto rresult, auto rmessage) {
+        result = rresult;
+        message = rmessage;
+    });
+
+    wait_for([&] {
+        return result;
+    });
+
+    BOOST_TEST(result == server_error::invalid_identifier);
+    BOOST_TEST(message["error"].template get<int>() == server_error::invalid_identifier);
+    BOOST_TEST(message["errorCategory"].template get<std::string>() == "server");
+}
+
+BOOST_AUTO_TEST_CASE(invalid_identifier_2)
+{
+    boost::system::error_code result;
+    nlohmann::json message;
+
+    ctl_->send({
+        { "command",    "server-info"   },
+        { "server",     ""              }
+    });
+    ctl_->recv([&] (auto rresult, auto rmessage) {
+        result = rresult;
+        message = rmessage;
+    });
+
+    wait_for([&] {
+        return result;
+    });
+
+    BOOST_TEST(result == server_error::invalid_identifier);
+    BOOST_TEST(message["error"].template get<int>() == server_error::invalid_identifier);
+    BOOST_TEST(message["errorCategory"].template get<std::string>() == "server");
+}
+
+BOOST_AUTO_TEST_CASE(not_found)
+{
+    boost::system::error_code result;
+    nlohmann::json message;
+
+    ctl_->send({
+        { "command",    "server-info"   },
+        { "server",     "unknown"       }
+    });
+    ctl_->recv([&] (auto rresult, auto rmessage) {
+        result = rresult;
+        message = rmessage;
+    });
+
+    wait_for([&] {
+        return result;
+    });
+
+    BOOST_TEST(result == server_error::not_found);
+    BOOST_TEST(message["error"].template get<int>() == server_error::not_found);
+    BOOST_TEST(message["errorCategory"].template get<std::string>() == "server");
+}
+
+BOOST_AUTO_TEST_SUITE_END()
+
+BOOST_AUTO_TEST_SUITE_END()
+
+} // !irccd
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/src/libirccd/command-server-invite/CMakeLists.txt	Fri Dec 15 15:37:58 2017 +0100
@@ -0,0 +1,23 @@
+#
+# CMakeLists.txt -- CMake build system for irccd
+#
+# Copyright (c) 2013-2017 David Demelier <markand@malikania.fr>
+#
+# Permission to use, copy, modify, and/or distribute this software for any
+# purpose with or without fee is hereby granted, provided that the above
+# copyright notice and this permission notice appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+#
+
+irccd_define_test(
+    NAME command-server-invite
+    SOURCES main.cpp
+    LIBRARIES libirccd libirccdctl
+)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/src/libirccd/command-server-invite/main.cpp	Fri Dec 15 15:37:58 2017 +0100
@@ -0,0 +1,246 @@
+/*
+ * main.cpp -- test server-invite remote command
+ *
+ * Copyright (c) 2013-2017 David Demelier <markand@malikania.fr>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#define BOOST_TEST_MODULE "server-invite"
+#include <boost/test/unit_test.hpp>
+
+#include <irccd/daemon/server_service.hpp>
+
+#include <irccd/test/command_test.hpp>
+#include <irccd/test/journal_server.hpp>
+
+namespace irccd {
+
+namespace {
+
+class server_invite_test : public command_test<server_invite_command> {
+protected:
+    std::shared_ptr<journal_server> server_{new journal_server(service_, "test")};
+
+    server_invite_test()
+    {
+        daemon_->servers().add(server_);
+    }
+};
+
+} // !namespace
+
+BOOST_FIXTURE_TEST_SUITE(server_invite_test_suite, server_invite_test)
+
+BOOST_AUTO_TEST_CASE(basic)
+{
+    ctl_->send({
+        { "command",    "server-invite"     },
+        { "server",     "test"              },
+        { "target",     "francis"           },
+        { "channel",    "#music"            }
+    });
+
+    wait_for([this] () {
+        return !server_->cqueue().empty();
+    });
+
+    auto cmd = server_->cqueue().back();
+
+    BOOST_TEST(cmd["command"].get<std::string>() == "invite");
+    BOOST_TEST(cmd["channel"].get<std::string>() == "#music");
+    BOOST_TEST(cmd["target"].get<std::string>() == "francis");
+}
+
+BOOST_AUTO_TEST_SUITE(errors)
+
+BOOST_AUTO_TEST_CASE(invalid_identifier_1)
+{
+    boost::system::error_code result;
+    nlohmann::json message;
+
+    ctl_->send({
+        { "command",    "server-invite" },
+        { "server",     123456          },
+        { "target",     "francis"       },
+        { "channel",    "#music"        }
+    });
+    ctl_->recv([&] (auto rresult, auto rmessage) {
+        result = rresult;
+        message = rmessage;
+    });
+
+    wait_for([&] {
+        return result;
+    });
+
+    BOOST_TEST(result == server_error::invalid_identifier);
+    BOOST_TEST(message["error"].template get<int>() == server_error::invalid_identifier);
+    BOOST_TEST(message["errorCategory"].template get<std::string>() == "server");
+}
+
+BOOST_AUTO_TEST_CASE(invalid_identifier_2)
+{
+    boost::system::error_code result;
+    nlohmann::json message;
+
+    ctl_->send({
+        { "command",    "server-invite" },
+        { "server",     ""              },
+        { "target",     "francis"       },
+        { "channel",    "#music"        }
+    });
+    ctl_->recv([&] (auto rresult, auto rmessage) {
+        result = rresult;
+        message = rmessage;
+    });
+
+    wait_for([&] {
+        return result;
+    });
+
+    BOOST_TEST(result == server_error::invalid_identifier);
+    BOOST_TEST(message["error"].template get<int>() == server_error::invalid_identifier);
+    BOOST_TEST(message["errorCategory"].template get<std::string>() == "server");
+}
+
+BOOST_AUTO_TEST_CASE(invalid_nickname_1)
+{
+    boost::system::error_code result;
+    nlohmann::json message;
+
+    ctl_->send({
+        { "command",    "server-invite" },
+        { "server",     "test"          },
+        { "target",     ""              },
+        { "channel",    "#music"        }
+    });
+    ctl_->recv([&] (auto rresult, auto rmessage) {
+        result = rresult;
+        message = rmessage;
+    });
+
+    wait_for([&] {
+        return result;
+    });
+
+    BOOST_TEST(result == server_error::invalid_nickname);
+    BOOST_TEST(message["error"].template get<int>() == server_error::invalid_nickname);
+    BOOST_TEST(message["errorCategory"].template get<std::string>() == "server");
+}
+
+BOOST_AUTO_TEST_CASE(invalid_nickname_2)
+{
+    boost::system::error_code result;
+    nlohmann::json message;
+
+    ctl_->send({
+        { "command",    "server-invite" },
+        { "server",     "test"          },
+        { "target",     123456          },
+        { "channel",    "#music"        }
+    });
+    ctl_->recv([&] (auto rresult, auto rmessage) {
+        result = rresult;
+        message = rmessage;
+    });
+
+    wait_for([&] {
+        return result;
+    });
+
+    BOOST_TEST(result == server_error::invalid_nickname);
+    BOOST_TEST(message["error"].template get<int>() == server_error::invalid_nickname);
+    BOOST_TEST(message["errorCategory"].template get<std::string>() == "server");
+}
+
+BOOST_AUTO_TEST_CASE(invalid_channel_1)
+{
+    boost::system::error_code result;
+    nlohmann::json message;
+
+    ctl_->send({
+        { "command",    "server-invite" },
+        { "server",     "test"          },
+        { "target",     "jean"          },
+        { "channel",    ""              }
+    });
+    ctl_->recv([&] (auto rresult, auto rmessage) {
+        result = rresult;
+        message = rmessage;
+    });
+
+    wait_for([&] {
+        return result;
+    });
+
+    BOOST_TEST(result == server_error::invalid_channel);
+    BOOST_TEST(message["error"].template get<int>() == server_error::invalid_channel);
+    BOOST_TEST(message["errorCategory"].template get<std::string>() == "server");
+}
+
+BOOST_AUTO_TEST_CASE(invalid_channel_2)
+{
+    boost::system::error_code result;
+    nlohmann::json message;
+
+    ctl_->send({
+        { "command",    "server-invite" },
+        { "server",     "test"          },
+        { "target",     "jean"          },
+        { "channel",    123456          }
+    });
+    ctl_->recv([&] (auto rresult, auto rmessage) {
+        result = rresult;
+        message = rmessage;
+    });
+
+    wait_for([&] {
+        return result;
+    });
+
+    BOOST_TEST(result == server_error::invalid_channel);
+    BOOST_TEST(message["error"].template get<int>() == server_error::invalid_channel);
+    BOOST_TEST(message["errorCategory"].template get<std::string>() == "server");
+}
+
+BOOST_AUTO_TEST_CASE(not_found)
+{
+    boost::system::error_code result;
+    nlohmann::json message;
+
+    ctl_->send({
+        { "command",    "server-invite" },
+        { "server",     "unknown"       },
+        { "target",     "francis"       },
+        { "channel",    "#music"        }
+    });
+    ctl_->recv([&] (auto rresult, auto rmessage) {
+        result = rresult;
+        message = rmessage;
+    });
+
+    wait_for([&] {
+        return result;
+    });
+
+    BOOST_TEST(result == server_error::not_found);
+    BOOST_TEST(message["error"].template get<int>() == server_error::not_found);
+    BOOST_TEST(message["errorCategory"].template get<std::string>() == "server");
+}
+
+BOOST_AUTO_TEST_SUITE_END()
+
+BOOST_AUTO_TEST_SUITE_END()
+
+} // !irccd
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/src/libirccd/command-server-join/CMakeLists.txt	Fri Dec 15 15:37:58 2017 +0100
@@ -0,0 +1,23 @@
+#
+# CMakeLists.txt -- CMake build system for irccd
+#
+# Copyright (c) 2013-2017 David Demelier <markand@malikania.fr>
+#
+# Permission to use, copy, modify, and/or distribute this software for any
+# purpose with or without fee is hereby granted, provided that the above
+# copyright notice and this permission notice appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+#
+
+irccd_define_test(
+    NAME command-server-join
+    SOURCES main.cpp
+    LIBRARIES libirccd libirccdctl
+)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/src/libirccd/command-server-join/main.cpp	Fri Dec 15 15:37:58 2017 +0100
@@ -0,0 +1,210 @@
+/*
+ * main.cpp -- test server-join remote command
+ *
+ * Copyright (c) 2013-2017 David Demelier <markand@malikania.fr>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#define BOOST_TEST_MODULE "server-join"
+#include <boost/test/unit_test.hpp>
+
+#include <irccd/daemon/server_service.hpp>
+
+#include <irccd/test/command_test.hpp>
+#include <irccd/test/journal_server.hpp>
+
+namespace irccd {
+
+namespace {
+
+class server_join_test : public command_test<server_join_command> {
+protected:
+    std::shared_ptr<journal_server> server_{new journal_server(service_, "test")};
+
+    server_join_test()
+    {
+        daemon_->servers().add(server_);
+    }
+};
+
+} // !namespace
+
+BOOST_FIXTURE_TEST_SUITE(server_join_test_suite, server_join_test)
+
+BOOST_AUTO_TEST_CASE(basic)
+{
+    ctl_->send({
+        { "command",    "server-join"       },
+        { "server",     "test"              },
+        { "channel",    "#music"            },
+        { "password",   "plop"              }
+    });
+
+    wait_for([this] () {
+        return !server_->cqueue().empty();
+    });
+
+    auto cmd = server_->cqueue().back();
+
+    BOOST_TEST(cmd["command"].get<std::string>() == "join");
+    BOOST_TEST(cmd["channel"].get<std::string>() == "#music");
+    BOOST_TEST(cmd["password"].get<std::string>() == "plop");
+}
+
+BOOST_AUTO_TEST_CASE(nopassword)
+{
+    ctl_->send({
+        { "command",    "server-join"       },
+        { "server",     "test"              },
+        { "channel",    "#music"            }
+    });
+
+    wait_for([this] () {
+        return !server_->cqueue().empty();
+    });
+
+    auto cmd = server_->cqueue().back();
+
+    BOOST_TEST(cmd["command"].get<std::string>() == "join");
+    BOOST_TEST(cmd["channel"].get<std::string>() == "#music");
+    BOOST_TEST(cmd["password"].get<std::string>() == "");
+}
+
+BOOST_AUTO_TEST_SUITE(errors)
+
+BOOST_AUTO_TEST_CASE(invalid_identifier_1)
+{
+    boost::system::error_code result;
+    nlohmann::json message;
+
+    ctl_->send({
+        { "command",    "server-join"   },
+        { "server",     123456          },
+        { "channel",    "#music"        }
+    });
+    ctl_->recv([&] (auto rresult, auto rmessage) {
+        result = rresult;
+        message = rmessage;
+    });
+
+    wait_for([&] {
+        return result;
+    });
+
+    BOOST_TEST(result == server_error::invalid_identifier);
+    BOOST_TEST(message["error"].template get<int>() == server_error::invalid_identifier);
+    BOOST_TEST(message["errorCategory"].template get<std::string>() == "server");
+}
+
+BOOST_AUTO_TEST_CASE(invalid_identifier_2)
+{
+    boost::system::error_code result;
+    nlohmann::json message;
+
+    ctl_->send({
+        { "command",    "server-join"   },
+        { "server",     ""              },
+        { "channel",    "#music"        }
+    });
+    ctl_->recv([&] (auto rresult, auto rmessage) {
+        result = rresult;
+        message = rmessage;
+    });
+
+    wait_for([&] {
+        return result;
+    });
+
+    BOOST_TEST(result == server_error::invalid_identifier);
+    BOOST_TEST(message["error"].template get<int>() == server_error::invalid_identifier);
+    BOOST_TEST(message["errorCategory"].template get<std::string>() == "server");
+}
+
+BOOST_AUTO_TEST_CASE(invalid_channel_1)
+{
+    boost::system::error_code result;
+    nlohmann::json message;
+
+    ctl_->send({
+        { "command",    "server-join"   },
+        { "server",     "test"          },
+        { "channel",    ""              }
+    });
+    ctl_->recv([&] (auto rresult, auto rmessage) {
+        result = rresult;
+        message = rmessage;
+    });
+
+    wait_for([&] {
+        return result;
+    });
+
+    BOOST_TEST(result == server_error::invalid_channel);
+    BOOST_TEST(message["error"].template get<int>() == server_error::invalid_channel);
+    BOOST_TEST(message["errorCategory"].template get<std::string>() == "server");
+}
+
+BOOST_AUTO_TEST_CASE(invalid_channel_2)
+{
+    boost::system::error_code result;
+    nlohmann::json message;
+
+    ctl_->send({
+        { "command",    "server-join"   },
+        { "server",     "test"          },
+        { "channel",    123456          }
+    });
+    ctl_->recv([&] (auto rresult, auto rmessage) {
+        result = rresult;
+        message = rmessage;
+    });
+
+    wait_for([&] {
+        return result;
+    });
+
+    BOOST_TEST(result == server_error::invalid_channel);
+    BOOST_TEST(message["error"].template get<int>() == server_error::invalid_channel);
+    BOOST_TEST(message["errorCategory"].template get<std::string>() == "server");
+}
+
+BOOST_AUTO_TEST_CASE(not_found)
+{
+    boost::system::error_code result;
+    nlohmann::json message;
+
+    ctl_->send({
+        { "command",    "server-join"   },
+        { "server",     "unknown"       },
+        { "channel",    "#music"        }
+    });
+    ctl_->recv([&] (auto rresult, auto rmessage) {
+        result = rresult;
+        message = rmessage;
+    });
+
+    wait_for([&] {
+        return result;
+    });
+
+    BOOST_TEST(result == server_error::not_found);
+    BOOST_TEST(message["error"].template get<int>() == server_error::not_found);
+    BOOST_TEST(message["errorCategory"].template get<std::string>() == "server");
+}
+
+BOOST_AUTO_TEST_SUITE_END()
+
+BOOST_AUTO_TEST_SUITE_END()
+
+} // !irccd
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/src/libirccd/command-server-kick/CMakeLists.txt	Fri Dec 15 15:37:58 2017 +0100
@@ -0,0 +1,23 @@
+#
+# CMakeLists.txt -- CMake build system for irccd
+#
+# Copyright (c) 2013-2017 David Demelier <markand@malikania.fr>
+#
+# Permission to use, copy, modify, and/or distribute this software for any
+# purpose with or without fee is hereby granted, provided that the above
+# copyright notice and this permission notice appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+#
+
+irccd_define_test(
+    NAME command-server-kick
+    SOURCES main.cpp
+    LIBRARIES libirccd libirccdctl
+)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/src/libirccd/command-server-kick/main.cpp	Fri Dec 15 15:37:58 2017 +0100
@@ -0,0 +1,269 @@
+/*
+ * main.cpp -- test server-kick remote command
+ *
+ * Copyright (c) 2013-2017 David Demelier <markand@malikania.fr>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#define BOOST_TEST_MODULE "server-kick"
+#include <boost/test/unit_test.hpp>
+
+#include <irccd/daemon/server_service.hpp>
+
+#include <irccd/test/command_test.hpp>
+#include <irccd/test/journal_server.hpp>
+
+namespace irccd {
+
+namespace {
+
+class server_kick_test : public command_test<server_kick_command> {
+protected:
+    std::shared_ptr<journal_server> server_{new journal_server(service_, "test")};
+
+    server_kick_test()
+    {
+        daemon_->servers().add(server_);
+    }
+};
+
+} // !namespace
+
+BOOST_FIXTURE_TEST_SUITE(server_kick_test_suite, server_kick_test)
+
+BOOST_AUTO_TEST_CASE(basic)
+{
+    ctl_->send({
+        { "command",    "server-kick"       },
+        { "server",     "test"              },
+        { "target",     "francis"           },
+        { "channel",    "#staff"            },
+        { "reason",     "too noisy"         }
+    });
+
+    wait_for([this] () {
+        return !server_->cqueue().empty();
+    });
+
+    auto cmd = server_->cqueue().back();
+
+    BOOST_TEST(cmd["command"].get<std::string>() == "kick");
+    BOOST_TEST(cmd["channel"].get<std::string>() == "#staff");
+    BOOST_TEST(cmd["target"].get<std::string>() == "francis");
+    BOOST_TEST(cmd["reason"].get<std::string>() == "too noisy");
+}
+
+BOOST_AUTO_TEST_CASE(noreason)
+{
+    ctl_->send({
+        { "command",    "server-kick"       },
+        { "server",     "test"              },
+        { "target",     "francis"           },
+        { "channel",    "#staff"            }
+    });
+
+    wait_for([this] () {
+        return !server_->cqueue().empty();
+    });
+
+    auto cmd = server_->cqueue().back();
+
+    BOOST_TEST(cmd["command"].get<std::string>() == "kick");
+    BOOST_TEST(cmd["channel"].get<std::string>() == "#staff");
+    BOOST_TEST(cmd["target"].get<std::string>() == "francis");
+    BOOST_TEST(cmd["reason"].get<std::string>() == "");
+}
+
+BOOST_AUTO_TEST_SUITE(errors)
+
+BOOST_AUTO_TEST_CASE(invalid_identifier_1)
+{
+    boost::system::error_code result;
+    nlohmann::json message;
+
+    ctl_->send({
+        { "command",    "server-kick"   },
+        { "server",     123456          },
+        { "target",     "francis"       },
+        { "channel",    "#music"        }
+    });
+    ctl_->recv([&] (auto rresult, auto rmessage) {
+        result = rresult;
+        message = rmessage;
+    });
+
+    wait_for([&] {
+        return result;
+    });
+
+    BOOST_TEST(result == server_error::invalid_identifier);
+    BOOST_TEST(message["error"].template get<int>() == server_error::invalid_identifier);
+    BOOST_TEST(message["errorCategory"].template get<std::string>() == "server");
+}
+
+BOOST_AUTO_TEST_CASE(invalid_identifier_2)
+{
+    boost::system::error_code result;
+    nlohmann::json message;
+
+    ctl_->send({
+        { "command",    "server-kick"   },
+        { "server",     ""              },
+        { "target",     "francis"       },
+        { "channel",    "#music"        }
+    });
+    ctl_->recv([&] (auto rresult, auto rmessage) {
+        result = rresult;
+        message = rmessage;
+    });
+
+    wait_for([&] {
+        return result;
+    });
+
+    BOOST_TEST(result == server_error::invalid_identifier);
+    BOOST_TEST(message["error"].template get<int>() == server_error::invalid_identifier);
+    BOOST_TEST(message["errorCategory"].template get<std::string>() == "server");
+}
+
+BOOST_AUTO_TEST_CASE(invalid_nickname_1)
+{
+    boost::system::error_code result;
+    nlohmann::json message;
+
+    ctl_->send({
+        { "command",    "server-kick"   },
+        { "server",     "test"          },
+        { "target",     ""              },
+        { "channel",    "#music"        }
+    });
+    ctl_->recv([&] (auto rresult, auto rmessage) {
+        result = rresult;
+        message = rmessage;
+    });
+
+    wait_for([&] {
+        return result;
+    });
+
+    BOOST_TEST(result == server_error::invalid_nickname);
+    BOOST_TEST(message["error"].template get<int>() == server_error::invalid_nickname);
+    BOOST_TEST(message["errorCategory"].template get<std::string>() == "server");
+}
+
+BOOST_AUTO_TEST_CASE(invalid_nickname_2)
+{
+    boost::system::error_code result;
+    nlohmann::json message;
+
+    ctl_->send({
+        { "command",    "server-kick"   },
+        { "server",     "test"          },
+        { "target",     123456          },
+        { "channel",    "#music"        }
+    });
+    ctl_->recv([&] (auto rresult, auto rmessage) {
+        result = rresult;
+        message = rmessage;
+    });
+
+    wait_for([&] {
+        return result;
+    });
+
+    BOOST_TEST(result == server_error::invalid_nickname);
+    BOOST_TEST(message["error"].template get<int>() == server_error::invalid_nickname);
+    BOOST_TEST(message["errorCategory"].template get<std::string>() == "server");
+}
+
+BOOST_AUTO_TEST_CASE(invalid_channel_1)
+{
+    boost::system::error_code result;
+    nlohmann::json message;
+
+    ctl_->send({
+        { "command",    "server-kick"   },
+        { "server",     "test"          },
+        { "target",     "jean"          },
+        { "channel",    ""              }
+    });
+    ctl_->recv([&] (auto rresult, auto rmessage) {
+        result = rresult;
+        message = rmessage;
+    });
+
+    wait_for([&] {
+        return result;
+    });
+
+    BOOST_TEST(result == server_error::invalid_channel);
+    BOOST_TEST(message["error"].template get<int>() == server_error::invalid_channel);
+    BOOST_TEST(message["errorCategory"].template get<std::string>() == "server");
+}
+
+BOOST_AUTO_TEST_CASE(invalid_channel_2)
+{
+    boost::system::error_code result;
+    nlohmann::json message;
+
+    ctl_->send({
+        { "command",    "server-kick"   },
+        { "server",     "test"          },
+        { "target",     "jean"          },
+        { "channel",    123456          }
+    });
+    ctl_->recv([&] (auto rresult, auto rmessage) {
+        result = rresult;
+        message = rmessage;
+    });
+
+    wait_for([&] {
+        return result;
+    });
+
+    BOOST_TEST(result == server_error::invalid_channel);
+    BOOST_TEST(message["error"].template get<int>() == server_error::invalid_channel);
+    BOOST_TEST(message["errorCategory"].template get<std::string>() == "server");
+}
+
+BOOST_AUTO_TEST_CASE(not_found)
+{
+    boost::system::error_code result;
+    nlohmann::json message;
+
+    ctl_->send({
+        { "command",    "server-kick"   },
+        { "server",     "unknown"       },
+        { "target",     "francis"       },
+        { "channel",    "#music"        }
+    });
+    ctl_->recv([&] (auto rresult, auto rmessage) {
+        result = rresult;
+        message = rmessage;
+    });
+
+    wait_for([&] {
+        return result;
+    });
+
+    BOOST_TEST(result == server_error::not_found);
+    BOOST_TEST(message["error"].template get<int>() == server_error::not_found);
+    BOOST_TEST(message["errorCategory"].template get<std::string>() == "server");
+}
+
+BOOST_AUTO_TEST_SUITE_END()
+
+BOOST_AUTO_TEST_SUITE_END()
+
+} // !irccd
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/src/libirccd/command-server-list/CMakeLists.txt	Fri Dec 15 15:37:58 2017 +0100
@@ -0,0 +1,23 @@
+#
+# CMakeLists.txt -- CMake build system for irccd
+#
+# Copyright (c) 2013-2017 David Demelier <markand@malikania.fr>
+#
+# Permission to use, copy, modify, and/or distribute this software for any
+# purpose with or without fee is hereby granted, provided that the above
+# copyright notice and this permission notice appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+#
+
+irccd_define_test(
+    NAME command-server-list
+    SOURCES main.cpp
+    LIBRARIES libirccd libirccdctl
+)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/src/libirccd/command-server-list/main.cpp	Fri Dec 15 15:37:58 2017 +0100
@@ -0,0 +1,66 @@
+/*
+ * main.cpp -- test server-list remote command
+ *
+ * Copyright (c) 2013-2017 David Demelier <markand@malikania.fr>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#define BOOST_TEST_MODULE "server-list"
+#include <boost/test/unit_test.hpp>
+
+#include <irccd/daemon/server_service.hpp>
+
+#include <irccd/test/command_test.hpp>
+#include <irccd/test/journal_server.hpp>
+
+namespace irccd {
+
+namespace {
+
+class server_list_test : public command_test<server_list_command> {
+protected:
+    server_list_test()
+    {
+        daemon_->servers().add(std::make_unique<journal_server>(service_, "s1"));
+        daemon_->servers().add(std::make_unique<journal_server>(service_, "s2"));
+    }
+};
+
+} // !namespace
+
+BOOST_FIXTURE_TEST_SUITE(server_list_test_suite, server_list_test)
+
+BOOST_AUTO_TEST_CASE(basic)
+{
+    nlohmann::json result;
+
+    ctl_->send({{"command", "server-list"}});
+    ctl_->recv([&] (auto, auto msg) {
+        result = msg;
+    });
+
+    wait_for([&] () {
+        return result.is_object();
+    });
+
+    BOOST_TEST(result.is_object());
+    BOOST_TEST(result["list"].is_array());
+    BOOST_TEST(result["list"].size() == 2U);
+    BOOST_TEST(result["list"][0].get<std::string>() == "s1");
+    BOOST_TEST(result["list"][1].get<std::string>() == "s2");
+}
+
+BOOST_AUTO_TEST_SUITE_END()
+
+} // !irccd
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/src/libirccd/command-server-me/CMakeLists.txt	Fri Dec 15 15:37:58 2017 +0100
@@ -0,0 +1,23 @@
+#
+# CMakeLists.txt -- CMake build system for irccd
+#
+# Copyright (c) 2013-2017 David Demelier <markand@malikania.fr>
+#
+# Permission to use, copy, modify, and/or distribute this software for any
+# purpose with or without fee is hereby granted, provided that the above
+# copyright notice and this permission notice appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+#
+
+irccd_define_test(
+    NAME command-server-me
+    SOURCES main.cpp
+    LIBRARIES libirccd libirccdctl
+)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/src/libirccd/command-server-me/main.cpp	Fri Dec 15 15:37:58 2017 +0100
@@ -0,0 +1,196 @@
+/*
+ * main.cpp -- test server-me remote command
+ *
+ * Copyright (c) 2013-2017 David Demelier <markand@malikania.fr>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#define BOOST_TEST_MODULE "server-me"
+#include <boost/test/unit_test.hpp>
+
+#include <irccd/daemon/server_service.hpp>
+
+#include <irccd/test/command_test.hpp>
+#include <irccd/test/journal_server.hpp>
+
+namespace irccd {
+
+namespace {
+
+class server_me_test : public command_test<server_me_command> {
+protected:
+    std::shared_ptr<journal_server> server_{new journal_server(service_, "test")};
+
+    server_me_test()
+    {
+        daemon_->servers().add(server_);
+    }
+};
+
+} // !namespace
+
+BOOST_FIXTURE_TEST_SUITE(server_me_test_suite, server_me_test)
+
+BOOST_AUTO_TEST_CASE(basic)
+{
+    ctl_->send({
+        { "command",    "server-me"         },
+        { "server",     "test"              },
+        { "target",     "jean"              },
+        { "message",    "hello!"            }
+    });
+
+    wait_for([this] () {
+        return !server_->cqueue().empty();
+    });
+
+    auto cmd = server_->cqueue().back();
+
+    BOOST_TEST(cmd["command"].get<std::string>() == "me");
+    BOOST_TEST(cmd["message"].get<std::string>() == "hello!");
+    BOOST_TEST(cmd["target"].get<std::string>() == "jean");
+}
+
+BOOST_AUTO_TEST_SUITE(errors)
+
+BOOST_AUTO_TEST_CASE(invalid_identifier_1)
+{
+    boost::system::error_code result;
+    nlohmann::json message;
+
+    ctl_->send({
+        { "command",    "server-me" },
+        { "server",     123456      },
+        { "target",     "#music"    },
+        { "message",    "hello!"    }
+    });
+    ctl_->recv([&] (auto rresult, auto rmessage) {
+        result = rresult;
+        message = rmessage;
+    });
+
+    wait_for([&] {
+        return result;
+    });
+
+    BOOST_TEST(result == server_error::invalid_identifier);
+    BOOST_TEST(message["error"].template get<int>() == server_error::invalid_identifier);
+    BOOST_TEST(message["errorCategory"].template get<std::string>() == "server");
+}
+
+BOOST_AUTO_TEST_CASE(invalid_identifier_2)
+{
+    boost::system::error_code result;
+    nlohmann::json message;
+
+    ctl_->send({
+        { "command",    "server-me" },
+        { "server",     ""          },
+        { "target",     "#music"    },
+        { "message",    "hello!"    }
+    });
+    ctl_->recv([&] (auto rresult, auto rmessage) {
+        result = rresult;
+        message = rmessage;
+    });
+
+    wait_for([&] {
+        return result;
+    });
+
+    BOOST_TEST(result == server_error::invalid_identifier);
+    BOOST_TEST(message["error"].template get<int>() == server_error::invalid_identifier);
+    BOOST_TEST(message["errorCategory"].template get<std::string>() == "server");
+}
+
+BOOST_AUTO_TEST_CASE(invalid_channel_1)
+{
+    boost::system::error_code result;
+    nlohmann::json message;
+
+    ctl_->send({
+        { "command",    "server-me" },
+        { "server",     "test"      },
+        { "target",     ""          },
+        { "message",    "hello!"    }
+    });
+    ctl_->recv([&] (auto rresult, auto rmessage) {
+        result = rresult;
+        message = rmessage;
+    });
+
+    wait_for([&] {
+        return result;
+    });
+
+    BOOST_TEST(result == server_error::invalid_channel);
+    BOOST_TEST(message["error"].template get<int>() == server_error::invalid_channel);
+    BOOST_TEST(message["errorCategory"].template get<std::string>() == "server");
+}
+
+BOOST_AUTO_TEST_CASE(invalid_channel_2)
+{
+    boost::system::error_code result;
+    nlohmann::json message;
+
+    ctl_->send({
+        { "command",    "server-me" },
+        { "server",     "test"      },
+        { "target",     123456      },
+        { "message",    "hello!"    }
+    });
+    ctl_->recv([&] (auto rresult, auto rmessage) {
+        result = rresult;
+        message = rmessage;
+    });
+
+    wait_for([&] {
+        return result;
+    });
+
+    BOOST_TEST(result == server_error::invalid_channel);
+    BOOST_TEST(message["error"].template get<int>() == server_error::invalid_channel);
+    BOOST_TEST(message["errorCategory"].template get<std::string>() == "server");
+}
+
+BOOST_AUTO_TEST_CASE(not_found)
+{
+    boost::system::error_code result;
+    nlohmann::json message;
+
+    ctl_->send({
+        { "command",    "server-me" },
+        { "server",     "unknown"   },
+        { "target",     "#music"    },
+        { "message",    "hello!"    }
+    });
+    ctl_->recv([&] (auto rresult, auto rmessage) {
+        result = rresult;
+        message = rmessage;
+    });
+
+    wait_for([&] {
+        return result;
+    });
+
+    BOOST_TEST(result == server_error::not_found);
+    BOOST_TEST(message["error"].template get<int>() == server_error::not_found);
+    BOOST_TEST(message["errorCategory"].template get<std::string>() == "server");
+}
+
+BOOST_AUTO_TEST_SUITE_END()
+
+BOOST_AUTO_TEST_SUITE_END()
+
+} // !irccd
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/src/libirccd/command-server-message/CMakeLists.txt	Fri Dec 15 15:37:58 2017 +0100
@@ -0,0 +1,23 @@
+#
+# CMakeLists.txt -- CMake build system for irccd
+#
+# Copyright (c) 2013-2017 David Demelier <markand@malikania.fr>
+#
+# Permission to use, copy, modify, and/or distribute this software for any
+# purpose with or without fee is hereby granted, provided that the above
+# copyright notice and this permission notice appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+#
+
+irccd_define_test(
+    NAME command-server-message
+    SOURCES main.cpp
+    LIBRARIES libirccd libirccdctl
+)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/src/libirccd/command-server-message/main.cpp	Fri Dec 15 15:37:58 2017 +0100
@@ -0,0 +1,196 @@
+/*
+ * main.cpp -- test server-message remote command
+ *
+ * Copyright (c) 2013-2017 David Demelier <markand@malikania.fr>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#define BOOST_TEST_MODULE "server-message"
+#include <boost/test/unit_test.hpp>
+
+#include <irccd/daemon/server_service.hpp>
+
+#include <irccd/test/command_test.hpp>
+#include <irccd/test/journal_server.hpp>
+
+namespace irccd {
+
+namespace {
+
+class server_message_test : public command_test<server_message_command> {
+protected:
+    std::shared_ptr<journal_server> server_{new journal_server(service_, "test")};
+
+    server_message_test()
+    {
+        daemon_->servers().add(server_);
+    }
+};
+
+} // !namespace
+
+BOOST_FIXTURE_TEST_SUITE(server_message_test_suite, server_message_test)
+
+BOOST_AUTO_TEST_CASE(basic)
+{
+    ctl_->send({
+        { "command",    "server-message"    },
+        { "server",     "test"              },
+        { "target",     "#staff"            },
+        { "message",    "plop!"             }
+    });
+
+    wait_for([this] () {
+        return !server_->cqueue().empty();
+    });
+
+    auto cmd = server_->cqueue().back();
+
+    BOOST_TEST(cmd["command"].get<std::string>() == "message");
+    BOOST_TEST(cmd["message"].get<std::string>() == "plop!");
+    BOOST_TEST(cmd["target"].get<std::string>() == "#staff");
+}
+
+BOOST_AUTO_TEST_SUITE(errors)
+
+BOOST_AUTO_TEST_CASE(invalid_identifier_1)
+{
+    boost::system::error_code result;
+    nlohmann::json message;
+
+    ctl_->send({
+        { "command",    "server-message"    },
+        { "server",     123456              },
+        { "target",     "#music"            },
+        { "message",    "plop!"             }
+    });
+    ctl_->recv([&] (auto rresult, auto rmessage) {
+        result = rresult;
+        message = rmessage;
+    });
+
+    wait_for([&] {
+        return result;
+    });
+
+    BOOST_TEST(result == server_error::invalid_identifier);
+    BOOST_TEST(message["error"].template get<int>() == server_error::invalid_identifier);
+    BOOST_TEST(message["errorCategory"].template get<std::string>() == "server");
+}
+
+BOOST_AUTO_TEST_CASE(invalid_identifier_2)
+{
+    boost::system::error_code result;
+    nlohmann::json message;
+
+    ctl_->send({
+        { "command",    "server-message"    },
+        { "server",     ""                  },
+        { "target",     "#music"            },
+        { "message",    "plop!"             }
+    });
+    ctl_->recv([&] (auto rresult, auto rmessage) {
+        result = rresult;
+        message = rmessage;
+    });
+
+    wait_for([&] {
+        return result;
+    });
+
+    BOOST_TEST(result == server_error::invalid_identifier);
+    BOOST_TEST(message["error"].template get<int>() == server_error::invalid_identifier);
+    BOOST_TEST(message["errorCategory"].template get<std::string>() == "server");
+}
+
+BOOST_AUTO_TEST_CASE(invalid_channel_1)
+{
+    boost::system::error_code result;
+    nlohmann::json message;
+
+    ctl_->send({
+        { "command",    "server-message"    },
+        { "server",     "test"              },
+        { "target",     ""                  },
+        { "message",    "plop!"             }
+    });
+    ctl_->recv([&] (auto rresult, auto rmessage) {
+        result = rresult;
+        message = rmessage;
+    });
+
+    wait_for([&] {
+        return result;
+    });
+
+    BOOST_TEST(result == server_error::invalid_channel);
+    BOOST_TEST(message["error"].template get<int>() == server_error::invalid_channel);
+    BOOST_TEST(message["errorCategory"].template get<std::string>() == "server");
+}
+
+BOOST_AUTO_TEST_CASE(invalid_channel_2)
+{
+    boost::system::error_code result;
+    nlohmann::json message;
+
+    ctl_->send({
+        { "command",    "server-message"    },
+        { "server",     "test"              },
+        { "target",     123456              },
+        { "message",    "plop!"             }
+    });
+    ctl_->recv([&] (auto rresult, auto rmessage) {
+        result = rresult;
+        message = rmessage;
+    });
+
+    wait_for([&] {
+        return result;
+    });
+
+    BOOST_TEST(result == server_error::invalid_channel);
+    BOOST_TEST(message["error"].template get<int>() == server_error::invalid_channel);
+    BOOST_TEST(message["errorCategory"].template get<std::string>() == "server");
+}
+
+BOOST_AUTO_TEST_CASE(not_found)
+{
+    boost::system::error_code result;
+    nlohmann::json message;
+
+    ctl_->send({
+        { "command",    "server-message"    },
+        { "server",     "unknown"           },
+        { "target",     "#music"            },
+        { "message",    "plop!"             }
+    });
+    ctl_->recv([&] (auto rresult, auto rmessage) {
+        result = rresult;
+        message = rmessage;
+    });
+
+    wait_for([&] {
+        return result;
+    });
+
+    BOOST_TEST(result == server_error::not_found);
+    BOOST_TEST(message["error"].template get<int>() == server_error::not_found);
+    BOOST_TEST(message["errorCategory"].template get<std::string>() == "server");
+}
+
+BOOST_AUTO_TEST_SUITE_END()
+
+BOOST_AUTO_TEST_SUITE_END()
+
+} // !irccd
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/src/libirccd/command-server-mode/CMakeLists.txt	Fri Dec 15 15:37:58 2017 +0100
@@ -0,0 +1,23 @@
+#
+# CMakeLists.txt -- CMake build system for irccd
+#
+# Copyright (c) 2013-2017 David Demelier <markand@malikania.fr>
+#
+# Permission to use, copy, modify, and/or distribute this software for any
+# purpose with or without fee is hereby granted, provided that the above
+# copyright notice and this permission notice appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+#
+
+irccd_define_test(
+    NAME command-server-mode
+    SOURCES main.cpp
+    LIBRARIES libirccd libirccdctl
+)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/src/libirccd/command-server-mode/main.cpp	Fri Dec 15 15:37:58 2017 +0100
@@ -0,0 +1,245 @@
+/*
+ * main.cpp -- test server-mode remote command
+ *
+ * Copyright (c) 2013-2017 David Demelier <markand@malikania.fr>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#define BOOST_TEST_MODULE "server-mode"
+#include <boost/test/unit_test.hpp>
+
+#include <irccd/daemon/server_service.hpp>
+
+#include <irccd/test/command_test.hpp>
+#include <irccd/test/journal_server.hpp>
+
+namespace irccd {
+
+namespace {
+
+class server_mode_test : public command_test<server_mode_command> {
+protected:
+    std::shared_ptr<journal_server> server_{new journal_server(service_, "test")};
+
+    server_mode_test()
+    {
+        daemon_->servers().add(server_);
+    }
+};
+
+} // !namespace
+
+BOOST_FIXTURE_TEST_SUITE(server_mode_test_suite, server_mode_test)
+
+BOOST_AUTO_TEST_CASE(basic)
+{
+    ctl_->send({
+        { "command",    "server-mode"   },
+        { "server",     "test"          },
+        { "channel",    "#irccd"        },
+        { "mode",       "+t"            }
+    });
+
+    wait_for([this] () {
+        return !server_->cqueue().empty();
+    });
+
+    auto cmd = server_->cqueue().back();
+
+    BOOST_TEST(cmd["command"].get<std::string>() == "mode");
+    BOOST_TEST(cmd["channel"].get<std::string>() == "#irccd");
+    BOOST_TEST(cmd["mode"].get<std::string>() == "+t");
+}
+
+BOOST_AUTO_TEST_SUITE(errors)
+
+BOOST_AUTO_TEST_CASE(invalid_identifier_1)
+{
+    boost::system::error_code result;
+    nlohmann::json message;
+
+    ctl_->send({
+        { "command",    "server-mode"   },
+        { "server",     123456          },
+        { "channel",    "#music"        },
+        { "mode",       "+i"            }
+    });
+    ctl_->recv([&] (auto rresult, auto rmessage) {
+        result = rresult;
+        message = rmessage;
+    });
+
+    wait_for([&] {
+        return result;
+    });
+
+    BOOST_TEST(result == server_error::invalid_identifier);
+    BOOST_TEST(message["error"].template get<int>() == server_error::invalid_identifier);
+    BOOST_TEST(message["errorCategory"].template get<std::string>() == "server");
+}
+
+BOOST_AUTO_TEST_CASE(invalid_identifier_2)
+{
+    boost::system::error_code result;
+    nlohmann::json message;
+
+    ctl_->send({
+        { "command",    "server-mode"   },
+        { "server",     ""              },
+        { "channel",    "#music"        },
+        { "mode",       "+i"            }
+    });
+    ctl_->recv([&] (auto rresult, auto rmessage) {
+        result = rresult;
+        message = rmessage;
+    });
+
+    wait_for([&] {
+        return result;
+    });
+
+    BOOST_TEST(result == server_error::invalid_identifier);
+    BOOST_TEST(message["error"].template get<int>() == server_error::invalid_identifier);
+    BOOST_TEST(message["errorCategory"].template get<std::string>() == "server");
+}
+
+BOOST_AUTO_TEST_CASE(invalid_channel_1)
+{
+    boost::system::error_code result;
+    nlohmann::json message;
+
+    ctl_->send({
+        { "command",    "server-mode"   },
+        { "server",     "test"          },
+        { "channel",    ""              },
+        { "mode",       "+i"            }
+    });
+    ctl_->recv([&] (auto rresult, auto rmessage) {
+        result = rresult;
+        message = rmessage;
+    });
+
+    wait_for([&] {
+        return result;
+    });
+
+    BOOST_TEST(result == server_error::invalid_channel);
+    BOOST_TEST(message["error"].template get<int>() == server_error::invalid_channel);
+    BOOST_TEST(message["errorCategory"].template get<std::string>() == "server");
+}
+
+BOOST_AUTO_TEST_CASE(invalid_channel_2)
+{
+    boost::system::error_code result;
+    nlohmann::json message;
+
+    ctl_->send({
+        { "command",    "server-mode"   },
+        { "server",     "test"          },
+        { "channel",    123456          },
+        { "mode",       "+i"            }
+    });
+    ctl_->recv([&] (auto rresult, auto rmessage) {
+        result = rresult;
+        message = rmessage;
+    });
+
+    wait_for([&] {
+        return result;
+    });
+
+    BOOST_TEST(result == server_error::invalid_channel);
+    BOOST_TEST(message["error"].template get<int>() == server_error::invalid_channel);
+    BOOST_TEST(message["errorCategory"].template get<std::string>() == "server");
+}
+
+BOOST_AUTO_TEST_CASE(invalid_mode_1)
+{
+    boost::system::error_code result;
+    nlohmann::json message;
+
+    ctl_->send({
+        { "command",    "server-mode"   },
+        { "server",     "test"          },
+        { "channel",    "#music"        },
+        { "mode",       ""              }
+    });
+    ctl_->recv([&] (auto rresult, auto rmessage) {
+        result = rresult;
+        message = rmessage;
+    });
+
+    wait_for([&] {
+        return result;
+    });
+
+    BOOST_TEST(result == server_error::invalid_mode);
+    BOOST_TEST(message["error"].template get<int>() == server_error::invalid_mode);
+    BOOST_TEST(message["errorCategory"].template get<std::string>() == "server");
+}
+
+BOOST_AUTO_TEST_CASE(invalid_mode_2)
+{
+    boost::system::error_code result;
+    nlohmann::json message;
+
+    ctl_->send({
+        { "command",    "server-mode"   },
+        { "server",     "test"          },
+        { "channel",    "#music"        },
+        { "mode",       123456          }
+    });
+    ctl_->recv([&] (auto rresult, auto rmessage) {
+        result = rresult;
+        message = rmessage;
+    });
+
+    wait_for([&] {
+        return result;
+    });
+
+    BOOST_TEST(result == server_error::invalid_mode);
+    BOOST_TEST(message["error"].template get<int>() == server_error::invalid_mode);
+    BOOST_TEST(message["errorCategory"].template get<std::string>() == "server");
+}
+BOOST_AUTO_TEST_CASE(not_found)
+{
+    boost::system::error_code result;
+    nlohmann::json message;
+
+    ctl_->send({
+        { "command",    "server-mode"   },
+        { "server",     "unknown"       },
+        { "channel",    "#music"        },
+        { "mode",       "+i"            }
+    });
+    ctl_->recv([&] (auto rresult, auto rmessage) {
+        result = rresult;
+        message = rmessage;
+    });
+
+    wait_for([&] {
+        return result;
+    });
+
+    BOOST_TEST(result == server_error::not_found);
+    BOOST_TEST(message["error"].template get<int>() == server_error::not_found);
+    BOOST_TEST(message["errorCategory"].template get<std::string>() == "server");
+}
+
+BOOST_AUTO_TEST_SUITE_END()
+
+BOOST_AUTO_TEST_SUITE_END()
+
+} // !irccd
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/src/libirccd/command-server-nick/CMakeLists.txt	Fri Dec 15 15:37:58 2017 +0100
@@ -0,0 +1,23 @@
+#
+# CMakeLists.txt -- CMake build system for irccd
+#
+# Copyright (c) 2013-2017 David Demelier <markand@malikania.fr>
+#
+# Permission to use, copy, modify, and/or distribute this software for any
+# purpose with or without fee is hereby granted, provided that the above
+# copyright notice and this permission notice appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+#
+
+irccd_define_test(
+    NAME command-server-nick
+    SOURCES main.cpp
+    LIBRARIES libirccd libirccdctl
+)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/src/libirccd/command-server-nick/main.cpp	Fri Dec 15 15:37:58 2017 +0100
@@ -0,0 +1,191 @@
+/*
+ * main.cpp -- test server-nick remote command
+ *
+ * Copyright (c) 2013-2017 David Demelier <markand@malikania.fr>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#define BOOST_TEST_MODULE "server-nick"
+#include <boost/test/unit_test.hpp>
+
+#include <irccd/daemon/server_service.hpp>
+
+#include <irccd/test/command_test.hpp>
+#include <irccd/test/journal_server.hpp>
+
+namespace irccd {
+
+namespace {
+
+class server_nick_test : public command_test<server_nick_command> {
+protected:
+    std::shared_ptr<journal_server> server_{new journal_server(service_, "test")};
+
+    server_nick_test()
+    {
+        daemon_->servers().add(server_);
+    }
+};
+
+} // !namespace
+
+BOOST_FIXTURE_TEST_SUITE(server_nick_test_suite, server_nick_test)
+
+BOOST_AUTO_TEST_CASE(basic)
+{
+    nlohmann::json result;
+
+    ctl_->send({
+        { "command",    "server-nick"   },
+        { "server",     "test"          },
+        { "nickname",   "chris"         }
+    });
+    ctl_->recv([&] (auto, auto msg) {
+        result = msg;
+    });
+
+    wait_for([&] () {
+        return result.is_object();
+    });
+
+    BOOST_TEST(result.is_object());
+    BOOST_TEST(server_->nickname() == "chris");
+}
+
+BOOST_AUTO_TEST_SUITE(errors)
+
+BOOST_AUTO_TEST_CASE(invalid_identifier_1)
+{
+    boost::system::error_code result;
+    nlohmann::json message;
+
+    ctl_->send({
+        { "command",    "server-nick"   },
+        { "server",     123456          },
+        { "nickname",   "chris"         }
+    });
+    ctl_->recv([&] (auto rresult, auto rmessage) {
+        result = rresult;
+        message = rmessage;
+    });
+
+    wait_for([&] {
+        return result;
+    });
+
+    BOOST_TEST(result == server_error::invalid_identifier);
+    BOOST_TEST(message["error"].template get<int>() == server_error::invalid_identifier);
+    BOOST_TEST(message["errorCategory"].template get<std::string>() == "server");
+}
+
+BOOST_AUTO_TEST_CASE(invalid_identifier_2)
+{
+    boost::system::error_code result;
+    nlohmann::json message;
+
+    ctl_->send({
+        { "command",    "server-nick"   },
+        { "server",     ""              },
+        { "nickname",   "chris"         }
+    });
+    ctl_->recv([&] (auto rresult, auto rmessage) {
+        result = rresult;
+        message = rmessage;
+    });
+
+    wait_for([&] {
+        return result;
+    });
+
+    BOOST_TEST(result == server_error::invalid_identifier);
+    BOOST_TEST(message["error"].template get<int>() == server_error::invalid_identifier);
+    BOOST_TEST(message["errorCategory"].template get<std::string>() == "server");
+}
+
+BOOST_AUTO_TEST_CASE(invalid_nickname_1)
+{
+    boost::system::error_code result;
+    nlohmann::json message;
+
+    ctl_->send({
+        { "command",    "server-nick"   },
+        { "server",     "test"          },
+        { "nickname",   ""              }
+    });
+    ctl_->recv([&] (auto rresult, auto rmessage) {
+        result = rresult;
+        message = rmessage;
+    });
+
+    wait_for([&] {
+        return result;
+    });
+
+    BOOST_TEST(result == server_error::invalid_nickname);
+    BOOST_TEST(message["error"].template get<int>() == server_error::invalid_nickname);
+    BOOST_TEST(message["errorCategory"].template get<std::string>() == "server");
+}
+
+BOOST_AUTO_TEST_CASE(invalid_nickname_2)
+{
+    boost::system::error_code result;
+    nlohmann::json message;
+
+    ctl_->send({
+        { "command",    "server-nick"   },
+        { "server",     "test"          },
+        { "nickname",   123456          }
+    });
+    ctl_->recv([&] (auto rresult, auto rmessage) {
+        result = rresult;
+        message = rmessage;
+    });
+
+    wait_for([&] {
+        return result;
+    });
+
+    BOOST_TEST(result == server_error::invalid_nickname);
+    BOOST_TEST(message["error"].template get<int>() == server_error::invalid_nickname);
+    BOOST_TEST(message["errorCategory"].template get<std::string>() == "server");
+}
+BOOST_AUTO_TEST_CASE(not_found)
+{
+    boost::system::error_code result;
+    nlohmann::json message;
+
+    ctl_->send({
+        { "command",    "server-nick"   },
+        { "server",     "unknown"       },
+        { "nickname",   "chris"         }
+    });
+    ctl_->recv([&] (auto rresult, auto rmessage) {
+        result = rresult;
+        message = rmessage;
+    });
+
+    wait_for([&] {
+        return result;
+    });
+
+    BOOST_TEST(result == server_error::not_found);
+    BOOST_TEST(message["error"].template get<int>() == server_error::not_found);
+    BOOST_TEST(message["errorCategory"].template get<std::string>() == "server");
+}
+
+BOOST_AUTO_TEST_SUITE_END()
+
+BOOST_AUTO_TEST_SUITE_END()
+
+} // !irccd
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/src/libirccd/command-server-notice/CMakeLists.txt	Fri Dec 15 15:37:58 2017 +0100
@@ -0,0 +1,23 @@
+#
+# CMakeLists.txt -- CMake build system for irccd
+#
+# Copyright (c) 2013-2017 David Demelier <markand@malikania.fr>
+#
+# Permission to use, copy, modify, and/or distribute this software for any
+# purpose with or without fee is hereby granted, provided that the above
+# copyright notice and this permission notice appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+#
+
+irccd_define_test(
+    NAME command-server-notice
+    SOURCES main.cpp
+    LIBRARIES libirccd libirccdctl
+)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/src/libirccd/command-server-notice/main.cpp	Fri Dec 15 15:37:58 2017 +0100
@@ -0,0 +1,196 @@
+/*
+ * main.cpp -- test server-notice remote command
+ *
+ * Copyright (c) 2013-2017 David Demelier <markand@malikania.fr>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#define BOOST_TEST_MODULE "server-notice"
+#include <boost/test/unit_test.hpp>
+
+#include <irccd/daemon/server_service.hpp>
+
+#include <irccd/test/command_test.hpp>
+#include <irccd/test/journal_server.hpp>
+
+namespace irccd {
+
+namespace {
+
+class server_notice_test : public command_test<server_notice_command> {
+protected:
+    std::shared_ptr<journal_server> server_{new journal_server(service_, "test")};
+
+    server_notice_test()
+    {
+        daemon_->servers().add(server_);
+    }
+};
+
+} // !namespace
+
+BOOST_FIXTURE_TEST_SUITE(server_notice_test_suite, server_notice_test)
+
+BOOST_AUTO_TEST_CASE(basic)
+{
+    ctl_->send({
+        { "command",    "server-notice" },
+        { "server",     "test"          },
+        { "target",     "#staff"        },
+        { "message",    "quiet!"        }
+    });
+
+    wait_for([this] () {
+        return !server_->cqueue().empty();
+    });
+
+    auto cmd = server_->cqueue().back();
+
+    BOOST_TEST(cmd["command"].get<std::string>() == "notice");
+    BOOST_TEST(cmd["message"].get<std::string>() == "quiet!");
+    BOOST_TEST(cmd["target"].get<std::string>() == "#staff");
+}
+
+BOOST_AUTO_TEST_SUITE(errors)
+
+BOOST_AUTO_TEST_CASE(invalid_identifier_1)
+{
+    boost::system::error_code result;
+    nlohmann::json message;
+
+    ctl_->send({
+        { "command",    "server-notice" },
+        { "server",     123456          },
+        { "target",     "#music"        },
+        { "message",    "quiet!"        }
+    });
+    ctl_->recv([&] (auto rresult, auto rmessage) {
+        result = rresult;
+        message = rmessage;
+    });
+
+    wait_for([&] {
+        return result;
+    });
+
+    BOOST_TEST(result == server_error::invalid_identifier);
+    BOOST_TEST(message["error"].template get<int>() == server_error::invalid_identifier);
+    BOOST_TEST(message["errorCategory"].template get<std::string>() == "server");
+}
+
+BOOST_AUTO_TEST_CASE(invalid_identifier_2)
+{
+    boost::system::error_code result;
+    nlohmann::json message;
+
+    ctl_->send({
+        { "command",    "server-notice" },
+        { "server",     ""              },
+        { "target",     "#music"        },
+        { "message",    "quiet!"        }
+    });
+    ctl_->recv([&] (auto rresult, auto rmessage) {
+        result = rresult;
+        message = rmessage;
+    });
+
+    wait_for([&] {
+        return result;
+    });
+
+    BOOST_TEST(result == server_error::invalid_identifier);
+    BOOST_TEST(message["error"].template get<int>() == server_error::invalid_identifier);
+    BOOST_TEST(message["errorCategory"].template get<std::string>() == "server");
+}
+
+BOOST_AUTO_TEST_CASE(invalid_channel_1)
+{
+    boost::system::error_code result;
+    nlohmann::json message;
+
+    ctl_->send({
+        { "command",    "server-notice" },
+        { "server",     "test"          },
+        { "target",     ""              },
+        { "message",    "quiet!"        }
+    });
+    ctl_->recv([&] (auto rresult, auto rmessage) {
+        result = rresult;
+        message = rmessage;
+    });
+
+    wait_for([&] {
+        return result;
+    });
+
+    BOOST_TEST(result == server_error::invalid_channel);
+    BOOST_TEST(message["error"].template get<int>() == server_error::invalid_channel);
+    BOOST_TEST(message["errorCategory"].template get<std::string>() == "server");
+}
+
+BOOST_AUTO_TEST_CASE(invalid_channel_2)
+{
+    boost::system::error_code result;
+    nlohmann::json message;
+
+    ctl_->send({
+        { "command",    "server-notice" },
+        { "server",     "test"          },
+        { "target",     123456          },
+        { "message",    "quiet!"        }
+    });
+    ctl_->recv([&] (auto rresult, auto rmessage) {
+        result = rresult;
+        message = rmessage;
+    });
+
+    wait_for([&] {
+        return result;
+    });
+
+    BOOST_TEST(result == server_error::invalid_channel);
+    BOOST_TEST(message["error"].template get<int>() == server_error::invalid_channel);
+    BOOST_TEST(message["errorCategory"].template get<std::string>() == "server");
+}
+
+BOOST_AUTO_TEST_CASE(not_found)
+{
+    boost::system::error_code result;
+    nlohmann::json message;
+
+    ctl_->send({
+        { "command",    "server-notice" },
+        { "server",     "unknown"       },
+        { "target",     "#music"        },
+        { "message",    "quiet!"        }
+    });
+    ctl_->recv([&] (auto rresult, auto rmessage) {
+        result = rresult;
+        message = rmessage;
+    });
+
+    wait_for([&] {
+        return result;
+    });
+
+    BOOST_TEST(result == server_error::not_found);
+    BOOST_TEST(message["error"].template get<int>() == server_error::not_found);
+    BOOST_TEST(message["errorCategory"].template get<std::string>() == "server");
+}
+
+BOOST_AUTO_TEST_SUITE_END()
+
+BOOST_AUTO_TEST_SUITE_END()
+
+} // !irccd
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/src/libirccd/command-server-part/CMakeLists.txt	Fri Dec 15 15:37:58 2017 +0100
@@ -0,0 +1,23 @@
+#
+# CMakeLists.txt -- CMake build system for irccd
+#
+# Copyright (c) 2013-2017 David Demelier <markand@malikania.fr>
+#
+# Permission to use, copy, modify, and/or distribute this software for any
+# purpose with or without fee is hereby granted, provided that the above
+# copyright notice and this permission notice appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+#
+
+irccd_define_test(
+    NAME command-server-part
+    SOURCES main.cpp
+    LIBRARIES libirccd libirccdctl
+)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/src/libirccd/command-server-part/main.cpp	Fri Dec 15 15:37:58 2017 +0100
@@ -0,0 +1,210 @@
+/*
+ * main.cpp -- test server-part remote command
+ *
+ * Copyright (c) 2013-2017 David Demelier <markand@malikania.fr>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright part and this permission part appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#define BOOST_TEST_MODULE "server-part"
+#include <boost/test/unit_test.hpp>
+
+#include <irccd/daemon/server_service.hpp>
+
+#include <irccd/test/command_test.hpp>
+#include <irccd/test/journal_server.hpp>
+
+namespace irccd {
+
+namespace {
+
+class server_part_test : public command_test<server_part_command> {
+protected:
+    std::shared_ptr<journal_server> server_{new journal_server(service_, "test")};
+
+    server_part_test()
+    {
+        daemon_->servers().add(server_);
+    }
+};
+
+} // !namespace
+
+BOOST_FIXTURE_TEST_SUITE(server_part_test_suite, server_part_test)
+
+BOOST_AUTO_TEST_CASE(basic)
+{
+    ctl_->send({
+        { "command",    "server-part"   },
+        { "server",     "test"          },
+        { "channel",    "#staff"        },
+        { "reason",     "too noisy"     }
+    });
+
+    wait_for([this] () {
+        return !server_->cqueue().empty();
+    });
+
+    auto cmd = server_->cqueue().back();
+
+    BOOST_TEST(cmd["command"].get<std::string>() == "part");
+    BOOST_TEST(cmd["channel"].get<std::string>() == "#staff");
+    BOOST_TEST(cmd["reason"].get<std::string>() == "too noisy");
+}
+
+BOOST_AUTO_TEST_CASE(noreason)
+{
+    ctl_->send({
+        { "command",    "server-part"   },
+        { "server",     "test"          },
+        { "channel",    "#staff"        }
+    });
+
+    wait_for([this] () {
+        return !server_->cqueue().empty();
+    });
+
+    auto cmd = server_->cqueue().back();
+
+    BOOST_TEST(cmd["command"].get<std::string>() == "part");
+    BOOST_TEST(cmd["channel"].get<std::string>() == "#staff");
+    BOOST_TEST(cmd["reason"].get<std::string>() == "");
+}
+
+BOOST_AUTO_TEST_SUITE(errors)
+
+BOOST_AUTO_TEST_CASE(invalid_identifier_1)
+{
+    boost::system::error_code result;
+    nlohmann::json message;
+
+    ctl_->send({
+        { "command",    "server-part"   },
+        { "server",     123456          },
+        { "channel",    "#music"        }
+    });
+    ctl_->recv([&] (auto rresult, auto rmessage) {
+        result = rresult;
+        message = rmessage;
+    });
+
+    wait_for([&] {
+        return result;
+    });
+
+    BOOST_TEST(result == server_error::invalid_identifier);
+    BOOST_TEST(message["error"].template get<int>() == server_error::invalid_identifier);
+    BOOST_TEST(message["errorCategory"].template get<std::string>() == "server");
+}
+
+BOOST_AUTO_TEST_CASE(invalid_identifier_2)
+{
+    boost::system::error_code result;
+    nlohmann::json message;
+
+    ctl_->send({
+        { "command",    "server-part"   },
+        { "server",     ""              },
+        { "channel",    "#music"        }
+    });
+    ctl_->recv([&] (auto rresult, auto rmessage) {
+        result = rresult;
+        message = rmessage;
+    });
+
+    wait_for([&] {
+        return result;
+    });
+
+    BOOST_TEST(result == server_error::invalid_identifier);
+    BOOST_TEST(message["error"].template get<int>() == server_error::invalid_identifier);
+    BOOST_TEST(message["errorCategory"].template get<std::string>() == "server");
+}
+
+BOOST_AUTO_TEST_CASE(invalid_channel_1)
+{
+    boost::system::error_code result;
+    nlohmann::json message;
+
+    ctl_->send({
+        { "command",    "server-part"   },
+        { "server",     "test"          },
+        { "channel",    ""              }
+    });
+    ctl_->recv([&] (auto rresult, auto rmessage) {
+        result = rresult;
+        message = rmessage;
+    });
+
+    wait_for([&] {
+        return result;
+    });
+
+    BOOST_TEST(result == server_error::invalid_channel);
+    BOOST_TEST(message["error"].template get<int>() == server_error::invalid_channel);
+    BOOST_TEST(message["errorCategory"].template get<std::string>() == "server");
+}
+
+BOOST_AUTO_TEST_CASE(invalid_channel_2)
+{
+    boost::system::error_code result;
+    nlohmann::json message;
+
+    ctl_->send({
+        { "command",    "server-part"   },
+        { "server",     "test"          },
+        { "channel",    123456          }
+    });
+    ctl_->recv([&] (auto rresult, auto rmessage) {
+        result = rresult;
+        message = rmessage;
+    });
+
+    wait_for([&] {
+        return result;
+    });
+
+    BOOST_TEST(result == server_error::invalid_channel);
+    BOOST_TEST(message["error"].template get<int>() == server_error::invalid_channel);
+    BOOST_TEST(message["errorCategory"].template get<std::string>() == "server");
+}
+
+BOOST_AUTO_TEST_CASE(not_found)
+{
+    boost::system::error_code result;
+    nlohmann::json message;
+
+    ctl_->send({
+        { "command",    "server-part"   },
+        { "server",     "unknown"       },
+        { "channel",    "#music"        }
+    });
+    ctl_->recv([&] (auto rresult, auto rmessage) {
+        result = rresult;
+        message = rmessage;
+    });
+
+    wait_for([&] {
+        return result;
+    });
+
+    BOOST_TEST(result == server_error::not_found);
+    BOOST_TEST(message["error"].template get<int>() == server_error::not_found);
+    BOOST_TEST(message["errorCategory"].template get<std::string>() == "server");
+}
+
+BOOST_AUTO_TEST_SUITE_END()
+
+BOOST_AUTO_TEST_SUITE_END()
+
+} // !irccd
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/src/libirccd/command-server-reconnect/CMakeLists.txt	Fri Dec 15 15:37:58 2017 +0100
@@ -0,0 +1,23 @@
+#
+# CMakeLists.txt -- CMake build system for irccd
+#
+# Copyright (c) 2013-2017 David Demelier <markand@malikania.fr>
+#
+# Permission to use, copy, modify, and/or distribute this software for any
+# purpose with or without fee is hereby granted, provided that the above
+# copyright notice and this permission notice appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+#
+
+irccd_define_test(
+    NAME command-server-reconnect
+    SOURCES main.cpp
+    LIBRARIES libirccd libirccdctl
+)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/src/libirccd/command-server-reconnect/main.cpp	Fri Dec 15 15:37:58 2017 +0100
@@ -0,0 +1,154 @@
+/*
+ * main.cpp -- test server-reconnect remote command
+ *
+ * Copyright (c) 2013-2017 David Demelier <markand@malikania.fr>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#define BOOST_TEST_MODULE "server-reconnect"
+#include <boost/test/unit_test.hpp>
+
+#include <irccd/daemon/server_service.hpp>
+
+#include <irccd/test/command_test.hpp>
+#include <irccd/test/journal_server.hpp>
+
+namespace irccd {
+
+namespace {
+
+class server_reconnect_test : public command_test<server_reconnect_command> {
+protected:
+    std::shared_ptr<journal_server> server1_{new journal_server(service_, "s1")};
+    std::shared_ptr<journal_server> server2_{new journal_server(service_, "s2")};
+
+    server_reconnect_test()
+    {
+        daemon_->servers().add(server1_);
+        daemon_->servers().add(server2_);
+    }
+};
+
+} // !namespace
+
+BOOST_FIXTURE_TEST_SUITE(server_reconnect_test_suite, server_reconnect_test)
+
+BOOST_AUTO_TEST_CASE(basic)
+{
+    ctl_->send({
+        { "command",    "server-reconnect"  },
+        { "server",     "s1"                }
+    });
+
+    wait_for([this] () {
+        return !server1_->cqueue().empty();
+    });
+
+    auto cmd1 = server1_->cqueue().back();
+
+    BOOST_TEST(cmd1["command"].get<std::string>() == "reconnect");
+    BOOST_TEST(server2_->cqueue().empty());
+}
+
+BOOST_AUTO_TEST_CASE(all)
+{
+    ctl_->send({{"command", "server-reconnect"}});
+
+    wait_for([this] () {
+        return !server1_->cqueue().empty() && !server2_->cqueue().empty();
+    });
+
+    auto cmd1 = server1_->cqueue().back();
+    auto cmd2 = server2_->cqueue().back();
+
+    BOOST_TEST(cmd1["command"].get<std::string>() == "reconnect");
+    BOOST_TEST(cmd2["command"].get<std::string>() == "reconnect");
+}
+
+BOOST_AUTO_TEST_SUITE(errors)
+
+BOOST_AUTO_TEST_CASE(invalid_identifier_1)
+{
+    boost::system::error_code result;
+    nlohmann::json message;
+
+    ctl_->send({
+        { "command",    "server-reconnect"  },
+        { "server",     123456              }
+    });
+    ctl_->recv([&] (auto rresult, auto rmessage) {
+        result = rresult;
+        message = rmessage;
+    });
+
+    wait_for([&] {
+        return result;
+    });
+
+    BOOST_TEST(result == server_error::invalid_identifier);
+    BOOST_TEST(message["error"].template get<int>() == server_error::invalid_identifier);
+    BOOST_TEST(message["errorCategory"].template get<std::string>() == "server");
+}
+
+BOOST_AUTO_TEST_CASE(invalid_identifier_2)
+{
+    boost::system::error_code result;
+    nlohmann::json message;
+
+    ctl_->send({
+        { "command",    "server-reconnect"  },
+        { "server",     ""                  }
+    });
+    ctl_->recv([&] (auto rresult, auto rmessage) {
+        result = rresult;
+        message = rmessage;
+    });
+
+    wait_for([&] {
+        return result;
+    });
+
+    BOOST_TEST(result == server_error::invalid_identifier);
+    BOOST_TEST(message["error"].template get<int>() == server_error::invalid_identifier);
+    BOOST_TEST(message["errorCategory"].template get<std::string>() == "server");
+}
+
+BOOST_AUTO_TEST_CASE(not_found)
+{
+    boost::system::error_code result;
+    nlohmann::json message;
+
+    ctl_->send({
+        { "command",    "server-reconnect"  },
+        { "server",     "unknown"           }
+    });
+    ctl_->recv([&] (auto rresult, auto rmessage) {
+        result = rresult;
+        message = rmessage;
+    });
+
+    wait_for([&] {
+        return result;
+    });
+
+    BOOST_TEST(result == server_error::not_found);
+    BOOST_TEST(message["error"].template get<int>() == server_error::not_found);
+    BOOST_TEST(message["errorCategory"].template get<std::string>() == "server");
+}
+
+BOOST_AUTO_TEST_SUITE_END()
+
+BOOST_AUTO_TEST_SUITE_END()
+
+} // !irccd
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/src/libirccd/command-server-topic/CMakeLists.txt	Fri Dec 15 15:37:58 2017 +0100
@@ -0,0 +1,23 @@
+#
+# CMakeLists.txt -- CMake build system for irccd
+#
+# Copyright (c) 2013-2017 David Demelier <markand@malikania.fr>
+#
+# Permission to use, copy, modify, and/or distribute this software for any
+# purpose with or without fee is hereby granted, provided that the above
+# copyright notice and this permission notice appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+#
+
+irccd_define_test(
+    NAME command-server-topic
+    SOURCES main.cpp
+    LIBRARIES libirccd libirccdctl
+)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/src/libirccd/command-server-topic/main.cpp	Fri Dec 15 15:37:58 2017 +0100
@@ -0,0 +1,196 @@
+/*
+ * main.cpp -- test server-topic remote command
+ *
+ * Copyright (c) 2013-2017 David Demelier <markand@malikania.fr>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#define BOOST_TEST_MODULE "server-topic"
+#include <boost/test/unit_test.hpp>
+
+#include <irccd/daemon/server_service.hpp>
+
+#include <irccd/test/command_test.hpp>
+#include <irccd/test/journal_server.hpp>
+
+namespace irccd {
+
+namespace {
+
+class server_topic_test : public command_test<server_topic_command> {
+protected:
+    std::shared_ptr<journal_server> server_{new journal_server(service_, "test")};
+
+    server_topic_test()
+    {
+        daemon_->servers().add(server_);
+    }
+};
+
+} // !namespace
+
+BOOST_FIXTURE_TEST_SUITE(server_topic_test_suite, server_topic_test)
+
+BOOST_AUTO_TEST_CASE(basic)
+{
+    ctl_->send({
+        { "command",    "server-topic"  },
+        { "server",     "test"          },
+        { "channel",    "#staff"        },
+        { "topic",      "new version"   }
+    });
+
+    wait_for([this] () {
+        return !server_->cqueue().empty();
+    });
+
+    auto cmd = server_->cqueue().back();
+
+    BOOST_TEST(cmd["command"].get<std::string>() == "topic");
+    BOOST_TEST(cmd["channel"].get<std::string>() == "#staff");
+    BOOST_TEST(cmd["topic"].get<std::string>() == "new version");
+}
+
+BOOST_AUTO_TEST_SUITE(errors)
+
+BOOST_AUTO_TEST_CASE(invalid_identifier_1)
+{
+    boost::system::error_code result;
+    nlohmann::json message;
+
+    ctl_->send({
+        { "command",    "server-topic"  },
+        { "server",     123456          },
+        { "channel",    "#music"        },
+        { "topic",      "plop"          }
+    });
+    ctl_->recv([&] (auto rresult, auto rmessage) {
+        result = rresult;
+        message = rmessage;
+    });
+
+    wait_for([&] {
+        return result;
+    });
+
+    BOOST_TEST(result == server_error::invalid_identifier);
+    BOOST_TEST(message["error"].template get<int>() == server_error::invalid_identifier);
+    BOOST_TEST(message["errorCategory"].template get<std::string>() == "server");
+}
+
+BOOST_AUTO_TEST_CASE(invalid_identifier_2)
+{
+    boost::system::error_code result;
+    nlohmann::json message;
+
+    ctl_->send({
+        { "command",    "server-topic"  },
+        { "server",     ""              },
+        { "channel",    "#music"        },
+        { "topic",      "plop"          }
+    });
+    ctl_->recv([&] (auto rresult, auto rmessage) {
+        result = rresult;
+        message = rmessage;
+    });
+
+    wait_for([&] {
+        return result;
+    });
+
+    BOOST_TEST(result == server_error::invalid_identifier);
+    BOOST_TEST(message["error"].template get<int>() == server_error::invalid_identifier);
+    BOOST_TEST(message["errorCategory"].template get<std::string>() == "server");
+}
+
+BOOST_AUTO_TEST_CASE(invalid_channel_1)
+{
+    boost::system::error_code result;
+    nlohmann::json message;
+
+    ctl_->send({
+        { "command",    "server-topic"  },
+        { "server",     "test"          },
+        { "channel",    ""              },
+        { "topic",      "plop"          }
+    });
+    ctl_->recv([&] (auto rresult, auto rmessage) {
+        result = rresult;
+        message = rmessage;
+    });
+
+    wait_for([&] {
+        return result;
+    });
+
+    BOOST_TEST(result == server_error::invalid_channel);
+    BOOST_TEST(message["error"].template get<int>() == server_error::invalid_channel);
+    BOOST_TEST(message["errorCategory"].template get<std::string>() == "server");
+}
+
+BOOST_AUTO_TEST_CASE(invalid_channel_2)
+{
+    boost::system::error_code result;
+    nlohmann::json message;
+
+    ctl_->send({
+        { "command",    "server-topic"  },
+        { "server",     "test"          },
+        { "channel",    123456          },
+        { "topic",      "plop"          }
+    });
+    ctl_->recv([&] (auto rresult, auto rmessage) {
+        result = rresult;
+        message = rmessage;
+    });
+
+    wait_for([&] {
+        return result;
+    });
+
+    BOOST_TEST(result == server_error::invalid_channel);
+    BOOST_TEST(message["error"].template get<int>() == server_error::invalid_channel);
+    BOOST_TEST(message["errorCategory"].template get<std::string>() == "server");
+}
+
+BOOST_AUTO_TEST_CASE(not_found)
+{
+    boost::system::error_code result;
+    nlohmann::json message;
+
+    ctl_->send({
+        { "command",    "server-topic"  },
+        { "server",     "unknown"       },
+        { "channel",    "#music"        },
+        { "topic",      "plop"          }
+    });
+    ctl_->recv([&] (auto rresult, auto rmessage) {
+        result = rresult;
+        message = rmessage;
+    });
+
+    wait_for([&] {
+        return result;
+    });
+
+    BOOST_TEST(result == server_error::not_found);
+    BOOST_TEST(message["error"].template get<int>() == server_error::not_found);
+    BOOST_TEST(message["errorCategory"].template get<std::string>() == "server");
+}
+
+BOOST_AUTO_TEST_SUITE_END()
+
+BOOST_AUTO_TEST_SUITE_END()
+
+} // !irccd
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/src/libirccd/dynlib-plugin/CMakeLists.txt	Fri Dec 15 15:37:58 2017 +0100
@@ -0,0 +1,43 @@
+#
+# CMakeLists.txt -- CMake build system for irccd
+#
+# Copyright (c) 2013-2017 David Demelier <markand@malikania.fr>
+#
+# Permission to use, copy, modify, and/or distribute this software for any
+# purpose with or without fee is hereby granted, provided that the above
+# copyright notice and this permission notice appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+#
+
+find_package(Boost REQUIRED QUIET)
+
+add_library(test-plugin MODULE test_plugin.cpp)
+target_link_libraries(test-plugin libirccd Boost::boost)
+set_target_properties(
+    test-plugin
+    PROPERTIES
+        PREFIX ""
+        RUNTIME_OUTPUT_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}
+)
+
+foreach (c ${CMAKE_CONFIGURATION_TYPES})
+    string(TOUPPER ${c} c)
+    set_target_properties(
+        test-plugin
+        PROPERTIES
+            RUNTIME_OUTPUT_DIRECTORY_${c} ${CMAKE_CURRENT_BINARY_DIR}
+    )
+endforeach ()
+
+irccd_define_test(
+    NAME dynlib-plugin
+    SOURCES main.cpp
+    LIBRARIES libirccd libirccd-test
+)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/src/libirccd/dynlib-plugin/main.cpp	Fri Dec 15 15:37:58 2017 +0100
@@ -0,0 +1,200 @@
+/*
+ * main.cpp -- test dynlib_plugin
+ *
+ * Copyright (c) 2013-2017 David Demelier <markand@malikania.fr>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#define BOOST_TEST_MODULE "dynlib_plugin"
+#include <boost/test/unit_test.hpp>
+
+/*
+ * For this test, we open a plugin written in C++ and pass a journal_server
+ * class for each of the plugin function.
+ *
+ * Then we verify that the appropriate function has been called correctly.
+ *
+ * Functions load, unload and reload can not be tested though.
+ */
+
+#include <irccd/daemon/dynlib_plugin.hpp>
+#include <irccd/daemon/irccd.hpp>
+
+#include <irccd/test/journal_server.hpp>
+
+namespace irccd {
+
+class fixture {
+protected:
+    boost::asio::io_service service_;
+    std::shared_ptr<journal_server> server_;
+    std::shared_ptr<plugin> plugin_;
+    irccd irccd_;
+
+    inline fixture()
+        : server_(std::make_shared<journal_server>(service_, "test"))
+        , irccd_(service_)
+    {
+        plugin_ = dynlib_plugin_loader({CMAKE_CURRENT_BINARY_DIR}).find("test-plugin");
+
+        if (!plugin_)
+            throw std::runtime_error("test plugin not found");
+    }
+};
+
+BOOST_FIXTURE_TEST_SUITE(dynlib_plugin_suite, fixture)
+
+BOOST_AUTO_TEST_CASE(on_command)
+{
+    plugin_->on_command(irccd_, {server_, "", "", ""});
+
+    BOOST_TEST(server_->cqueue().size() == 1U);
+    BOOST_TEST(server_->cqueue()[0]["command"].get<std::string>() == "message");
+    BOOST_TEST(server_->cqueue()[0]["message"].get<std::string>() == "on_command");
+    BOOST_TEST(server_->cqueue()[0]["target"].get<std::string>() == "test");
+}
+
+BOOST_AUTO_TEST_CASE(on_connect)
+{
+    plugin_->on_connect(irccd_, {server_});
+
+    BOOST_TEST(server_->cqueue().size() == 1U);
+    BOOST_TEST(server_->cqueue()[0]["command"].get<std::string>() == "message");
+    BOOST_TEST(server_->cqueue()[0]["message"].get<std::string>() == "on_connect");
+    BOOST_TEST(server_->cqueue()[0]["target"].get<std::string>() == "test");
+}
+
+BOOST_AUTO_TEST_CASE(on_invite)
+{
+    plugin_->on_invite(irccd_, {server_, "", "", ""});
+
+    BOOST_TEST(server_->cqueue().size() == 1U);
+    BOOST_TEST(server_->cqueue()[0]["command"].get<std::string>() == "message");
+    BOOST_TEST(server_->cqueue()[0]["message"].get<std::string>() == "on_invite");
+    BOOST_TEST(server_->cqueue()[0]["target"].get<std::string>() == "test");
+}
+
+BOOST_AUTO_TEST_CASE(on_join)
+{
+    plugin_->on_join(irccd_, {server_, "", ""});
+
+    BOOST_TEST(server_->cqueue().size() == 1U);
+    BOOST_TEST(server_->cqueue()[0]["command"].get<std::string>() == "message");
+    BOOST_TEST(server_->cqueue()[0]["message"].get<std::string>() == "on_join");
+    BOOST_TEST(server_->cqueue()[0]["target"].get<std::string>() == "test");
+}
+
+BOOST_AUTO_TEST_CASE(on_kick)
+{
+    plugin_->on_kick(irccd_, {server_, "", "", "", ""});
+
+    BOOST_TEST(server_->cqueue().size() == 1U);
+    BOOST_TEST(server_->cqueue()[0]["command"].get<std::string>() == "message");
+    BOOST_TEST(server_->cqueue()[0]["message"].get<std::string>() == "on_kick");
+    BOOST_TEST(server_->cqueue()[0]["target"].get<std::string>() == "test");
+}
+
+BOOST_AUTO_TEST_CASE(on_message)
+{
+    plugin_->on_message(irccd_, {server_, "", "", ""});
+
+    BOOST_TEST(server_->cqueue().size() == 1U);
+    BOOST_TEST(server_->cqueue()[0]["command"].get<std::string>() == "message");
+    BOOST_TEST(server_->cqueue()[0]["message"].get<std::string>() == "on_message");
+    BOOST_TEST(server_->cqueue()[0]["target"].get<std::string>() == "test");
+}
+
+BOOST_AUTO_TEST_CASE(on_me)
+{
+    plugin_->on_me(irccd_, {server_, "", "", ""});
+
+    BOOST_TEST(server_->cqueue().size() == 1U);
+    BOOST_TEST(server_->cqueue()[0]["command"].get<std::string>() == "message");
+    BOOST_TEST(server_->cqueue()[0]["message"].get<std::string>() == "on_me");
+    BOOST_TEST(server_->cqueue()[0]["target"].get<std::string>() == "test");
+}
+
+BOOST_AUTO_TEST_CASE(on_mode)
+{
+    plugin_->on_mode(irccd_, {server_, "", "", "", "", "", ""});
+
+    BOOST_TEST(server_->cqueue().size() == 1U);
+    BOOST_TEST(server_->cqueue()[0]["command"].get<std::string>() == "message");
+    BOOST_TEST(server_->cqueue()[0]["message"].get<std::string>() == "on_mode");
+    BOOST_TEST(server_->cqueue()[0]["target"].get<std::string>() == "test");
+}
+
+BOOST_AUTO_TEST_CASE(on_names)
+{
+    plugin_->on_names(irccd_, {server_, "", {}});
+
+    BOOST_TEST(server_->cqueue().size() == 1U);
+    BOOST_TEST(server_->cqueue()[0]["command"].get<std::string>() == "message");
+    BOOST_TEST(server_->cqueue()[0]["message"].get<std::string>() == "on_names");
+    BOOST_TEST(server_->cqueue()[0]["target"].get<std::string>() == "test");
+}
+
+BOOST_AUTO_TEST_CASE(on_nick)
+{
+    plugin_->on_nick(irccd_, {server_, "", ""});
+
+    BOOST_TEST(server_->cqueue().size() == 1U);
+    BOOST_TEST(server_->cqueue()[0]["command"].get<std::string>() == "message");
+    BOOST_TEST(server_->cqueue()[0]["message"].get<std::string>() == "on_nick");
+    BOOST_TEST(server_->cqueue()[0]["target"].get<std::string>() == "test");
+}
+
+BOOST_AUTO_TEST_CASE(on_notice)
+{
+    plugin_->on_notice(irccd_, {server_, "", "", ""});
+
+    BOOST_TEST(server_->cqueue().size() == 1U);
+    BOOST_TEST(server_->cqueue()[0]["command"].get<std::string>() == "message");
+    BOOST_TEST(server_->cqueue()[0]["message"].get<std::string>() == "on_notice");
+    BOOST_TEST(server_->cqueue()[0]["target"].get<std::string>() == "test");
+}
+
+BOOST_AUTO_TEST_CASE(on_part)
+{
+    plugin_->on_part(irccd_, {server_, "", "", ""});
+
+    BOOST_TEST(server_->cqueue().size() == 1U);
+    BOOST_TEST(server_->cqueue()[0]["command"].get<std::string>() == "message");
+    BOOST_TEST(server_->cqueue()[0]["message"].get<std::string>() == "on_part");
+    BOOST_TEST(server_->cqueue()[0]["target"].get<std::string>() == "test");
+}
+
+BOOST_AUTO_TEST_CASE(on_topic)
+{
+    plugin_->on_topic(irccd_, {server_, "", "", ""});
+
+    BOOST_TEST(server_->cqueue().size() == 1U);
+    BOOST_TEST(server_->cqueue()[0]["command"].get<std::string>() == "message");
+    BOOST_TEST(server_->cqueue()[0]["message"].get<std::string>() == "on_topic");
+    BOOST_TEST(server_->cqueue()[0]["target"].get<std::string>() == "test");
+}
+
+BOOST_AUTO_TEST_CASE(on_whois)
+{
+    plugin_->on_whois(irccd_, {server_, {"", "", "", "", {}}});
+
+    BOOST_TEST(server_->cqueue().size() == 1U);
+    BOOST_TEST(server_->cqueue()[0]["command"].get<std::string>() == "message");
+    BOOST_TEST(server_->cqueue()[0]["message"].get<std::string>() == "on_whois");
+    BOOST_TEST(server_->cqueue()[0]["target"].get<std::string>() == "test");
+}
+
+BOOST_AUTO_TEST_SUITE_END()
+
+} // !irccd
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/src/libirccd/dynlib-plugin/test_plugin.cpp	Fri Dec 15 15:37:58 2017 +0100
@@ -0,0 +1,110 @@
+/*
+ * test_plugin.cpp -- basic exported plugin test
+ *
+ * Copyright (c) 2013-2017 David Demelier <markand@malikania.fr>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include <boost/dll.hpp>
+
+#include <irccd/daemon/plugin.hpp>
+
+namespace irccd {
+
+class test_plugin : public plugin {
+public:
+    using plugin::plugin;
+
+    void on_command(irccd&, const message_event& event) override
+    {
+        event.server->message("test", "on_command");
+    }
+
+    void on_connect(irccd&, const connect_event& event) override
+    {
+        event.server->message("test", "on_connect");
+    }
+
+    void on_invite(irccd&, const invite_event& event) override
+    {
+        event.server->message("test", "on_invite");
+    }
+
+    void on_join(irccd&, const join_event& event) override
+    {
+        event.server->message("test", "on_join");
+    }
+
+    void on_kick(irccd&, const kick_event& event) override
+    {
+        event.server->message("test", "on_kick");
+    }
+
+    void on_message(irccd&, const message_event& event) override
+    {
+        event.server->message("test", "on_message");
+    }
+
+    void on_me(irccd&, const me_event& event) override
+    {
+        event.server->message("test", "on_me");
+    }
+
+    void on_mode(irccd&, const mode_event& event) override
+    {
+        event.server->message("test", "on_mode");
+    }
+
+    void on_names(irccd&, const names_event& event) override
+    {
+        event.server->message("test", "on_names");
+    }
+
+    void on_nick(irccd&, const nick_event& event) override
+    {
+        event.server->message("test", "on_nick");
+    }
+
+    void on_notice(irccd&, const notice_event& event) override
+    {
+        event.server->message("test", "on_notice");
+    }
+
+    void on_part(irccd&, const part_event& event) override
+    {
+        event.server->message("test", "on_part");
+    }
+
+    void on_topic(irccd&, const topic_event& event) override
+    {
+        event.server->message("test", "on_topic");
+    }
+
+    void on_whois(irccd&, const whois_event& event) override
+    {
+        event.server->message("test", "on_whois");
+    }
+};
+
+} // !irccd
+
+extern "C" {
+
+BOOST_SYMBOL_EXPORT
+std::unique_ptr<irccd::plugin> irccd_testplugin_load(std::string name, std::string path)
+{
+    return std::make_unique<irccd::test_plugin>(name, path);
+}
+
+} // !C
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/src/libirccd/irc/CMakeLists.txt	Fri Dec 15 15:37:58 2017 +0100
@@ -0,0 +1,23 @@
+#
+# CMakeLists.txt -- CMake build system for irccd
+#
+# Copyright (c) 2013-2017 David Demelier <markand@malikania.fr>
+#
+# Permission to use, copy, modify, and/or distribute this software for any
+# purpose with or without fee is hereby granted, provided that the above
+# copyright notice and this permission notice appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+#
+
+irccd_define_test(
+    NAME irc
+    SOURCES main.cpp
+    LIBRARIES libirccd
+)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/src/libirccd/irc/main.cpp	Fri Dec 15 15:37:58 2017 +0100
@@ -0,0 +1,85 @@
+/*
+ * main.cpp -- test irc functions
+ *
+ * Copyright (c) 2013-2017 David Demelier <markand@malikania.fr>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#define BOOST_TEST_MODULE "irc"
+#include <boost/test/unit_test.hpp>
+
+#include <irccd/daemon/irc.hpp>
+
+namespace irccd {
+
+BOOST_AUTO_TEST_SUITE(message_parse)
+
+BOOST_AUTO_TEST_CASE(no_prefix)
+{
+    irc::message m;
+
+    BOOST_TEST(!m);
+
+    m = irc::message::parse("PRIVMSG jean :bonjour à toi");
+    BOOST_TEST(m);
+    BOOST_TEST(m.prefix().empty());
+    BOOST_TEST(m.command() == "PRIVMSG");
+    BOOST_TEST(m.args().size() == 2U);
+    BOOST_TEST(m.args()[0] == "jean");
+    BOOST_TEST(m.args()[1] == "bonjour à toi");
+}
+
+BOOST_AUTO_TEST_CASE(prefix)
+{
+    irc::message m;
+
+    BOOST_TEST(!m);
+
+    m = irc::message::parse(":127.0.0.1 PRIVMSG jean :bonjour à toi");
+    BOOST_TEST(m);
+    BOOST_TEST(m.prefix() == "127.0.0.1");
+    BOOST_TEST(m.command() == "PRIVMSG");
+    BOOST_TEST(m.args().size() == 2U);
+    BOOST_TEST(m.args()[0] == "jean");
+    BOOST_TEST(m.args()[1] == "bonjour à toi");
+}
+
+BOOST_AUTO_TEST_SUITE_END()
+
+BOOST_AUTO_TEST_SUITE(user_parse)
+
+BOOST_AUTO_TEST_CASE(basics)
+{
+    auto user = irc::user::parse("jean!~jean@127.0.0.1");
+
+    BOOST_TEST(user.nick() == "jean");
+    BOOST_TEST(user.host() == "~jean@127.0.0.1");
+
+    auto usersimple = irc::user::parse("jean");
+
+    BOOST_TEST(usersimple.nick() == "jean");
+    BOOST_TEST(usersimple.host().empty());
+}
+
+BOOST_AUTO_TEST_CASE(empty)
+{
+    auto user = irc::user::parse("");
+
+    BOOST_TEST(user.nick().empty());
+    BOOST_TEST(user.host().empty());
+}
+
+BOOST_AUTO_TEST_SUITE_END()
+
+} // !irccd
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/src/libirccd/logger/CMakeLists.txt	Fri Dec 15 15:37:58 2017 +0100
@@ -0,0 +1,23 @@
+#
+# CMakeLists.txt -- CMake build system for irccd
+#
+# Copyright (c) 2013-2017 David Demelier <markand@malikania.fr>
+#
+# Permission to use, copy, modify, and/or distribute this software for any
+# purpose with or without fee is hereby granted, provided that the above
+# copyright notice and this permission notice appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+#
+
+irccd_define_test(
+    NAME logger
+    SOURCES main.cpp
+    LIBRARIES libirccd
+)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/src/libirccd/logger/main.cpp	Fri Dec 15 15:37:58 2017 +0100
@@ -0,0 +1,116 @@
+/*
+ * main.cpp -- test logger functions
+ *
+ * Copyright (c) 2013-2017 David Demelier <markand@malikania.fr>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include <algorithm>
+
+#define BOOST_TEST_MODULE "Logger"
+#include <boost/test/unit_test.hpp>
+
+#include <irccd/daemon/logger.hpp>
+
+namespace irccd {
+
+class my_logger : public logger {
+public:
+    std::string line_debug;
+    std::string line_info;
+    std::string line_warning;
+
+    void write_debug(const std::string& line) override
+    {
+        line_debug = line;
+    }
+
+    void write_info(const std::string& line) override
+    {
+        line_info = line;
+    }
+
+    void write_warning(const std::string& line) override
+    {
+        line_warning = line;
+    }
+};
+
+class my_filter : public logger_filter {
+public:
+    std::string pre_debug(std::string input) const override
+    {
+        return std::reverse(input.begin(), input.end()), input;
+    }
+
+    std::string pre_info(std::string input) const override
+    {
+        return std::reverse(input.begin(), input.end()), input;
+    }
+
+    std::string pre_warning(std::string input) const override
+    {
+        return std::reverse(input.begin(), input.end()), input;
+    }
+};
+
+class logger_test {
+public:
+    my_logger log_;
+
+    logger_test()
+    {
+        log_.set_filter(std::make_unique<my_filter>());
+        log_.set_verbose(true);
+    }
+};
+
+BOOST_FIXTURE_TEST_SUITE(logger_test_suite, logger_test)
+
+#if !defined(NDEBUG)
+
+BOOST_AUTO_TEST_CASE(debug)
+{
+    log_.debug("debug");
+
+    BOOST_REQUIRE_EQUAL("gubed", log_.line_debug);
+}
+
+#endif
+
+BOOST_AUTO_TEST_CASE(info)
+{
+    log_.info("info");
+
+    BOOST_REQUIRE_EQUAL("ofni", log_.line_info);
+}
+
+BOOST_AUTO_TEST_CASE(info_quiet)
+{
+    log_.set_verbose(false);
+    log_.info("info");
+
+    BOOST_REQUIRE(log_.line_info.empty());
+}
+
+BOOST_AUTO_TEST_CASE(warning)
+{
+    log_.warning("warning");
+
+    BOOST_REQUIRE_EQUAL("gninraw", log_.line_warning);
+}
+
+BOOST_AUTO_TEST_SUITE_END()
+
+} // !irccd
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/src/libirccd/rules/CMakeLists.txt	Fri Dec 15 15:37:58 2017 +0100
@@ -0,0 +1,23 @@
+#
+# CMakeLists.txt -- CMake build system for irccd
+#
+# Copyright (c) 2013-2017 David Demelier <markand@malikania.fr>
+#
+# Permission to use, copy, modify, and/or distribute this software for any
+# purpose with or without fee is hereby granted, provided that the above
+# copyright notice and this permission notice appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+#
+
+irccd_define_test(
+    NAME rules
+    SOURCES main.cpp
+    LIBRARIES libirccd
+)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/src/libirccd/rules/main.cpp	Fri Dec 15 15:37:58 2017 +0100
@@ -0,0 +1,246 @@
+/*
+ * main.cpp -- test irccd rules
+ *
+ * Copyright (c) 2013-2017 David Demelier <markand@malikania.fr>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#define BOOST_TEST_MODULE "Rules"
+#include <boost/test/unit_test.hpp>
+
+#include <irccd/daemon/irccd.hpp>
+#include <irccd/daemon/logger.hpp>
+#include <irccd/daemon/rule_service.hpp>
+
+namespace irccd {
+
+/*
+ * Simulate the following rules configuration:
+ *
+ * #
+ * # On all servers, each channel #staff can't use the onCommand event,
+ * # everything else is allowed.
+ * #
+ * [rule]       #1
+ * servers      = ""
+ * channels     = "#staff"
+ * events       = "onCommand"
+ * action       = drop
+ *
+ * #
+ * # However, the same onCommand on #staff is allowed on server "unsafe"
+ * #
+ * [rule]       #2
+ * servers      = "unsafe"
+ * channels     = "#staff"
+ * events       = "onCommand"
+ * action       = accept
+ *
+ * #
+ * # Plugin game is only allowed on server "malikania" and "localhost",
+ * # channel "#games" and events "onMessage, onCommand".
+ * #
+ * # The first rule #3-1 disable the plugin game for every server, it is
+ * # reenabled again with the #3-2.
+ * #
+ * [rule]       #3-1
+ * plugins      = "game"
+ * action       = drop
+ *
+ * [rule]       #3-2
+ * servers      = "malikania localhost"
+ * channels     = "#games"
+ * plugins      = "game"
+ * events       = "onMessage onCommand"
+ * action       = accept
+ */
+class rules_test {
+protected:
+    boost::asio::io_service service_;
+    irccd daemon_{service_};
+    rule_service rules_{daemon_};
+
+    rules_test()
+    {
+        daemon_.set_log(std::make_unique<silent_logger>());
+
+        // #1
+        {
+            rules_.add({
+                rule::set{                }, // Servers
+                rule::set{ "#staff"       }, // Channels
+                rule::set{                }, // Origins
+                rule::set{                }, // Plugins
+                rule::set{ "onCommand"    }, // Events
+                rule::action_type::drop
+            });
+        }
+
+        // #2
+        {
+            rules_.add({
+                rule::set{ "unsafe"       },
+                rule::set{ "#staff"       },
+                rule::set{                },
+                rule::set{                },
+                rule::set{ "onCommand"    },
+                rule::action_type::accept
+            });
+        }
+
+        // #3-1
+        {
+            rules_.add({
+                rule::set{},
+                rule::set{},
+                rule::set{},
+                rule::set{"game"},
+                rule::set{},
+                rule::action_type::drop
+            });
+        }
+
+        // #3-2
+        {
+            rules_.add({
+                rule::set{ "malikania", "localhost"   },
+                rule::set{ "#games"                   },
+                rule::set{                            },
+                rule::set{ "game"                     },
+                rule::set{ "onCommand", "onMessage"   },
+                rule::action_type::accept
+            });
+        }
+    }
+};
+
+BOOST_FIXTURE_TEST_SUITE(rules_test_suite, rules_test)
+
+BOOST_AUTO_TEST_CASE(basic_match1)
+{
+    rule m;
+
+    /*
+     * [rule]
+     */
+    BOOST_REQUIRE(m.match("freenode", "#test", "a", "", ""));
+    BOOST_REQUIRE(m.match("", "", "", "", ""));
+}
+
+BOOST_AUTO_TEST_CASE(basic_match2)
+{
+    rule m(rule::set{"freenode"});
+
+    /*
+     * [rule]
+     * servers    = "freenode"
+     */
+
+    BOOST_REQUIRE(m.match("freenode", "#test", "a", "", ""));
+    BOOST_REQUIRE(!m.match("malikania", "#test", "a", "", ""));
+    BOOST_REQUIRE(m.match("freenode", "", "jean", "", "onMessage"));
+}
+
+BOOST_AUTO_TEST_CASE(basic_match3)
+{
+    rule m(rule::set{"freenode"}, rule::set{"#staff"});
+
+    /*
+     * [rule]
+     * servers    = "freenode"
+     * channels    = "#staff"
+     */
+
+    BOOST_REQUIRE(m.match("freenode", "#staff", "a", "", ""));
+    BOOST_REQUIRE(!m.match("freenode", "#test", "a", "", ""));
+    BOOST_REQUIRE(!m.match("malikania", "#staff", "a", "", ""));
+}
+
+BOOST_AUTO_TEST_CASE(basic_match4)
+{
+    rule m(rule::set{"malikania"}, rule::set{"#staff"}, rule::set{"a"});
+
+    /*
+     * [rule]
+     * servers    = "malikania"
+     * channels    = "#staff"
+     * plugins    = "a"
+     */
+
+    BOOST_REQUIRE(m.match("malikania", "#staff", "a", "",""));
+    BOOST_REQUIRE(!m.match("malikania", "#staff", "b", "", ""));
+    BOOST_REQUIRE(!m.match("freenode", "#staff", "a", "", ""));
+}
+
+BOOST_AUTO_TEST_CASE(complex_match1)
+{
+    rule m(rule::set{"malikania", "freenode"});
+
+    /*
+     * [rule]
+     * servers    = "malikania freenode"
+     */
+
+    BOOST_REQUIRE(m.match("malikania", "", "", "", ""));
+    BOOST_REQUIRE(m.match("freenode", "", "", "", ""));
+    BOOST_REQUIRE(!m.match("no", "", "", "", ""));
+}
+
+BOOST_AUTO_TEST_CASE(basic_solve)
+{
+    /* Allowed */
+    BOOST_REQUIRE(rules_.solve("malikania", "#staff", "", "a", "onMessage"));
+
+    /* Allowed */
+    BOOST_REQUIRE(rules_.solve("freenode", "#staff", "", "b", "onTopic"));
+
+    /* Not allowed */
+    BOOST_REQUIRE(!rules_.solve("malikania", "#staff", "", "", "onCommand"));
+
+    /* Not allowed */
+    BOOST_REQUIRE(!rules_.solve("freenode", "#staff", "", "c", "onCommand"));
+
+    /* Allowed */
+    BOOST_REQUIRE(rules_.solve("unsafe", "#staff", "", "c", "onCommand"));
+}
+
+BOOST_AUTO_TEST_CASE(games_solve)
+{
+    /* Allowed */
+    BOOST_REQUIRE(rules_.solve("malikania", "#games", "", "game", "onMessage"));
+
+    /* Allowed */
+    BOOST_REQUIRE(rules_.solve("localhost", "#games", "", "game", "onMessage"));
+
+    /* Allowed */
+    BOOST_REQUIRE(rules_.solve("malikania", "#games", "", "game", "onCommand"));
+
+    /* Not allowed */
+    BOOST_REQUIRE(!rules_.solve("malikania", "#games", "", "game", "onQuery"));
+
+    /* Not allowed */
+    BOOST_REQUIRE(!rules_.solve("freenode", "#no", "", "game", "onMessage"));
+
+    /* Not allowed */
+    BOOST_REQUIRE(!rules_.solve("malikania", "#test", "", "game", "onMessage"));
+}
+
+BOOST_AUTO_TEST_CASE(fix_645)
+{
+    BOOST_REQUIRE(!rules_.solve("MALIKANIA", "#STAFF", "", "SYSTEM", "onCommand"));
+}
+
+BOOST_AUTO_TEST_SUITE_END()
+
+} // !irccd
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/src/libirccd/util/CMakeLists.txt	Fri Dec 15 15:37:58 2017 +0100
@@ -0,0 +1,23 @@
+#
+# CMakeLists.txt -- CMake build system for irccd
+#
+# Copyright (c) 2013-2017 David Demelier <markand@malikania.fr>
+#
+# Permission to use, copy, modify, and/or distribute this software for any
+# purpose with or without fee is hereby granted, provided that the above
+# copyright notice and this permission notice appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+#
+
+irccd_define_test(
+    NAME util
+    SOURCES main.cpp
+    LIBRARIES libirccd
+)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/src/libirccd/util/main.cpp	Fri Dec 15 15:37:58 2017 +0100
@@ -0,0 +1,530 @@
+/*
+ * main.cpp -- test util functions
+ *
+ * Copyright (c) 2013-2017 David Demelier <markand@malikania.fr>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#define BOOST_TEST_MODULE "util"
+#include <boost/test/unit_test.hpp>
+
+#include <cstdint>
+
+#include <irccd/util.hpp>
+#include <irccd/fs_util.hpp>
+#include <irccd/string_util.hpp>
+#include <irccd/system.hpp>
+
+namespace std {
+
+std::ostream& operator<<(std::ostream& out, const std::vector<std::string>& list)
+{
+    for (const auto& s : list)
+        out << s << " ";
+
+    return out;
+}
+
+} // !std
+
+namespace irccd {
+
+BOOST_AUTO_TEST_SUITE(format)
+
+/*
+ * string_util::format function
+ * --------------------------------------------------------
+ */
+
+BOOST_AUTO_TEST_CASE(nothing)
+{
+    std::string expected = "hello world!";
+    std::string result = string_util::format("hello world!");
+
+    BOOST_REQUIRE_EQUAL(expected, result);
+}
+
+BOOST_AUTO_TEST_CASE(escape)
+{
+    string_util::subst params;
+
+    params.keywords.emplace("target", "hello");
+
+    BOOST_REQUIRE_EQUAL("$@#", string_util::format("$@#"));
+    BOOST_REQUIRE_EQUAL(" $ @ # ", string_util::format(" $ @ # "));
+    BOOST_REQUIRE_EQUAL("#", string_util::format("#"));
+    BOOST_REQUIRE_EQUAL(" # ", string_util::format(" # "));
+    BOOST_REQUIRE_EQUAL("#@", string_util::format("#@"));
+    BOOST_REQUIRE_EQUAL("##", string_util::format("##"));
+    BOOST_REQUIRE_EQUAL("#!", string_util::format("#!"));
+    BOOST_REQUIRE_EQUAL("#{target}", string_util::format("##{target}"));
+    BOOST_REQUIRE_EQUAL("@hello", string_util::format("@#{target}", params));
+    BOOST_REQUIRE_EQUAL("hello#", string_util::format("#{target}#", params));
+    BOOST_REQUIRE_THROW(string_util::format("#{failure"), std::exception);
+}
+
+BOOST_AUTO_TEST_CASE(disable_date)
+{
+    string_util::subst params;
+
+    params.flags &= ~(string_util::subst_flags::date);
+
+    BOOST_REQUIRE_EQUAL("%H:%M", string_util::format("%H:%M", params));
+}
+
+BOOST_AUTO_TEST_CASE(disable_keywords)
+{
+    string_util::subst params;
+
+    params.keywords.emplace("target", "hello");
+    params.flags &= ~(string_util::subst_flags::keywords);
+
+    BOOST_REQUIRE_EQUAL("#{target}", string_util::format("#{target}", params));
+}
+
+BOOST_AUTO_TEST_CASE(disable_env)
+{
+    string_util::subst params;
+
+    params.flags &= ~(string_util::subst_flags::env);
+
+    BOOST_REQUIRE_EQUAL("${HOME}", string_util::format("${HOME}", params));
+}
+
+BOOST_AUTO_TEST_CASE(keyword_simple)
+{
+    string_util::subst params;
+
+    params.keywords.insert({"target", "irccd"});
+
+    std::string expected = "hello irccd!";
+    std::string result = string_util::format("hello #{target}!", params);
+
+    BOOST_REQUIRE_EQUAL(expected, result);
+}
+
+BOOST_AUTO_TEST_CASE(keyword_multiple)
+{
+    string_util::subst params;
+
+    params.keywords.insert({"target", "irccd"});
+    params.keywords.insert({"source", "nightmare"});
+
+    std::string expected = "hello irccd from nightmare!";
+    std::string result = string_util::format("hello #{target} from #{source}!", params);
+
+    BOOST_REQUIRE_EQUAL(expected, result);
+}
+
+BOOST_AUTO_TEST_CASE(keyword_adj_twice)
+{
+    string_util::subst params;
+
+    params.keywords.insert({"target", "irccd"});
+
+    std::string expected = "hello irccdirccd!";
+    std::string result = string_util::format("hello #{target}#{target}!", params);
+
+    BOOST_REQUIRE_EQUAL(expected, result);
+}
+
+BOOST_AUTO_TEST_CASE(keyword_missing)
+{
+    std::string expected = "hello !";
+    std::string result = string_util::format("hello #{target}!");
+
+    BOOST_REQUIRE_EQUAL(expected, result);
+}
+
+BOOST_AUTO_TEST_CASE(env_simple)
+{
+    std::string home = sys::env("HOME");
+
+    if (!home.empty()) {
+        std::string expected = "my home is " + home;
+        std::string result = string_util::format("my home is ${HOME}");
+
+        BOOST_REQUIRE_EQUAL(expected, result);
+    }
+}
+
+BOOST_AUTO_TEST_CASE(env_missing)
+{
+    std::string expected = "value is ";
+    std::string result = string_util::format("value is ${HOPE_THIS_VAR_NOT_EXIST}");
+
+    BOOST_REQUIRE_EQUAL(expected, result);
+}
+
+BOOST_AUTO_TEST_SUITE_END()
+
+/*
+ * string_util::split function
+ * --------------------------------------------------------
+ */
+
+BOOST_AUTO_TEST_SUITE(split)
+
+using list = std::vector<std::string>;
+
+BOOST_AUTO_TEST_CASE(simple)
+{
+    list expected { "a", "b" };
+    list result = string_util::split("a;b", ";");
+
+    BOOST_REQUIRE_EQUAL(expected, result);
+}
+
+BOOST_AUTO_TEST_CASE(cut)
+{
+    list expected { "msg", "#staff", "foo bar baz" };
+    list result = string_util::split("msg;#staff;foo bar baz", ";", 3);
+
+    BOOST_REQUIRE_EQUAL(expected, result);
+}
+
+BOOST_AUTO_TEST_SUITE_END()
+
+/*
+ * string_util::strip function
+ * --------------------------------------------------------
+ */
+
+BOOST_AUTO_TEST_SUITE(strip)
+
+BOOST_AUTO_TEST_CASE(left)
+{
+    std::string value = "   123";
+    std::string result = string_util::strip(value);
+
+    BOOST_REQUIRE_EQUAL("123", result);
+}
+
+BOOST_AUTO_TEST_CASE(right)
+{
+    std::string value = "123   ";
+    std::string result = string_util::strip(value);
+
+    BOOST_REQUIRE_EQUAL("123", result);
+}
+
+BOOST_AUTO_TEST_CASE(both)
+{
+    std::string value = "   123   ";
+    std::string result = string_util::strip(value);
+
+    BOOST_REQUIRE_EQUAL("123", result);
+}
+
+BOOST_AUTO_TEST_CASE(none)
+{
+    std::string value = "without";
+    std::string result = string_util::strip(value);
+
+    BOOST_REQUIRE_EQUAL("without", result);
+}
+
+BOOST_AUTO_TEST_CASE(betweenEmpty)
+{
+    std::string value = "one list";
+    std::string result = string_util::strip(value);
+
+    BOOST_REQUIRE_EQUAL("one list", result);
+}
+
+BOOST_AUTO_TEST_CASE(betweenLeft)
+{
+    std::string value = "  space at left";
+    std::string result = string_util::strip(value);
+
+    BOOST_REQUIRE_EQUAL("space at left", result);
+}
+
+BOOST_AUTO_TEST_CASE(betweenRight)
+{
+    std::string value = "space at right  ";
+    std::string result = string_util::strip(value);
+
+    BOOST_REQUIRE_EQUAL("space at right", result);
+}
+
+BOOST_AUTO_TEST_CASE(betweenBoth)
+{
+    std::string value = "  space at both  ";
+    std::string result = string_util::strip(value);
+
+    BOOST_REQUIRE_EQUAL("space at both", result);
+}
+
+BOOST_AUTO_TEST_CASE(empty)
+{
+    std::string value = "    ";
+    std::string result = string_util::strip(value);
+
+    BOOST_REQUIRE_EQUAL("", result);
+}
+
+BOOST_AUTO_TEST_SUITE_END()
+
+/*
+ * string_util::join function
+ * --------------------------------------------------------
+ */
+
+BOOST_AUTO_TEST_SUITE(join)
+
+BOOST_AUTO_TEST_CASE(empty)
+{
+    std::string expected = "";
+    std::string result = string_util::join<int>({});
+
+    BOOST_REQUIRE_EQUAL(expected, result);
+}
+
+BOOST_AUTO_TEST_CASE(one)
+{
+    std::string expected = "1";
+    std::string result = string_util::join({1});
+
+    BOOST_REQUIRE_EQUAL(expected, result);
+}
+
+BOOST_AUTO_TEST_CASE(two)
+{
+    std::string expected = "1:2";
+    std::string result = string_util::join({1, 2});
+
+    BOOST_REQUIRE_EQUAL(expected, result);
+}
+
+BOOST_AUTO_TEST_CASE(delimiterString)
+{
+    std::string expected = "1;;2;;3";
+    std::string result = string_util::join({1, 2, 3}, ";;");
+
+    BOOST_REQUIRE_EQUAL(expected, result);
+}
+
+BOOST_AUTO_TEST_CASE(delimiterChar)
+{
+    std::string expected = "1@2@3@4";
+    std::string result = string_util::join({1, 2, 3, 4}, '@');
+
+    BOOST_REQUIRE_EQUAL(expected, result);
+}
+
+BOOST_AUTO_TEST_SUITE_END()
+
+/*
+ * string_util::is_identifier function
+ * --------------------------------------------------------
+ */
+
+BOOST_AUTO_TEST_SUITE(is_identifier_valid)
+
+BOOST_AUTO_TEST_CASE(correct)
+{
+    BOOST_REQUIRE(string_util::is_identifier("localhost"));
+    BOOST_REQUIRE(string_util::is_identifier("localhost2"));
+    BOOST_REQUIRE(string_util::is_identifier("localhost2-4_"));
+}
+
+BOOST_AUTO_TEST_CASE(incorrect)
+{
+    BOOST_REQUIRE(!string_util::is_identifier(""));
+    BOOST_REQUIRE(!string_util::is_identifier("localhost with spaces"));
+    BOOST_REQUIRE(!string_util::is_identifier("localhost*"));
+    BOOST_REQUIRE(!string_util::is_identifier("&&"));
+    BOOST_REQUIRE(!string_util::is_identifier("@'"));
+    BOOST_REQUIRE(!string_util::is_identifier("##"));
+    BOOST_REQUIRE(!string_util::is_identifier("===++"));
+}
+
+BOOST_AUTO_TEST_SUITE_END()
+
+/*
+ * string_util::is_boolean function
+ * --------------------------------------------------------
+ */
+
+BOOST_AUTO_TEST_SUITE(is_boolean)
+
+BOOST_AUTO_TEST_CASE(correct)
+{
+    // true
+    BOOST_REQUIRE(string_util::is_boolean("true"));
+    BOOST_REQUIRE(string_util::is_boolean("True"));
+    BOOST_REQUIRE(string_util::is_boolean("TRUE"));
+    BOOST_REQUIRE(string_util::is_boolean("TruE"));
+
+    // yes
+    BOOST_REQUIRE(string_util::is_boolean("yes"));
+    BOOST_REQUIRE(string_util::is_boolean("Yes"));
+    BOOST_REQUIRE(string_util::is_boolean("YES"));
+    BOOST_REQUIRE(string_util::is_boolean("YeS"));
+
+    // on
+    BOOST_REQUIRE(string_util::is_boolean("on"));
+    BOOST_REQUIRE(string_util::is_boolean("On"));
+    BOOST_REQUIRE(string_util::is_boolean("oN"));
+    BOOST_REQUIRE(string_util::is_boolean("ON"));
+
+    // 1
+    BOOST_REQUIRE(string_util::is_boolean("1"));
+}
+
+BOOST_AUTO_TEST_CASE(incorrect)
+{
+    BOOST_REQUIRE(!string_util::is_boolean("false"));
+    BOOST_REQUIRE(!string_util::is_boolean("lol"));
+    BOOST_REQUIRE(!string_util::is_boolean(""));
+    BOOST_REQUIRE(!string_util::is_boolean("0"));
+}
+
+BOOST_AUTO_TEST_SUITE_END()
+
+/*
+ * string_util::is_number function
+ * --------------------------------------------------------
+ */
+
+BOOST_AUTO_TEST_SUITE(is_number)
+
+BOOST_AUTO_TEST_CASE(correct)
+{
+    BOOST_REQUIRE(string_util::is_number("123"));
+    BOOST_REQUIRE(string_util::is_number("-123"));
+    BOOST_REQUIRE(string_util::is_number("123.67"));
+}
+
+BOOST_AUTO_TEST_CASE(incorrect)
+{
+    BOOST_REQUIRE(!string_util::is_number("lol"));
+    BOOST_REQUIRE(!string_util::is_number("this is not a number"));
+}
+
+BOOST_AUTO_TEST_SUITE_END()
+
+/*
+ * string_util::to_int function
+ * ------------------------------------------------------------------
+ */
+
+BOOST_AUTO_TEST_SUITE(to_int)
+
+BOOST_AUTO_TEST_CASE(signed_to_int)
+{
+    BOOST_TEST(string_util::to_int("10")                     == 10);
+    BOOST_TEST(string_util::to_int<std::int8_t>("-10")       == -10);
+    BOOST_TEST(string_util::to_int<std::int8_t>("10")        == 10);
+    BOOST_TEST(string_util::to_int<std::int16_t>("-1000")    == -1000);
+    BOOST_TEST(string_util::to_int<std::int16_t>("1000")     == 1000);
+    BOOST_TEST(string_util::to_int<std::int32_t>("-1000")    == -1000);
+    BOOST_TEST(string_util::to_int<std::int32_t>("1000")     == 1000);
+}
+
+BOOST_AUTO_TEST_CASE(signed_to_int64)
+{
+    BOOST_TEST(string_util::to_int<std::int64_t>("-9223372036854775807") == -9223372036854775807LL);
+    BOOST_TEST(string_util::to_int<std::int64_t>("9223372036854775807") == 9223372036854775807LL);
+}
+
+BOOST_AUTO_TEST_CASE(unsigned_to_uint)
+{
+    BOOST_TEST(string_util::to_uint("10")                    == 10U);
+    BOOST_TEST(string_util::to_uint<std::uint8_t>("10")       == 10U);
+    BOOST_TEST(string_util::to_uint<std::uint16_t>("1000")    == 1000U);
+    BOOST_TEST(string_util::to_uint<std::uint32_t>("1000")    == 1000U);
+}
+
+BOOST_AUTO_TEST_CASE(unsigned_to_uint64)
+{
+    BOOST_TEST(string_util::to_uint<std::uint64_t>("18446744073709551615") == 18446744073709551615ULL);
+}
+
+BOOST_AUTO_TEST_SUITE_END()
+
+BOOST_AUTO_TEST_SUITE(errors)
+
+BOOST_AUTO_TEST_CASE(invalid_argument)
+{
+    BOOST_REQUIRE_THROW(string_util::to_int("plopation"), std::invalid_argument);
+    BOOST_REQUIRE_THROW(string_util::to_uint("plopation"), std::invalid_argument);
+}
+
+BOOST_AUTO_TEST_CASE(out_of_range)
+{
+    BOOST_REQUIRE_THROW(string_util::to_int<std::int8_t>("1000"), std::out_of_range);
+    BOOST_REQUIRE_THROW(string_util::to_int<std::int8_t>("-1000"), std::out_of_range);
+    BOOST_REQUIRE_THROW(string_util::to_uint<std::uint8_t>("1000"), std::out_of_range);
+    BOOST_REQUIRE_THROW(string_util::to_uint<std::uint8_t>("-1000"), std::out_of_range);
+}
+
+BOOST_AUTO_TEST_SUITE_END()
+
+/*
+ * fs_util::find function (name)
+ * ------------------------------------------------------------------
+ */
+
+BOOST_AUTO_TEST_SUITE(fs_find_name)
+
+BOOST_AUTO_TEST_CASE(not_recursive)
+{
+    auto file1 = fs_util::find(CMAKE_SOURCE_DIR "/tests/root", "file-1.txt", false);
+    auto file2 = fs_util::find(CMAKE_SOURCE_DIR "/tests/root", "file-2.txt", false);
+
+    BOOST_TEST(file1.find("file-1.txt") != std::string::npos);
+    BOOST_TEST(file2.empty());
+}
+
+BOOST_AUTO_TEST_CASE(recursive)
+{
+    auto file1 = fs_util::find(CMAKE_SOURCE_DIR "/tests/root", "file-1.txt", true);
+    auto file2 = fs_util::find(CMAKE_SOURCE_DIR "/tests/root", "file-2.txt", true);
+
+    BOOST_TEST(file1.find("file-1.txt") != std::string::npos);
+    BOOST_TEST(file2.find("file-2.txt") != std::string::npos);
+}
+
+BOOST_AUTO_TEST_SUITE_END()
+
+/*
+ * fs_util::find function (regex)
+ * ------------------------------------------------------------------
+ */
+
+BOOST_AUTO_TEST_SUITE(fs_find_regex)
+
+BOOST_AUTO_TEST_CASE(not_recursive)
+{
+    const std::regex regex("file-[12]\\.txt");
+
+    auto file = fs_util::find(CMAKE_SOURCE_DIR "/tests/root", regex, false);
+
+    BOOST_TEST(file.find("file-1.txt") != std::string::npos);
+}
+
+BOOST_AUTO_TEST_CASE(recursive)
+{
+    const std::regex regex("file-[12]\\.txt");
+
+    auto file = fs_util::find(CMAKE_SOURCE_DIR "/tests/root/level-1", regex, true);
+
+    BOOST_TEST(file.find("file-2.txt") != std::string::npos);
+}
+
+BOOST_AUTO_TEST_SUITE_END()
+
+} // !irccd
--- a/tests/src/logger-jsapi/CMakeLists.txt	Thu Dec 14 21:51:22 2017 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,23 +0,0 @@
-#
-# CMakeLists.txt -- CMake build system for irccd
-#
-# Copyright (c) 2013-2017 David Demelier <markand@malikania.fr>
-#
-# Permission to use, copy, modify, and/or distribute this software for any
-# purpose with or without fee is hereby granted, provided that the above
-# copyright notice and this permission notice appear in all copies.
-#
-# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
-# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
-# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
-# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
-# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
-# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
-# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
-#
-
-irccd_define_test(
-    NAME logger-jsapi
-    SOURCES main.cpp
-    LIBRARIES libirccd-js
-)
--- a/tests/src/logger-jsapi/main.cpp	Thu Dec 14 21:51:22 2017 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,102 +0,0 @@
-/*
- * main.cpp -- test Irccd.Logger API
- *
- * Copyright (c) 2013-2017 David Demelier <markand@malikania.fr>
- *
- * Permission to use, copy, modify, and/or distribute this software for any
- * purpose with or without fee is hereby granted, provided that the above
- * copyright notice and this permission notice appear in all copies.
- *
- * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
- * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
- * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
- * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
- * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
- * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
- * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
- */
-
-#define BOOST_TEST_MODULE "Logger Javascript API"
-#include <boost/test/unit_test.hpp>
-
-#include <irccd/daemon/logger.hpp>
-
-#include <irccd/js/logger_jsapi.hpp>
-#include <irccd/js/plugin_jsapi.hpp>
-
-#include <irccd/test/js_test.hpp>
-
-namespace irccd {
-
-class logger_test : public js_test<logger_jsapi, plugin_jsapi> {
-protected:
-    std::string line_info;
-    std::string line_warning;
-    std::string line_debug;
-
-    class my_logger : public logger {
-    private:
-        logger_test& test_;
-
-    public:
-        inline my_logger(logger_test& test) noexcept
-            : test_(test)
-        {
-        }
-
-        void write_info(const std::string& line) override
-        {
-            test_.line_info = line;
-        }
-
-        void write_warning(const std::string& line) override
-        {
-            test_.line_warning = line;
-        }
-
-        void write_debug(const std::string& line) override
-        {
-            test_.line_debug = line;
-        }
-    };
-
-    logger_test()
-    {
-        irccd_.set_log(std::make_unique<my_logger>(*this));
-        irccd_.log().set_verbose(true);
-    }
-};
-
-BOOST_FIXTURE_TEST_SUITE(logger_jsapi_suite, logger_test)
-
-BOOST_AUTO_TEST_CASE(info)
-{
-    if (duk_peval_string(plugin_->context(), "Irccd.Logger.info(\"hello!\");") != 0)
-        throw dukx_stack(plugin_->context(), -1);
-
-    BOOST_TEST("plugin test: hello!" == line_info);
-}
-
-BOOST_AUTO_TEST_CASE(warning)
-{
-    if (duk_peval_string(plugin_->context(), "Irccd.Logger.warning(\"FAIL!\");") != 0)
-        throw dukx_stack(plugin_->context(), -1);
-
-    BOOST_TEST("plugin test: FAIL!" == line_warning);
-}
-
-#if !defined(NDEBUG)
-
-BOOST_AUTO_TEST_CASE(debug)
-{
-    if (duk_peval_string(plugin_->context(), "Irccd.Logger.debug(\"starting\");") != 0)
-        throw dukx_stack(plugin_->context(), -1);
-
-    BOOST_TEST("plugin test: starting" == line_debug);
-}
-
-#endif
-
-BOOST_AUTO_TEST_SUITE_END()
-
-} // !irccd
--- a/tests/src/logger/CMakeLists.txt	Thu Dec 14 21:51:22 2017 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,23 +0,0 @@
-#
-# CMakeLists.txt -- CMake build system for irccd
-#
-# Copyright (c) 2013-2017 David Demelier <markand@malikania.fr>
-#
-# Permission to use, copy, modify, and/or distribute this software for any
-# purpose with or without fee is hereby granted, provided that the above
-# copyright notice and this permission notice appear in all copies.
-#
-# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
-# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
-# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
-# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
-# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
-# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
-# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
-#
-
-irccd_define_test(
-    NAME logger
-    SOURCES main.cpp
-    LIBRARIES libirccd
-)
--- a/tests/src/logger/main.cpp	Thu Dec 14 21:51:22 2017 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,116 +0,0 @@
-/*
- * main.cpp -- test logger functions
- *
- * Copyright (c) 2013-2017 David Demelier <markand@malikania.fr>
- *
- * Permission to use, copy, modify, and/or distribute this software for any
- * purpose with or without fee is hereby granted, provided that the above
- * copyright notice and this permission notice appear in all copies.
- *
- * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
- * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
- * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
- * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
- * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
- * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
- * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
- */
-
-#include <algorithm>
-
-#define BOOST_TEST_MODULE "Logger"
-#include <boost/test/unit_test.hpp>
-
-#include <irccd/daemon/logger.hpp>
-
-namespace irccd {
-
-class my_logger : public logger {
-public:
-    std::string line_debug;
-    std::string line_info;
-    std::string line_warning;
-
-    void write_debug(const std::string& line) override
-    {
-        line_debug = line;
-    }
-
-    void write_info(const std::string& line) override
-    {
-        line_info = line;
-    }
-
-    void write_warning(const std::string& line) override
-    {
-        line_warning = line;
-    }
-};
-
-class my_filter : public logger_filter {
-public:
-    std::string pre_debug(std::string input) const override
-    {
-        return std::reverse(input.begin(), input.end()), input;
-    }
-
-    std::string pre_info(std::string input) const override
-    {
-        return std::reverse(input.begin(), input.end()), input;
-    }
-
-    std::string pre_warning(std::string input) const override
-    {
-        return std::reverse(input.begin(), input.end()), input;
-    }
-};
-
-class logger_test {
-public:
-    my_logger log_;
-
-    logger_test()
-    {
-        log_.set_filter(std::make_unique<my_filter>());
-        log_.set_verbose(true);
-    }
-};
-
-BOOST_FIXTURE_TEST_SUITE(logger_test_suite, logger_test)
-
-#if !defined(NDEBUG)
-
-BOOST_AUTO_TEST_CASE(debug)
-{
-    log_.debug("debug");
-
-    BOOST_REQUIRE_EQUAL("gubed", log_.line_debug);
-}
-
-#endif
-
-BOOST_AUTO_TEST_CASE(info)
-{
-    log_.info("info");
-
-    BOOST_REQUIRE_EQUAL("ofni", log_.line_info);
-}
-
-BOOST_AUTO_TEST_CASE(info_quiet)
-{
-    log_.set_verbose(false);
-    log_.info("info");
-
-    BOOST_REQUIRE(log_.line_info.empty());
-}
-
-BOOST_AUTO_TEST_CASE(warning)
-{
-    log_.warning("warning");
-
-    BOOST_REQUIRE_EQUAL("gninraw", log_.line_warning);
-}
-
-BOOST_AUTO_TEST_SUITE_END()
-
-} // !irccd
--- a/tests/src/plugin-ask/CMakeLists.txt	Thu Dec 14 21:51:22 2017 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,28 +0,0 @@
-#
-# CMakeLists.txt -- CMake build system for irccd
-#
-# Copyright (c) 2013-2017 David Demelier <markand@malikania.fr>
-#
-# Permission to use, copy, modify, and/or distribute this software for any
-# purpose with or without fee is hereby granted, provided that the above
-# copyright notice and this permission notice appear in all copies.
-#
-# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
-# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
-# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
-# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
-# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
-# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
-# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
-#
-
-irccd_define_test(
-    NAME plugin-ask
-    SOURCES
-        ${CMAKE_CURRENT_SOURCE_DIR}/main.cpp
-        ${CMAKE_CURRENT_SOURCE_DIR}/answers.conf
-    LIBRARIES libirccd
-    FLAGS
-        PLUGIN_NAME="ask"
-        PLUGIN_PATH="${CMAKE_SOURCE_DIR}/plugins/ask/ask.js"
-)
--- a/tests/src/plugin-ask/answers.conf	Thu Dec 14 21:51:22 2017 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,2 +0,0 @@
-NO
-YES
\ No newline at end of file
--- a/tests/src/plugin-ask/main.cpp	Thu Dec 14 21:51:22 2017 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,76 +0,0 @@
-/*
- * main.cpp -- test ask plugin
- *
- * Copyright (c) 2013-2017 David Demelier <markand@malikania.fr>
- *
- * Permission to use, copy, modify, and/or distribute this software for any
- * purpose with or without fee is hereby granted, provided that the above
- * copyright notice and this permission notice appear in all copies.
- *
- * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
- * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
- * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
- * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
- * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
- * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
- * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
- */
-
-#define BOOST_TEST_MODULE "Ask plugin"
-#include <boost/test/unit_test.hpp>
-
-#include <irccd/daemon/irccd.hpp>
-#include <irccd/daemon/server.hpp>
-
-#include <irccd/test/plugin_test.hpp>
-
-namespace irccd {
-
-class ask_test : public plugin_test {
-public:
-    inline ask_test()
-        : plugin_test(PLUGIN_NAME, PLUGIN_PATH)
-    {
-        plugin_->set_config({
-            { "file", CMAKE_CURRENT_SOURCE_DIR "/answers.conf" }
-        });
-        plugin_->on_load(irccd_);
-    }
-};
-
-BOOST_FIXTURE_TEST_SUITE(ask_test_suite, ask_test)
-
-BOOST_AUTO_TEST_CASE(basic)
-{
-    bool no = false;
-    bool yes = false;
-
-    /*
-     * Invoke the plugin 1000 times, it will be very unlucky to not have both
-     * answers in that amount of tries.
-     */
-    for (int i = 0; i < 1000; ++i) {
-        plugin_->on_command(irccd_, {server_, "tester", "#dummy", ""});
-
-        auto cmd = server_->cqueue().front();
-
-        BOOST_REQUIRE_EQUAL(cmd["command"].get<std::string>(), "message");
-        BOOST_REQUIRE_EQUAL(cmd["target"].get<std::string>(), "#dummy");
-
-        auto msg = cmd["message"].get<std::string>();
-
-        if (msg == "tester, YES")
-            yes = true;
-        if (msg == "tester, NO")
-            no = true;
-
-        server_->cqueue().clear();
-    }
-
-    BOOST_REQUIRE(no);
-    BOOST_REQUIRE(yes);
-}
-
-BOOST_AUTO_TEST_SUITE_END()
-
-} // !irccd
--- a/tests/src/plugin-auth/CMakeLists.txt	Thu Dec 14 21:51:22 2017 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,26 +0,0 @@
-#
-# CMakeLists.txt -- CMake build system for irccd
-#
-# Copyright (c) 2013-2017 David Demelier <markand@malikania.fr>
-#
-# Permission to use, copy, modify, and/or distribute this software for any
-# purpose with or without fee is hereby granted, provided that the above
-# copyright notice and this permission notice appear in all copies.
-#
-# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
-# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
-# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
-# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
-# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
-# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
-# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
-#
-
-irccd_define_test(
-    NAME plugin-auth
-    SOURCES main.cpp
-    LIBRARIES libirccd
-    FLAGS
-        PLUGIN_NAME="auth"
-        PLUGIN_PATH="${CMAKE_SOURCE_DIR}/plugins/auth/auth.js"
-)
--- a/tests/src/plugin-auth/main.cpp	Thu Dec 14 21:51:22 2017 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,93 +0,0 @@
-/*
- * main.cpp -- test auth plugin
- *
- * Copyright (c) 2013-2017 David Demelier <markand@malikania.fr>
- *
- * Permission to use, copy, modify, and/or distribute this software for any
- * purpose with or without fee is hereby granted, provided that the above
- * copyright notice and this permission notice appear in all copies.
- *
- * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
- * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
- * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
- * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
- * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
- * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
- * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
- */
-
-#define BOOST_TEST_MODULE "Auth plugin"
-#include <boost/test/unit_test.hpp>
-
-#include <irccd/daemon/irccd.hpp>
-#include <irccd/daemon/server.hpp>
-
-#include <irccd/test/plugin_test.hpp>
-
-namespace irccd {
-
-class auth_test : public plugin_test {
-protected:
-    std::shared_ptr<journal_server> nickserv1_;
-    std::shared_ptr<journal_server> nickserv2_;
-    std::shared_ptr<journal_server> quakenet_;
-
-public:
-    auth_test()
-        : plugin_test(PLUGIN_NAME, PLUGIN_PATH)
-        , nickserv1_(std::make_shared<journal_server>(service_, "nickserv1"))
-        , nickserv2_(std::make_shared<journal_server>(service_, "nickserv2"))
-        , quakenet_(std::make_shared<journal_server>(service_, "quakenet"))
-    {
-        plugin_->set_config({
-            { "nickserv1.type", "nickserv" },
-            { "nickserv1.password", "plopation" },
-            { "nickserv2.type", "nickserv" },
-            { "nickserv2.password", "something" },
-            { "nickserv2.username", "jean" },
-            { "quakenet.type", "quakenet" },
-            { "quakenet.password", "hello" },
-            { "quakenet.username", "mario" }
-        });
-        plugin_->on_load(irccd_);
-    }
-};
-
-BOOST_FIXTURE_TEST_SUITE(auth_test_suite, auth_test)
-
-BOOST_AUTO_TEST_CASE(nickserv1)
-{
-    plugin_->on_connect(irccd_, {nickserv1_});
-
-    auto cmd = nickserv1_->cqueue().front();
-
-    BOOST_REQUIRE_EQUAL(cmd["command"].get<std::string>(), "message");
-    BOOST_REQUIRE_EQUAL(cmd["target"].get<std::string>(), "NickServ");
-    BOOST_REQUIRE_EQUAL(cmd["message"].get<std::string>(), "identify plopation");
-}
-
-BOOST_AUTO_TEST_CASE(nickserv2)
-{
-    plugin_->on_connect(irccd_, {nickserv2_});
-
-    auto cmd = nickserv2_->cqueue().front();
-
-    BOOST_REQUIRE_EQUAL(cmd["command"].get<std::string>(), "message");
-    BOOST_REQUIRE_EQUAL(cmd["target"].get<std::string>(), "NickServ");
-    BOOST_REQUIRE_EQUAL(cmd["message"].get<std::string>(), "identify jean something");
-}
-
-BOOST_AUTO_TEST_CASE(quakenet)
-{
-    plugin_->on_connect(irccd_, {quakenet_});
-
-    auto cmd = quakenet_->cqueue().front();
-
-    BOOST_REQUIRE_EQUAL(cmd["command"].get<std::string>(), "message");
-    BOOST_REQUIRE_EQUAL(cmd["target"].get<std::string>(), "Q@CServe.quakenet.org");
-    BOOST_REQUIRE_EQUAL(cmd["message"].get<std::string>(), "AUTH mario hello");
-}
-
-BOOST_AUTO_TEST_SUITE_END()
-
-} // !irccd
--- a/tests/src/plugin-config-command/CMakeLists.txt	Thu Dec 14 21:51:22 2017 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,23 +0,0 @@
-#
-# CMakeLists.txt -- CMake build system for irccd
-#
-# Copyright (c) 2013-2017 David Demelier <markand@malikania.fr>
-#
-# Permission to use, copy, modify, and/or distribute this software for any
-# purpose with or without fee is hereby granted, provided that the above
-# copyright notice and this permission notice appear in all copies.
-#
-# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
-# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
-# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
-# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
-# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
-# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
-# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
-#
-
-irccd_define_test(
-    NAME plugin-config-command
-    SOURCES main.cpp
-    LIBRARIES libirccd libirccdctl
-)
--- a/tests/src/plugin-config-command/main.cpp	Thu Dec 14 21:51:22 2017 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,161 +0,0 @@
-/*
- * main.cpp -- test plugin-config remote command
- *
- * Copyright (c) 2013-2017 David Demelier <markand@malikania.fr>
- *
- * Permission to use, copy, modify, and/or distribute this software for any
- * purpose with or without fee is hereby granted, provided that the above
- * copyright notice and this permission notice appear in all copies.
- *
- * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
- * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
- * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
- * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
- * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
- * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
- * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
- */
-
-#define BOOST_TEST_MODULE "plugin-config"
-#include <boost/test/unit_test.hpp>
-
-#include <irccd/daemon/command.hpp>
-#include <irccd/daemon/plugin_service.hpp>
-
-#include <irccd/test/command_test.hpp>
-
-namespace irccd {
-
-namespace {
-
-class custom_plugin : public plugin {
-public:
-    plugin_config config_;
-
-    custom_plugin(std::string name = "test")
-        : plugin(std::move(name), "")
-    {
-    }
-
-    plugin_config config() override
-    {
-        return config_;
-    }
-
-    void set_config(plugin_config config) override
-    {
-        config_ = std::move(config);
-    }
-};
-
-} // !namespace
-
-BOOST_FIXTURE_TEST_SUITE(plugin_config_test_suite, command_test<plugin_config_command>)
-
-BOOST_AUTO_TEST_CASE(set)
-{
-    daemon_->plugins().add(std::make_unique<custom_plugin>("test"));
-    ctl_->send({
-        { "command",    "plugin-config" },
-        { "plugin",     "test"          },
-        { "variable",   "verbosy"       },
-        { "value",      "falsy"         }
-    });
-
-    wait_for([&] {
-        return !daemon_->plugins().require("test")->config().empty();
-    });
-
-    auto config = daemon_->plugins().require("test")->config();
-
-    BOOST_TEST(!config.empty());
-    BOOST_TEST(config["verbosy"] == "falsy");
-}
-
-BOOST_AUTO_TEST_CASE(get)
-{
-    auto plugin = std::make_unique<custom_plugin>("test");
-    auto json = nlohmann::json();
-
-    plugin->set_config({
-        { "x1", "10" },
-        { "x2", "20" }
-    });
-
-    daemon_->plugins().add(std::move(plugin));
-    ctl_->send({
-        { "command", "plugin-config" },
-        { "plugin", "test" },
-        { "variable", "x1" }
-    });
-    ctl_->recv([&] (auto, auto message) {
-        json = std::move(message);
-    });
-
-    wait_for([&] {
-        return json.is_object();
-    });
-
-    BOOST_TEST(json.is_object());
-    BOOST_TEST(json["variables"]["x1"].get<std::string>() == "10");
-    BOOST_TEST(json["variables"]["x2"].is_null());
-}
-
-BOOST_AUTO_TEST_CASE(getall)
-{
-    auto plugin = std::make_unique<custom_plugin>("test");
-    auto json = nlohmann::json();
-
-    plugin->set_config({
-        { "x1", "10" },
-        { "x2", "20" }
-    });
-
-    daemon_->plugins().add(std::move(plugin));
-    ctl_->send({
-        { "command", "plugin-config" },
-        { "plugin", "test" }
-    });
-    ctl_->recv([&] (auto, auto message) {
-        json = std::move(message);
-    });
-
-    wait_for([&] {
-        return json.is_object();
-    });
-
-    BOOST_TEST(json.is_object());
-    BOOST_TEST(json["variables"]["x1"].get<std::string>() == "10");
-    BOOST_TEST(json["variables"]["x2"].get<std::string>() == "20");
-}
-
-BOOST_AUTO_TEST_SUITE(errors)
-
-BOOST_AUTO_TEST_CASE(not_found)
-{
-    boost::system::error_code result;
-    nlohmann::json message;
-
-    ctl_->send({
-        { "command",    "plugin-config" },
-        { "plugin",     "unknown"       }
-    });
-    ctl_->recv([&] (auto rresult, auto rmessage) {
-        result = rresult;
-        message = rmessage;
-    });
-
-    wait_for([&] {
-        return result;
-    });
-
-    BOOST_TEST(result == plugin_error::not_found);
-    BOOST_TEST(message["error"].template get<int>() == plugin_error::not_found);
-    BOOST_TEST(message["errorCategory"].template get<std::string>() == "plugin");
-}
-
-BOOST_AUTO_TEST_SUITE_END()
-
-BOOST_AUTO_TEST_SUITE_END()
-
-} // !irccd
--- a/tests/src/plugin-hangman/CMakeLists.txt	Thu Dec 14 21:51:22 2017 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,29 +0,0 @@
-#
-# CMakeLists.txt -- CMake build system for irccd
-#
-# Copyright (c) 2013-2017 David Demelier <markand@malikania.fr>
-#
-# Permission to use, copy, modify, and/or distribute this software for any
-# purpose with or without fee is hereby granted, provided that the above
-# copyright notice and this permission notice appear in all copies.
-#
-# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
-# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
-# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
-# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
-# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
-# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
-# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
-#
-
-irccd_define_test(
-    NAME plugin-hangman
-    SOURCES
-        main.cpp
-        words.conf
-        wordlist_fix_644.conf
-    LIBRARIES libirccd
-    FLAGS
-        PLUGIN_NAME="hangman"
-        PLUGIN_PATH="${CMAKE_SOURCE_DIR}/plugins/hangman/hangman.js"
-)
--- a/tests/src/plugin-hangman/main.cpp	Thu Dec 14 21:51:22 2017 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,380 +0,0 @@
-/*
- * main.cpp -- test hangman plugin
- *
- * Copyright (c) 2013-2017 David Demelier <markand@malikania.fr>
- *
- * Permission to use, copy, modify, and/or distribute this software for any
- * purpose with or without fee is hereby granted, provided that the above
- * copyright notice and this permission notice appear in all copies.
- *
- * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
- * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
- * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
- * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
- * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
- * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
- * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
- */
-
-#include <unordered_map>
-#include <unordered_set>
-
-#define BOOST_TEST_MODULE "Hangman plugin"
-#include <boost/test/unit_test.hpp>
-
-#include <irccd/daemon/irccd.hpp>
-#include <irccd/daemon/server.hpp>
-
-#include <irccd/test/plugin_test.hpp>
-
-namespace irccd {
-
-class hangman_test : public plugin_test {
-public:
-    hangman_test()
-        : plugin_test(PLUGIN_NAME, PLUGIN_PATH)
-    {
-        plugin_->set_formats({
-            { "asked", "asked=#{plugin}:#{command}:#{server}:#{channel}:#{origin}:#{nickname}:#{letter}" },
-            { "dead", "dead=#{plugin}:#{command}:#{server}:#{channel}:#{origin}:#{nickname}:#{word}" },
-            { "found", "found=#{plugin}:#{command}:#{server}:#{channel}:#{origin}:#{nickname}:#{word}" },
-            { "start", "start=#{plugin}:#{command}:#{server}:#{channel}:#{origin}:#{nickname}:#{word}" },
-            { "running", "running=#{plugin}:#{command}:#{server}:#{channel}:#{origin}:#{nickname}:#{word}" },
-            { "win", "win=#{plugin}:#{command}:#{server}:#{channel}:#{origin}:#{nickname}:#{word}" },
-            { "wrong-letter", "wrong-letter=#{plugin}:#{command}:#{server}:#{channel}:#{origin}:#{nickname}:#{letter}" },
-            { "wrong-player", "wrong-player=#{plugin}:#{command}:#{server}:#{channel}:#{origin}:#{nickname}:#{letter}" },
-            { "wrong-word", "wrong-word=#{plugin}:#{command}:#{server}:#{channel}:#{origin}:#{nickname}:#{word}" }
-        });
-    }
-
-    void load(plugin_config config = {})
-    {
-        // Add file if not there.
-        if (config.count("file") == 0)
-            config.emplace("file", CMAKE_CURRENT_SOURCE_DIR "/words.conf");
-
-        plugin_->set_config(config);
-        plugin_->on_load(irccd_);
-    }
-};
-
-BOOST_FIXTURE_TEST_SUITE(hangman_test_suite, hangman_test)
-
-BOOST_AUTO_TEST_CASE(asked)
-{
-    load({{ "collaborative", "false" }});
-
-    plugin_->on_command(irccd_, {server_, "jean!jean@localhost", "#hangman", ""});
-
-    auto cmd = server_->cqueue().back();
-
-    BOOST_TEST(cmd["command"].get<std::string>() == "message");
-    BOOST_TEST(cmd["target"].get<std::string>() == "#hangman");
-    BOOST_TEST(cmd["message"].get<std::string>() == "start=hangman:!hangman:test:#hangman:jean!jean@localhost:jean:_ _ _");
-
-    plugin_->on_message(irccd_, {server_, "jean!jean@localhost", "#hangman", "s"});
-    cmd = server_->cqueue().back();
-
-    BOOST_TEST(cmd["command"].get<std::string>() == "message");
-    BOOST_TEST(cmd["target"].get<std::string>() == "#hangman");
-    BOOST_TEST(cmd["message"].get<std::string>() == "found=hangman:!hangman:test:#hangman:jean!jean@localhost:jean:s _ _");
-
-    plugin_->on_message(irccd_, {server_, "jean!jean@localhost", "#hangman", "s"});
-    cmd = server_->cqueue().back();
-
-    BOOST_TEST(cmd["command"].get<std::string>() == "message");
-    BOOST_TEST(cmd["target"].get<std::string>() == "#hangman");
-    BOOST_TEST(cmd["message"].get<std::string>() == "asked=hangman:!hangman:test:#hangman:jean!jean@localhost:jean:s");
-}
-
-BOOST_AUTO_TEST_CASE(dead)
-{
-    load({{ "collaborative", "false" }});
-
-    plugin_->on_command(irccd_, {server_, "jean!jean@localhost", "#hangman", ""});
-    plugin_->on_message(irccd_, {server_, "jean!jean@localhost", "#hangman", "a"});
-    plugin_->on_message(irccd_, {server_, "jean!jean@localhost", "#hangman", "b"});
-    plugin_->on_message(irccd_, {server_, "jean!jean@localhost", "#hangman", "c"});
-    plugin_->on_message(irccd_, {server_, "jean!jean@localhost", "#hangman", "d"});
-    plugin_->on_message(irccd_, {server_, "jean!jean@localhost", "#hangman", "e"});
-    plugin_->on_message(irccd_, {server_, "jean!jean@localhost", "#hangman", "f"});
-    plugin_->on_message(irccd_, {server_, "jean!jean@localhost", "#hangman", "g"});
-    plugin_->on_message(irccd_, {server_, "jean!jean@localhost", "#hangman", "h"});
-    plugin_->on_message(irccd_, {server_, "jean!jean@localhost", "#hangman", "i"});
-    plugin_->on_message(irccd_, {server_, "jean!jean@localhost", "#hangman", "j"});
-
-    auto cmd = server_->cqueue().back();
-
-    BOOST_TEST(cmd["command"].get<std::string>() == "message");
-    BOOST_TEST(cmd["target"].get<std::string>() == "#hangman");
-    BOOST_TEST(cmd["message"].get<std::string>() == "dead=hangman:!hangman:test:#hangman:jean!jean@localhost:jean:sky");
-}
-
-BOOST_AUTO_TEST_CASE(found)
-{
-    load({{ "collaborative", "false" }});
-
-    plugin_->on_command(irccd_, {server_, "jean!jean@localhost", "#hangman", ""});
-    plugin_->on_message(irccd_, {server_, "jean!jean@localhost", "#hangman", "s"});
-
-    auto cmd = server_->cqueue().back();
-
-    BOOST_TEST(cmd["command"].get<std::string>() == "message");
-    BOOST_TEST(cmd["target"].get<std::string>() == "#hangman");
-    BOOST_TEST(cmd["message"].get<std::string>() == "found=hangman:!hangman:test:#hangman:jean!jean@localhost:jean:s _ _");
-}
-
-BOOST_AUTO_TEST_CASE(start)
-{
-    load();
-
-    plugin_->on_command(irccd_, {server_, "jean!jean@localhost", "#hangman", ""});
-
-    auto cmd = server_->cqueue().back();
-
-    BOOST_TEST(cmd["command"].get<std::string>() == "message");
-    BOOST_TEST(cmd["target"].get<std::string>() == "#hangman");
-    BOOST_TEST(cmd["message"].get<std::string>() == "start=hangman:!hangman:test:#hangman:jean!jean@localhost:jean:_ _ _");
-}
-
-BOOST_AUTO_TEST_CASE(win1)
-{
-    load({{ "collaborative", "false" }});
-
-    plugin_->on_command(irccd_, {server_, "jean!jean@localhost", "#hangman", ""});
-    plugin_->on_message(irccd_, {server_, "jean!jean@localhost", "#hangman", "s"});
-    plugin_->on_message(irccd_, {server_, "jean!jean@localhost", "#hangman", "k"});
-    plugin_->on_message(irccd_, {server_, "jean!jean@localhost", "#hangman", "y"});
-
-    auto cmd = server_->cqueue().back();
-
-    BOOST_TEST(cmd["command"].get<std::string>() == "message");
-    BOOST_TEST(cmd["target"].get<std::string>() == "#hangman");
-    BOOST_TEST(cmd["message"].get<std::string>() == "win=hangman:!hangman:test:#hangman:jean!jean@localhost:jean:sky");
-}
-
-BOOST_AUTO_TEST_CASE(win2)
-{
-    load({{ "collaborative", "false" }});
-
-    plugin_->on_command(irccd_, {server_, "jean!jean@localhost", "#hangman", ""});
-    plugin_->on_command(irccd_, {server_, "jean!jean@localhost", "#hangman", "sky"});
-
-    auto cmd = server_->cqueue().back();
-
-    BOOST_TEST(cmd["command"].get<std::string>() == "message");
-    BOOST_TEST(cmd["target"].get<std::string>() == "#hangman");
-    BOOST_TEST(cmd["message"].get<std::string>() == "win=hangman:!hangman:test:#hangman:jean!jean@localhost:jean:sky");
-}
-
-BOOST_AUTO_TEST_CASE(wrong_letter)
-{
-    load();
-
-    plugin_->on_command(irccd_, {server_, "jean!jean@localhost", "#hangman", ""});
-    plugin_->on_message(irccd_, {server_, "jean!jean@localhost", "#hangman", "x"});
-
-    auto cmd = server_->cqueue().back();
-
-    BOOST_TEST(cmd["command"].get<std::string>() == "message");
-    BOOST_TEST(cmd["target"].get<std::string>() == "#hangman");
-    BOOST_TEST(cmd["message"].get<std::string>() == "wrong-letter=hangman:!hangman:test:#hangman:jean!jean@localhost:jean:x");
-}
-
-BOOST_AUTO_TEST_CASE(wrongWord)
-{
-    load();
-
-    plugin_->on_command(irccd_, {server_, "jean!jean@localhost", "#hangman", ""});
-    plugin_->on_command(irccd_, {server_, "jean!jean@localhost", "#hangman", "cheese"});
-
-    auto cmd = server_->cqueue().back();
-
-    BOOST_TEST(cmd["command"].get<std::string>() == "message");
-    BOOST_TEST(cmd["target"].get<std::string>() == "#hangman");
-    BOOST_TEST(cmd["message"].get<std::string>() == "wrong-word=hangman:!hangman:test:#hangman:jean!jean@localhost:jean:cheese");
-}
-
-BOOST_AUTO_TEST_CASE(collaborative_disabled)
-{
-    // Disable collaborative mode.
-    load({{ "collaborative", "false" }});
-
-    plugin_->on_command(irccd_, {server_, "jean!jean@localhost", "#hangman", ""});
-    plugin_->on_message(irccd_, {server_, "jean!jean@localhost", "#hangman", "s"});
-
-    auto cmd = server_->cqueue().back();
-
-    BOOST_TEST(cmd["command"].get<std::string>() == "message");
-    BOOST_TEST(cmd["target"].get<std::string>() == "#hangman");
-    BOOST_TEST(cmd["message"].get<std::string>() == "found=hangman:!hangman:test:#hangman:jean!jean@localhost:jean:s _ _");
-
-    plugin_->on_message(irccd_, {server_, "jean!jean@localhost", "#hangman", "k"});
-    cmd = server_->cqueue().back();
-
-    BOOST_TEST(cmd["command"].get<std::string>() == "message");
-    BOOST_TEST(cmd["target"].get<std::string>() == "#hangman");
-    BOOST_TEST(cmd["message"].get<std::string>() == "found=hangman:!hangman:test:#hangman:jean!jean@localhost:jean:s k _");
-}
-
-BOOST_AUTO_TEST_CASE(collaborative_enabled)
-{
-    // Enable collaborative mode.
-    load({{ "collaborative", "true" }});
-
-    plugin_->on_command(irccd_, {server_, "jean!jean@localhost", "#hangman", ""});
-    plugin_->on_message(irccd_, {server_, "jean!jean@localhost", "#hangman", "s"});
-
-    auto cmd = server_->cqueue().back();
-
-    BOOST_TEST(cmd["command"].get<std::string>() == "message");
-    BOOST_TEST(cmd["target"].get<std::string>() == "#hangman");
-    BOOST_TEST(cmd["message"].get<std::string>() == "found=hangman:!hangman:test:#hangman:jean!jean@localhost:jean:s _ _");
-
-    plugin_->on_message(irccd_, {server_, "jean!jean@localhost", "#hangman", "k"});
-    cmd = server_->cqueue().back();
-
-    BOOST_TEST(cmd["command"].get<std::string>() == "message");
-    BOOST_TEST(cmd["target"].get<std::string>() == "#hangman");
-    BOOST_TEST(cmd["message"].get<std::string>() == "wrong-player=hangman:!hangman:test:#hangman:jean!jean@localhost:jean:k");
-
-    plugin_->on_message(irccd_, {server_, "francis!francis@localhost", "#hangman", "k"});
-    cmd = server_->cqueue().back();
-
-    BOOST_TEST(cmd["command"].get<std::string>() == "message");
-    BOOST_TEST(cmd["target"].get<std::string>() == "#hangman");
-    BOOST_TEST(cmd["message"].get<std::string>() == "found=hangman:!hangman:test:#hangman:francis!francis@localhost:francis:s k _");
-}
-
-BOOST_AUTO_TEST_CASE(case_fix_642)
-{
-    load();
-
-    plugin_->on_command(irccd_, {server_, "jean!jean@localhost", "#hangman", ""});
-    plugin_->on_message(irccd_, {server_, "jean!jean@localhost", "#HANGMAN", "s"});
-
-    auto cmd = server_->cqueue().back();
-
-    BOOST_TEST(cmd["command"].get<std::string>() == "message");
-    BOOST_TEST(cmd["target"].get<std::string>() == "#hangman");
-    BOOST_TEST(cmd["message"].get<std::string>() == "found=hangman:!hangman:test:#hangman:jean!jean@localhost:jean:s _ _");
-
-    plugin_->on_message(irccd_, {server_, "jean!jean@localhost", "#HaNGMaN", "k"});
-    cmd = server_->cqueue().back();
-
-    BOOST_TEST(cmd["command"].get<std::string>() == "message");
-    BOOST_TEST(cmd["target"].get<std::string>() == "#hangman");
-    BOOST_TEST(cmd["message"].get<std::string>() == "wrong-player=hangman:!hangman:test:#hangman:jean!jean@localhost:jean:k");
-
-    plugin_->on_message(irccd_, {server_, "francis!francis@localhost", "#hAngmAn", "k"});
-    cmd = server_->cqueue().back();
-
-    BOOST_TEST(cmd["command"].get<std::string>() == "message");
-    BOOST_TEST(cmd["target"].get<std::string>() == "#hangman");
-    BOOST_TEST(cmd["message"].get<std::string>() == "found=hangman:!hangman:test:#hangman:francis!francis@localhost:francis:s k _");
-}
-
-BOOST_AUTO_TEST_CASE(query)
-{
-    load();
-
-    // Query mode is never collaborative.
-    plugin_->on_command(irccd_, {server_, "jean!jean@localhost", "irccd", ""});
-
-    auto cmd = server_->cqueue().back();
-
-    BOOST_TEST(cmd["command"].get<std::string>() == "message");
-    BOOST_TEST(cmd["target"].get<std::string>() == "jean!jean@localhost");
-    BOOST_TEST(cmd["message"].get<std::string>() == "start=hangman:!hangman:test:jean!jean@localhost:jean!jean@localhost:jean:_ _ _");
-
-    plugin_->on_message(irccd_, {server_, "jean!jean@localhost", "irccd", "s"});
-    cmd = server_->cqueue().back();
-
-    BOOST_TEST(cmd["command"].get<std::string>() == "message");
-    BOOST_TEST(cmd["target"].get<std::string>() == "jean!jean@localhost");
-    BOOST_TEST(cmd["message"].get<std::string>() == "found=hangman:!hangman:test:jean!jean@localhost:jean!jean@localhost:jean:s _ _");
-
-    plugin_->on_message(irccd_, {server_, "jean!jean@localhost", "irccd", "k"});
-    cmd = server_->cqueue().back();
-
-    BOOST_TEST(cmd["command"].get<std::string>() == "message");
-    BOOST_TEST(cmd["target"].get<std::string>() == "jean!jean@localhost");
-    BOOST_TEST(cmd["message"].get<std::string>() == "found=hangman:!hangman:test:jean!jean@localhost:jean!jean@localhost:jean:s k _");
-
-    plugin_->on_command(irccd_, {server_, "jean!jean@localhost", "irccd", "sky"});
-    cmd = server_->cqueue().back();
-
-    BOOST_TEST(cmd["command"].get<std::string>() == "message");
-    BOOST_TEST(cmd["target"].get<std::string>() == "jean!jean@localhost");
-    BOOST_TEST(cmd["message"].get<std::string>() == "win=hangman:!hangman:test:jean!jean@localhost:jean!jean@localhost:jean:sky");
-}
-
-BOOST_AUTO_TEST_CASE(running)
-{
-    load();
-
-    plugin_->on_command(irccd_, {server_, "jean!jean@localhost", "#hangman", ""});
-    plugin_->on_message(irccd_, {server_, "jean!jean@localhost", "#hangman", "y"});
-    plugin_->on_command(irccd_, {server_, "jean!jean@localhost", "#hangman", ""});
-
-    auto cmd = server_->cqueue().back();
-
-    BOOST_TEST(cmd["command"].get<std::string>() == "message");
-    BOOST_TEST(cmd["target"].get<std::string>() == "#hangman");
-    BOOST_TEST(cmd["message"].get<std::string>() == "running=hangman:!hangman:test:#hangman:jean!jean@localhost:jean:_ _ y");
-}
-
-BOOST_AUTO_TEST_CASE(wordlist_fix_644)
-{
-    /*
-     * To be sure that the selection use the same list, we create a list of
-     * three words that has different size to determine which one was selected.
-     *
-     * Then we run 3 games and verify that the old selection is not the same
-     * as the current.
-     *
-     * This is not very accurate but it's better than nothing.
-     */
-    load({{ "file", CMAKE_CURRENT_SOURCE_DIR "/wordlist_fix_644.conf" }});
-
-    std::unordered_map<unsigned, std::string> words{
-        { 5, "abc"     },
-        { 7, "abcd"    },
-        { 9, "abcde"   }
-    };
-    std::unordered_set<unsigned> found;
-
-    plugin_->set_formats({
-        { "start", "#{word}" }
-    });
-
-    unsigned last, current;
-
-    // 1. Initial game + finish.
-    plugin_->on_command(irccd_, {server_, "jean!jean@localhost", "#hangman", ""});
-    last = server_->cqueue().back()["message"].get<std::string>().length();
-    found.insert(last);
-    plugin_->on_command(irccd_, {server_, "jean!jean@localhost", "#hangman", words[last]});
-
-    // 2. Current must not be the last one.
-    plugin_->on_command(irccd_, {server_, "jean!jean@localhost", "#hangman", ""});
-    current = server_->cqueue().back()["message"].get<std::string>().length();
-
-    BOOST_TEST(last != current);
-    BOOST_TEST(0U == found.count(current));
-
-    found.insert(current);
-    last = current;
-    plugin_->on_command(irccd_, {server_, "jean!jean@localhost", "#hangman", words[current]});
-
-    // 3. Last word must be the one that is kept into the map.
-    plugin_->on_command(irccd_, {server_, "jean!jean@localhost", "#hangman", ""});
-    current = server_->cqueue().back()["message"].get<std::string>().length();
-
-    BOOST_TEST(last != current);
-    BOOST_TEST(0U == found.count(current));
-}
-
-BOOST_AUTO_TEST_SUITE_END()
-
-} // !irccd
--- a/tests/src/plugin-hangman/wordlist_fix_644.conf	Thu Dec 14 21:51:22 2017 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,3 +0,0 @@
-abc
-abcd
-abcde
--- a/tests/src/plugin-hangman/words.conf	Thu Dec 14 21:51:22 2017 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,1 +0,0 @@
-sky
--- a/tests/src/plugin-history/CMakeLists.txt	Thu Dec 14 21:51:22 2017 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,26 +0,0 @@
-#
-# CMakeLists.txt -- CMake build system for irccd
-#
-# Copyright (c) 2013-2017 David Demelier <markand@malikania.fr>
-#
-# Permission to use, copy, modify, and/or distribute this software for any
-# purpose with or without fee is hereby granted, provided that the above
-# copyright notice and this permission notice appear in all copies.
-#
-# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
-# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
-# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
-# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
-# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
-# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
-# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
-#
-
-irccd_define_test(
-    NAME plugin-history
-    SOURCES main.cpp
-    LIBRARIES libirccd
-    FLAGS
-        PLUGIN_NAME="history"
-        PLUGIN_PATH="${CMAKE_SOURCE_DIR}/plugins/history/history.js"
-)
--- a/tests/src/plugin-history/broken-conf.json	Thu Dec 14 21:51:22 2017 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,1 +0,0 @@
-this is not a json file
--- a/tests/src/plugin-history/main.cpp	Thu Dec 14 21:51:22 2017 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,149 +0,0 @@
-/*
- * main.cpp -- test history plugin
- *
- * Copyright (c) 2013-2017 David Demelier <markand@malikania.fr>
- *
- * Permission to use, copy, modify, and/or distribute this software for any
- * purpose with or without fee is hereby granted, provided that the above
- * copyright notice and this permission notice appear in all copies.
- *
- * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
- * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
- * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
- * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
- * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
- * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
- * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
- */
-
-#include <regex>
-
-#define BOOST_TEST_MODULE "History plugin"
-#include <boost/test/unit_test.hpp>
-
-#include <irccd/daemon/irccd.hpp>
-#include <irccd/daemon/server.hpp>
-
-#include <irccd/test/plugin_test.hpp>
-
-namespace irccd {
-
-class history_test : public plugin_test {
-public:
-    history_test()
-        : plugin_test(PLUGIN_NAME, PLUGIN_PATH)
-    {
-        plugin_->set_formats({
-            { "error", "error=#{plugin}:#{command}:#{server}:#{channel}:#{origin}:#{nickname}" },
-            { "seen", "seen=#{plugin}:#{command}:#{server}:#{channel}:#{origin}:#{nickname}:#{target}:%H:%M" },
-            { "said", "said=#{plugin}:#{command}:#{server}:#{channel}:#{origin}:#{nickname}:#{target}:#{message}:%H:%M" },
-            { "unknown", "unknown=#{plugin}:#{command}:#{server}:#{channel}:#{origin}:#{nickname}:#{target}" },
-        });
-    }
-
-    void load(plugin_config config = {})
-    {
-        // Add file if not there.
-        if (config.count("file") == 0)
-            config.emplace("file", CMAKE_CURRENT_SOURCE_DIR "/words.conf");
-
-        plugin_->set_config(config);
-        plugin_->on_load(irccd_);
-    }
-};
-
-BOOST_FIXTURE_TEST_SUITE(history_test_suite, history_test)
-
-BOOST_AUTO_TEST_CASE(format_error)
-{
-    load({{"file", CMAKE_CURRENT_SOURCE_DIR "/broken-conf.json"}});
-
-    plugin_->on_command(irccd_, {server_, "jean!jean@localhost", "#history", "seen francis"});
-
-    auto cmd = server_->cqueue().front();
-
-    BOOST_REQUIRE_EQUAL(cmd["command"].get<std::string>(), "message");
-    BOOST_REQUIRE_EQUAL(cmd["target"].get<std::string>(), "#history");
-    BOOST_REQUIRE_EQUAL(cmd["message"].get<std::string>(), "error=history:!history:test:#history:jean!jean@localhost:jean");
-}
-
-BOOST_AUTO_TEST_CASE(format_seen)
-{
-    const std::regex rule("seen=history:!history:test:#history:destructor!dst@localhost:destructor:jean:\\d{2}:\\d{2}");
-
-    remove(CMAKE_CURRENT_BINARY_DIR "/seen.json");
-    load({{ "file", CMAKE_CURRENT_BINARY_DIR "/seen.json" }});
-
-    plugin_->on_message(irccd_, {server_, "jean!jean@localhost", "#history", "hello"});
-    plugin_->on_command(irccd_, {server_, "destructor!dst@localhost", "#history", "seen jean"});
-
-    auto cmd = server_->cqueue().front();
-
-    BOOST_REQUIRE_EQUAL(cmd["command"].get<std::string>(), "message");
-    BOOST_REQUIRE_EQUAL(cmd["target"].get<std::string>(), "#history");
-    BOOST_REQUIRE(std::regex_match(cmd["message"].get<std::string>(), rule));
-}
-
-BOOST_AUTO_TEST_CASE(format_said)
-{
-    std::regex rule("said=history:!history:test:#history:destructor!dst@localhost:destructor:jean:hello:\\d{2}:\\d{2}");
-
-    remove(CMAKE_CURRENT_BINARY_DIR "/said.json");
-    load({{ "file", CMAKE_CURRENT_BINARY_DIR "/said.json" }});
-
-    plugin_->on_message(irccd_, {server_, "jean!jean@localhost", "#history", "hello"});
-    plugin_->on_command(irccd_, {server_, "destructor!dst@localhost", "#history", "said jean"});
-
-    auto cmd = server_->cqueue().front();
-
-    BOOST_REQUIRE_EQUAL(cmd["command"].get<std::string>(), "message");
-    BOOST_REQUIRE_EQUAL(cmd["target"].get<std::string>(), "#history");
-    BOOST_REQUIRE(std::regex_match(cmd["message"].get<std::string>(), rule));
-}
-
-BOOST_AUTO_TEST_CASE(format_unknown)
-{
-    remove(CMAKE_CURRENT_BINARY_DIR "/unknown.json");
-    load({{ "file", CMAKE_CURRENT_BINARY_DIR "/unknown.json" }});
-
-    plugin_->on_message(irccd_, {server_, "jean!jean@localhost", "#history", "hello"});
-    plugin_->on_command(irccd_, {server_, "destructor!dst@localhost", "#history", "seen nobody"});
-
-    auto cmd = server_->cqueue().front();
-
-    BOOST_REQUIRE_EQUAL(cmd["command"].get<std::string>(), "message");
-    BOOST_REQUIRE_EQUAL(cmd["target"].get<std::string>(), "#history");
-    BOOST_REQUIRE_EQUAL(cmd["message"].get<std::string>(), "unknown=history:!history:test:#history:destructor!dst@localhost:destructor:nobody");
-}
-
-BOOST_AUTO_TEST_CASE(fix_642)
-{
-    const std::regex rule("said=history:!history:test:#history:destructor!dst@localhost:destructor:jean:hello:\\d{2}:\\d{2}");
-
-    remove(CMAKE_CURRENT_BINARY_DIR "/case.json");
-    load({{"file", CMAKE_CURRENT_BINARY_DIR "/case.json"}});
-
-    plugin_->on_message(irccd_, {server_, "JeaN!JeaN@localhost", "#history", "hello"});
-
-    // Full caps.
-    plugin_->on_command(irccd_, {server_, "destructor!dst@localhost", "#HISTORY", "said JEAN"});
-
-    auto cmd = server_->cqueue().front();
-
-    BOOST_REQUIRE_EQUAL(cmd["command"].get<std::string>(), "message");
-    BOOST_REQUIRE_EQUAL(cmd["target"].get<std::string>(), "#history");
-    BOOST_REQUIRE(std::regex_match(cmd["message"].get<std::string>(), rule));
-
-    // Random caps.
-    plugin_->on_command(irccd_, {server_, "destructor!dst@localhost", "#HiSToRy", "said JeaN"});
-
-    cmd = server_->cqueue().back();
-
-    BOOST_REQUIRE_EQUAL(cmd["command"].get<std::string>(), "message");
-    BOOST_REQUIRE_EQUAL(cmd["target"].get<std::string>(), "#history");
-    BOOST_REQUIRE(std::regex_match(cmd["message"].get<std::string>(), rule));
-}
-
-BOOST_AUTO_TEST_SUITE_END()
-
-} // !irccd
--- a/tests/src/plugin-info-command/CMakeLists.txt	Thu Dec 14 21:51:22 2017 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,23 +0,0 @@
-#
-# CMakeLists.txt -- CMake build system for irccd
-#
-# Copyright (c) 2013-2017 David Demelier <markand@malikania.fr>
-#
-# Permission to use, copy, modify, and/or distribute this software for any
-# purpose with or without fee is hereby granted, provided that the above
-# copyright notice and this permission notice appear in all copies.
-#
-# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
-# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
-# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
-# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
-# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
-# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
-# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
-#
-
-irccd_define_test(
-    NAME plugin-info-command
-    SOURCES main.cpp
-    LIBRARIES libirccd libirccdctl
-)
--- a/tests/src/plugin-info-command/main.cpp	Thu Dec 14 21:51:22 2017 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,90 +0,0 @@
-/*
- * main.cpp -- test plugin-info remote command
- *
- * Copyright (c) 2013-2017 David Demelier <markand@malikania.fr>
- *
- * Permission to use, copy, modify, and/or distribute this software for any
- * purpose with or without fee is hereby granted, provided that the above
- * copyright notice and this permission notice appear in all copies.
- *
- * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
- * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
- * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
- * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
- * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
- * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
- * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
- */
-
-#define BOOST_TEST_MODULE "plugin-info"
-#include <boost/test/unit_test.hpp>
-
-#include <irccd/daemon/command.hpp>
-#include <irccd/daemon/plugin_service.hpp>
-
-#include <irccd/test/command_test.hpp>
-
-namespace irccd {
-
-BOOST_FIXTURE_TEST_SUITE(plugin_info_test_suite, command_test<plugin_info_command>)
-
-BOOST_AUTO_TEST_CASE(basic)
-{
-    auto plg = std::make_unique<plugin>("test", "");
-    auto response = nlohmann::json();
-
-    plg->set_author("Francis Beaugrand");
-    plg->set_license("GPL");
-    plg->set_summary("Completely useless plugin");
-    plg->set_version("0.0.0.0.0.0.0.0.1-beta5");
-
-    daemon_->plugins().add(std::move(plg));
-    ctl_->recv([&] (auto, auto msg) {
-        response = std::move(msg);
-    });
-    ctl_->send({
-        { "command",    "plugin-info"       },
-        { "plugin",     "test"              },
-    });
-
-    wait_for([&] () {
-        return response.is_object();
-    });
-
-    BOOST_TEST(response.is_object());
-    BOOST_TEST(response["author"].get<std::string>() == "Francis Beaugrand");
-    BOOST_TEST(response["license"].get<std::string>() == "GPL");
-    BOOST_TEST(response["summary"].get<std::string>() == "Completely useless plugin");
-    BOOST_TEST(response["version"].get<std::string>() == "0.0.0.0.0.0.0.0.1-beta5");
-}
-
-BOOST_AUTO_TEST_SUITE(errors)
-
-BOOST_AUTO_TEST_CASE(not_found)
-{
-    boost::system::error_code result;
-    nlohmann::json message;
-
-    ctl_->send({
-        { "command",    "plugin-info"   },
-        { "plugin",     "unknown"       }
-    });
-    ctl_->recv([&] (auto rresult, auto rmessage) {
-        result = rresult;
-        message = rmessage;
-    });
-
-    wait_for([&] {
-        return result;
-    });
-
-    BOOST_TEST(result == plugin_error::not_found);
-    BOOST_TEST(message["error"].template get<int>() == plugin_error::not_found);
-    BOOST_TEST(message["errorCategory"].template get<std::string>() == "plugin");
-}
-
-BOOST_AUTO_TEST_SUITE_END()
-
-BOOST_AUTO_TEST_SUITE_END()
-
-} // !irccd
--- a/tests/src/plugin-list-command/CMakeLists.txt	Thu Dec 14 21:51:22 2017 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,23 +0,0 @@
-#
-# CMakeLists.txt -- CMake build system for irccd
-#
-# Copyright (c) 2013-2017 David Demelier <markand@malikania.fr>
-#
-# Permission to use, copy, modify, and/or distribute this software for any
-# purpose with or without fee is hereby granted, provided that the above
-# copyright notice and this permission notice appear in all copies.
-#
-# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
-# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
-# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
-# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
-# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
-# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
-# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
-#
-
-irccd_define_test(
-    NAME plugin-list-command
-    SOURCES main.cpp
-    LIBRARIES libirccd libirccdctl
-)
--- a/tests/src/plugin-list-command/main.cpp	Thu Dec 14 21:51:22 2017 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,60 +0,0 @@
-/*
- * main.cpp -- test plugin-list remote command
- *
- * Copyright (c) 2013-2017 David Demelier <markand@malikania.fr>
- *
- * Permission to use, copy, modify, and/or distribute this software for any
- * purpose with or without fee is hereby granted, provided that the above
- * copyright notice and this permission notice appear in all copies.
- *
- * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
- * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
- * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
- * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
- * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
- * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
- * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
- */
-
-#define BOOST_TEST_MODULE "plugin-list"
-#include <boost/test/unit_test.hpp>
-
-#include <irccd/daemon/command.hpp>
-#include <irccd/daemon/plugin_service.hpp>
-
-#include <irccd/test/command_test.hpp>
-
-namespace irccd {
-
-class plugin_list_test : public command_test<plugin_list_command> {
-public:
-    plugin_list_test()
-    {
-        daemon_->plugins().add(std::make_unique<plugin>("t1", ""));
-        daemon_->plugins().add(std::make_unique<plugin>("t2", ""));
-    }
-};
-
-BOOST_FIXTURE_TEST_SUITE(plugin_list_test_suite, plugin_list_test)
-
-BOOST_AUTO_TEST_CASE(basic)
-{
-    auto response = nlohmann::json();
-
-    ctl_->send({{"command", "plugin-list"}});
-    ctl_->recv([&] (auto, auto message) {
-        response = message;
-    });
-
-    wait_for([&] () {
-        return response.is_object();
-    });
-
-    BOOST_TEST(response.is_object());
-    BOOST_TEST("t1", response["list"][0]);
-    BOOST_TEST("t2", response["list"][1]);
-}
-
-BOOST_AUTO_TEST_SUITE_END()
-
-} // !irccd
--- a/tests/src/plugin-load-command/CMakeLists.txt	Thu Dec 14 21:51:22 2017 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,23 +0,0 @@
-#
-# CMakeLists.txt -- CMake build system for irccd
-#
-# Copyright (c) 2013-2017 David Demelier <markand@malikania.fr>
-#
-# Permission to use, copy, modify, and/or distribute this software for any
-# purpose with or without fee is hereby granted, provided that the above
-# copyright notice and this permission notice appear in all copies.
-#
-# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
-# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
-# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
-# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
-# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
-# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
-# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
-#
-
-irccd_define_test(
-    NAME plugin-load-command
-    SOURCES main.cpp
-    LIBRARIES libirccd libirccdctl
-)
--- a/tests/src/plugin-load-command/main.cpp	Thu Dec 14 21:51:22 2017 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,168 +0,0 @@
-/*
- * main.cpp -- test plugin-load remote command
- *
- * Copyright (c) 2013-2017 David Demelier <markand@malikania.fr>
- *
- * Permission to use, copy, modify, and/or distribute this software for any
- * purpose with or without fee is hereby granted, provided that the above
- * copyright notice and this permission notice appear in all copies.
- *
- * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
- * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
- * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
- * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
- * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
- * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
- * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
- */
-
-#define BOOST_TEST_MODULE "plugin-load"
-#include <boost/test/unit_test.hpp>
-
-#include <irccd/daemon/command.hpp>
-#include <irccd/daemon/plugin_service.hpp>
-
-#include <irccd/test/command_test.hpp>
-
-namespace irccd {
-
-namespace {
-
-class custom_loader : public plugin_loader {
-public:
-    custom_loader()
-        : plugin_loader({}, {".none"})
-    {
-    }
-
-    std::shared_ptr<plugin> open(const std::string&,
-                                 const std::string&) noexcept override
-    {
-        return nullptr;
-    }
-
-    std::shared_ptr<plugin> find(const std::string& id) noexcept override
-    {
-        class broken : public plugin {
-        public:
-            using plugin::plugin;
-
-            void on_load(irccd&) override
-            {
-                throw std::runtime_error("broken");
-            }
-        };
-
-        /*
-         * The 'magic' plugin will be created for the unit tests, all other
-         * plugins will return null.
-         */
-        if (id == "magic")
-            return std::make_unique<plugin>(id, "");
-        if (id == "broken")
-            return std::make_unique<broken>(id, "");
-
-        return nullptr;
-    }
-};
-
-class plugin_load_test : public command_test<plugin_load_command> {
-public:
-    plugin_load_test()
-    {
-        daemon_->plugins().add_loader(std::make_unique<custom_loader>());
-        daemon_->plugins().add(std::make_unique<plugin>("already", ""));
-    }
-};
-
-} // !namespace
-
-BOOST_FIXTURE_TEST_SUITE(plugin_load_test_suite, plugin_load_test)
-
-BOOST_AUTO_TEST_CASE(basic)
-{
-    ctl_->send({
-        { "command",    "plugin-load"   },
-        { "plugin",     "magic"         }
-    });
-
-    wait_for([&] () {
-        return daemon_->plugins().has("magic");
-    });
-
-    BOOST_TEST(!daemon_->plugins().list().empty());
-    BOOST_TEST(daemon_->plugins().has("magic"));
-}
-
-BOOST_AUTO_TEST_SUITE(errors)
-
-BOOST_AUTO_TEST_CASE(not_found)
-{
-    boost::system::error_code result;
-
-    ctl_->send({
-        { "command",    "plugin-load"   },
-        { "plugin",     "unknown"       }
-    });
-    ctl_->recv([&] (auto code, auto) {
-        result = code;
-    });
-
-    wait_for([&] {
-        return result;
-    });
-
-    BOOST_TEST(result == plugin_error::not_found);
-}
-
-BOOST_AUTO_TEST_CASE(already_exists)
-{
-    boost::system::error_code result;
-    nlohmann::json message;
-
-    ctl_->send({
-        { "command",    "plugin-load"   },
-        { "plugin",     "already"       }
-    });
-    ctl_->recv([&] (auto rresult, auto rmessage) {
-        result = rresult;
-        message = rmessage;
-    });
-
-    wait_for([&] {
-        return result;
-    });
-
-    BOOST_TEST(result == plugin_error::already_exists);
-    BOOST_TEST(message["error"].template get<int>() == plugin_error::already_exists);
-    BOOST_TEST(message["errorCategory"].template get<std::string>() == "plugin");
-}
-
-BOOST_AUTO_TEST_CASE(exec_error)
-{
-    boost::system::error_code result;
-    nlohmann::json message;
-
-    ctl_->send({
-        { "command",    "plugin-load"   },
-        { "plugin",     "broken"        }
-    });
-    ctl_->recv([&] (auto rresult, auto rmessage) {
-        result = rresult;
-        message = rmessage;
-    });
-
-    wait_for([&] {
-        return result;
-    });
-
-    BOOST_TEST(result == plugin_error::exec_error);
-    BOOST_TEST(message["error"].template get<int>() == plugin_error::exec_error);
-    BOOST_TEST(message["errorCategory"].template get<std::string>() == "plugin");
-}
-
-BOOST_AUTO_TEST_SUITE_END()
-
-BOOST_AUTO_TEST_SUITE_END()
-
-} // !irccd
--- a/tests/src/plugin-logger/CMakeLists.txt	Thu Dec 14 21:51:22 2017 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,26 +0,0 @@
-#
-# CMakeLists.txt -- CMake build system for irccd
-#
-# Copyright (c) 2013-2017 David Demelier <markand@malikania.fr>
-#
-# Permission to use, copy, modify, and/or distribute this software for any
-# purpose with or without fee is hereby granted, provided that the above
-# copyright notice and this permission notice appear in all copies.
-#
-# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
-# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
-# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
-# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
-# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
-# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
-# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
-#
-
-irccd_define_test(
-    NAME plugin-logger
-    SOURCES main.cpp
-    LIBRARIES libirccd
-    FLAGS
-        PLUGIN_NAME="logger"
-        PLUGIN_PATH="${CMAKE_SOURCE_DIR}/plugins/logger/logger.js"
-)
--- a/tests/src/plugin-logger/main.cpp	Thu Dec 14 21:51:22 2017 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,155 +0,0 @@
-/*
- * main.cpp -- test logger plugin
- *
- * Copyright (c) 2013-2017 David Demelier <markand@malikania.fr>
- *
- * Permission to use, copy, modify, and/or distribute this software for any
- * purpose with or without fee is hereby granted, provided that the above
- * copyright notice and this permission notice appear in all copies.
- *
- * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
- * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
- * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
- * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
- * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
- * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
- * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
- */
-
-#include <fstream>
-#include <iterator>
-
-#define BOOST_TEST_MODULE "Logger plugin"
-#include <boost/test/unit_test.hpp>
-
-#include <irccd/daemon/irccd.hpp>
-#include <irccd/daemon/server.hpp>
-
-#include <irccd/test/plugin_test.hpp>
-
-namespace irccd {
-
-class logger_test : public plugin_test {
-protected:
-    std::string last() const
-    {
-        std::ifstream file(CMAKE_CURRENT_BINARY_DIR "/log.txt");
-
-        return std::string(std::istreambuf_iterator<char>(file.rdbuf()), {});
-    }
-
-public:
-    logger_test()
-        : plugin_test(PLUGIN_NAME, PLUGIN_PATH)
-    {
-        remove(CMAKE_CURRENT_BINARY_DIR "/log.txt");
-
-        plugin_->set_formats({
-            { "join", "join=#{server}:#{channel}:#{origin}:#{nickname}" },
-            { "kick", "kick=#{server}:#{channel}:#{origin}:#{nickname}:#{target}:#{reason}" },
-            { "me", "me=#{server}:#{channel}:#{origin}:#{nickname}:#{message}" },
-            { "message", "message=#{server}:#{channel}:#{origin}:#{nickname}:#{message}" },
-            { "mode", "mode=#{server}:#{origin}:#{channel}:#{mode}:#{limit}:#{user}:#{mask}" },
-            { "notice", "notice=#{server}:#{origin}:#{channel}:#{message}" },
-            { "part", "part=#{server}:#{channel}:#{origin}:#{nickname}:#{reason}" },
-            { "query", "query=#{server}:#{origin}:#{nickname}:#{message}" },
-            { "topic", "topic=#{server}:#{channel}:#{origin}:#{nickname}:#{topic}" },
-        });
-    }
-
-    void load(plugin_config config = plugin_config())
-    {
-        if (config.count("path") == 0)
-            config.emplace("path", CMAKE_CURRENT_BINARY_DIR "/log.txt");
-
-        plugin_->set_config(config);
-        plugin_->on_load(irccd_);
-    }
-};
-
-BOOST_FIXTURE_TEST_SUITE(logger_test_suite, logger_test)
-
-BOOST_AUTO_TEST_CASE(format_join)
-{
-    load();
-
-    plugin_->on_join(irccd_, {server_, "jean!jean@localhost", "#staff"});
-
-    BOOST_REQUIRE_EQUAL("join=test:#staff:jean!jean@localhost:jean\n", last());
-}
-
-BOOST_AUTO_TEST_CASE(format_kick)
-{
-    load();
-
-    plugin_->on_kick(irccd_, {server_, "jean!jean@localhost", "#staff", "badboy", "please do not flood"});
-
-    BOOST_REQUIRE_EQUAL("kick=test:#staff:jean!jean@localhost:jean:badboy:please do not flood\n", last());
-}
-
-BOOST_AUTO_TEST_CASE(format_me)
-{
-    load();
-
-    plugin_->on_me(irccd_, {server_, "jean!jean@localhost", "#staff", "is drinking water"});
-
-    BOOST_REQUIRE_EQUAL("me=test:#staff:jean!jean@localhost:jean:is drinking water\n", last());
-}
-
-BOOST_AUTO_TEST_CASE(format_message)
-{
-    load();
-
-    plugin_->on_message(irccd_, {server_, "jean!jean@localhost", "#staff", "hello guys"});
-
-    BOOST_REQUIRE_EQUAL("message=test:#staff:jean!jean@localhost:jean:hello guys\n", last());
-}
-
-BOOST_AUTO_TEST_CASE(format_mode)
-{
-    load();
-
-    plugin_->on_mode(irccd_, {server_, "jean!jean@localhost", "chris", "+i", "l", "u", "m"});
-
-    BOOST_REQUIRE_EQUAL("mode=test:jean!jean@localhost:chris:+i:l:u:m\n", last());
-}
-
-BOOST_AUTO_TEST_CASE(format_notice)
-{
-    load();
-
-    plugin_->on_notice(irccd_, {server_, "jean!jean@localhost", "chris", "tu veux voir mon chat ?"});
-
-    BOOST_REQUIRE_EQUAL("notice=test:jean!jean@localhost:chris:tu veux voir mon chat ?\n", last());
-}
-
-BOOST_AUTO_TEST_CASE(format_part)
-{
-    load();
-
-    plugin_->on_part(irccd_, {server_, "jean!jean@localhost", "#staff", "too noisy here"});
-
-    BOOST_REQUIRE_EQUAL("part=test:#staff:jean!jean@localhost:jean:too noisy here\n", last());
-}
-
-BOOST_AUTO_TEST_CASE(format_topic)
-{
-    load();
-
-    plugin_->on_topic(irccd_, {server_, "jean!jean@localhost", "#staff", "oh yeah yeaaaaaaaah"});
-
-    BOOST_REQUIRE_EQUAL("topic=test:#staff:jean!jean@localhost:jean:oh yeah yeaaaaaaaah\n", last());
-}
-
-BOOST_AUTO_TEST_CASE(fix_642)
-{
-    load();
-
-    plugin_->on_message(irccd_, {server_, "jean!jean@localhost", "#STAFF", "hello guys"});
-
-    BOOST_REQUIRE_EQUAL("message=test:#staff:jean!jean@localhost:jean:hello guys\n", last());
-}
-
-BOOST_AUTO_TEST_SUITE_END()
-
-} // !irccd
--- a/tests/src/plugin-plugin/CMakeLists.txt	Thu Dec 14 21:51:22 2017 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,26 +0,0 @@
-#
-# CMakeLists.txt -- CMake build system for irccd
-#
-# Copyright (c) 2013-2017 David Demelier <markand@malikania.fr>
-#
-# Permission to use, copy, modify, and/or distribute this software for any
-# purpose with or without fee is hereby granted, provided that the above
-# copyright notice and this permission notice appear in all copies.
-#
-# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
-# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
-# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
-# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
-# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
-# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
-# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
-#
-
-irccd_define_test(
-    NAME plugin-plugin
-    SOURCES main.cpp
-    LIBRARIES libirccd
-    FLAGS
-        PLUGIN_NAME="plugin"
-        PLUGIN_PATH="${CMAKE_SOURCE_DIR}/plugins/plugin/plugin.js"
-)
--- a/tests/src/plugin-plugin/main.cpp	Thu Dec 14 21:51:22 2017 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,127 +0,0 @@
-/*
- * main.cpp -- test plugin plugin
- *
- * Copyright (c) 2013-2017 David Demelier <markand@malikania.fr>
- *
- * Permission to use, copy, modify, and/or distribute this software for any
- * purpose with or without fee is hereby granted, provided that the above
- * copyright notice and this permission notice appear in all copies.
- *
- * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
- * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
- * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
- * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
- * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
- * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
- * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
- */
-
-#define BOOST_TEST_MODULE "Plugin plugin"
-#include <boost/test/unit_test.hpp>
-
-#include <irccd/string_util.hpp>
-
-#include <irccd/daemon/irccd.hpp>
-#include <irccd/daemon/plugin_service.hpp>
-#include <irccd/daemon/server.hpp>
-
-#include <irccd/test/plugin_test.hpp>
-
-namespace irccd {
-
-class fake_plugin : public plugin {
-public:
-    fake_plugin()
-        : plugin("fake", "")
-    {
-        set_author("jean");
-        set_version("0.0.0.0.0.1");
-        set_license("BEER");
-        set_summary("Fake White Beer 2000");
-    }
-};
-
-class test_fixture : public plugin_test {
-public:
-    test_fixture()
-        : plugin_test(PLUGIN_NAME, PLUGIN_PATH)
-    {
-        irccd_.plugins().add(std::make_shared<fake_plugin>());
-
-        plugin_->set_formats({
-            { "usage", "usage=#{plugin}:#{command}:#{server}:#{channel}:#{origin}:#{nickname}" },
-            { "info", "info=#{plugin}:#{command}:#{server}:#{channel}:#{origin}:#{nickname}:#{author}:#{license}:#{name}:#{summary}:#{version}" },
-            { "not-found", "not-found=#{plugin}:#{command}:#{server}:#{channel}:#{origin}:#{nickname}:#{name}" },
-            { "too-long", "too-long=#{plugin}:#{command}:#{server}:#{channel}:#{origin}:#{nickname}" }
-        });
-        plugin_->on_load(irccd_);
-    }
-};
-
-BOOST_FIXTURE_TEST_SUITE(test_fixture_suite, test_fixture)
-
-BOOST_AUTO_TEST_CASE(format_usage)
-{
-    nlohmann::json cmd;
-
-    plugin_->on_command(irccd_, {server_, "jean!jean@localhost", "#staff", ""});
-    cmd = server_->cqueue().front();
-
-    BOOST_REQUIRE_EQUAL(cmd["command"].get<std::string>(), "message");
-    BOOST_REQUIRE_EQUAL(cmd["target"].get<std::string>(), "#staff");
-    BOOST_REQUIRE_EQUAL(cmd["message"].get<std::string>(), "usage=plugin:!plugin:test:#staff:jean!jean@localhost:jean");
-
-    plugin_->on_command(irccd_, {server_, "jean!jean@localhost", "#staff", "fail"});
-    cmd = server_->cqueue().front();
-
-    BOOST_REQUIRE_EQUAL(cmd["command"].get<std::string>(), "message");
-    BOOST_REQUIRE_EQUAL(cmd["target"].get<std::string>(), "#staff");
-    BOOST_REQUIRE_EQUAL(cmd["message"].get<std::string>(), "usage=plugin:!plugin:test:#staff:jean!jean@localhost:jean");
-
-    plugin_->on_command(irccd_, {server_, "jean!jean@localhost", "#staff", "info"});
-    cmd = server_->cqueue().front();
-
-    BOOST_REQUIRE_EQUAL(cmd["command"].get<std::string>(), "message");
-    BOOST_REQUIRE_EQUAL(cmd["target"].get<std::string>(), "#staff");
-    BOOST_REQUIRE_EQUAL(cmd["message"].get<std::string>(), "usage=plugin:!plugin:test:#staff:jean!jean@localhost:jean");
-}
-
-BOOST_AUTO_TEST_CASE(format_info)
-{
-    plugin_->on_command(irccd_, {server_, "jean!jean@localhost", "#staff", "info fake"});
-
-    auto cmd = server_->cqueue().front();
-
-    BOOST_REQUIRE_EQUAL(cmd["command"].get<std::string>(), "message");
-    BOOST_REQUIRE_EQUAL(cmd["target"].get<std::string>(), "#staff");
-    BOOST_REQUIRE_EQUAL(cmd["message"].get<std::string>(), "info=plugin:!plugin:test:#staff:jean!jean@localhost:jean:jean:BEER:fake:Fake White Beer 2000:0.0.0.0.0.1");
-}
-
-BOOST_AUTO_TEST_CASE(format_not_found)
-{
-    plugin_->on_command(irccd_, {server_, "jean!jean@localhost", "#staff", "info doesnotexistsihope"});
-
-    auto cmd = server_->cqueue().front();
-
-    BOOST_REQUIRE_EQUAL(cmd["command"].get<std::string>(), "message");
-    BOOST_REQUIRE_EQUAL(cmd["target"].get<std::string>(), "#staff");
-    BOOST_REQUIRE_EQUAL(cmd["message"].get<std::string>(), "not-found=plugin:!plugin:test:#staff:jean!jean@localhost:jean:doesnotexistsihope");
-}
-
-BOOST_AUTO_TEST_CASE(format_too_long)
-{
-    for (int i = 0; i < 100; ++i)
-        irccd_.plugins().add(std::make_shared<plugin>(string_util::sprintf("plugin-n-%d", i), ""));
-
-    plugin_->on_command(irccd_, {server_, "jean!jean@localhost", "#staff", "list"});
-
-    auto cmd = server_->cqueue().front();
-
-    BOOST_REQUIRE_EQUAL(cmd["command"].get<std::string>(), "message");
-    BOOST_REQUIRE_EQUAL(cmd["target"].get<std::string>(), "#staff");
-    BOOST_REQUIRE_EQUAL(cmd["message"].get<std::string>(), "too-long=plugin:!plugin:test:#staff:jean!jean@localhost:jean");
-}
-
-BOOST_AUTO_TEST_SUITE_END()
-
-} // !irccd
--- a/tests/src/plugin-reload-command/CMakeLists.txt	Thu Dec 14 21:51:22 2017 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,23 +0,0 @@
-#
-# CMakeLists.txt -- CMake build system for irccd
-#
-# Copyright (c) 2013-2017 David Demelier <markand@malikania.fr>
-#
-# Permission to use, copy, modify, and/or distribute this software for any
-# purpose with or without fee is hereby granted, provided that the above
-# copyright notice and this permission notice appear in all copies.
-#
-# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
-# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
-# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
-# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
-# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
-# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
-# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
-#
-
-irccd_define_test(
-    NAME plugin-reload-command
-    SOURCES main.cpp
-    LIBRARIES libirccd libirccdctl
-)
--- a/tests/src/plugin-reload-command/main.cpp	Thu Dec 14 21:51:22 2017 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,138 +0,0 @@
-/*
- * main.cpp -- test plugin-reload remote command
- *
- * Copyright (c) 2013-2017 David Demelier <markand@malikania.fr>
- *
- * Permission to use, copy, modify, and/or distribute this software for any
- * purpose with or without fee is hereby granted, provided that the above
- * copyright notice and this permission notice appear in all copies.
- *
- * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
- * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
- * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
- * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
- * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
- * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
- * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
- */
-
-#define BOOST_TEST_MODULE "plugin-reload"
-#include <boost/test/unit_test.hpp>
-
-#include <irccd/daemon/command.hpp>
-#include <irccd/daemon/plugin_service.hpp>
-
-#include <irccd/test/command_test.hpp>
-
-namespace irccd {
-
-namespace {
-
-class custom_plugin : public plugin {
-public:
-    bool reloaded{false};
-
-    custom_plugin()
-        : plugin("test", "")
-    {
-    }
-
-    void on_reload(irccd&) override
-    {
-        reloaded = true;
-    }
-};
-
-class broken_plugin : public plugin {
-public:
-    using plugin::plugin;
-
-    void on_reload(irccd&) override
-    {
-        throw std::runtime_error("broken");
-    }
-};
-
-class plugin_reload_test : public command_test<plugin_reload_command> {
-protected:
-    std::shared_ptr<custom_plugin> plugin_;
-
-    plugin_reload_test()
-        : plugin_(std::make_shared<custom_plugin>())
-    {
-        daemon_->plugins().add(plugin_);
-        daemon_->plugins().add(std::make_unique<broken_plugin>("broken", ""));
-    }
-};
-
-} // !namespace
-
-BOOST_FIXTURE_TEST_SUITE(plugin_reload_test_suite, plugin_reload_test)
-
-BOOST_AUTO_TEST_CASE(basic)
-{
-    ctl_->send({
-        { "command",    "plugin-reload" },
-        { "plugin",     "test"          }
-    });
-
-    wait_for([&] () {
-        return plugin_->reloaded;
-    });
-
-    BOOST_TEST(plugin_->reloaded);
-}
-
-BOOST_AUTO_TEST_SUITE(errors)
-
-BOOST_AUTO_TEST_CASE(not_found)
-{
-    boost::system::error_code result;
-    nlohmann::json message;
-
-    ctl_->send({
-        { "command",    "plugin-reload" },
-        { "plugin",     "unknown"       }
-    });
-    ctl_->recv([&] (auto rresult, auto rmessage) {
-        result = rresult;
-        message = rmessage;
-    });
-
-    wait_for([&] {
-        return result;
-    });
-
-    BOOST_TEST(result == plugin_error::not_found);
-    BOOST_TEST(message["error"].template get<int>() == plugin_error::not_found);
-    BOOST_TEST(message["errorCategory"].template get<std::string>() == "plugin");
-}
-
-BOOST_AUTO_TEST_CASE(exec_error)
-{
-    boost::system::error_code result;
-    nlohmann::json message;
-
-    ctl_->send({
-        { "command",    "plugin-reload" },
-        { "plugin",     "broken"        }
-    });
-    ctl_->recv([&] (auto rresult, auto rmessage) {
-        result = rresult;
-        message = rmessage;
-    });
-
-    wait_for([&] {
-        return result;
-    });
-
-    BOOST_TEST(result == plugin_error::exec_error);
-    BOOST_TEST(message["error"].template get<int>() == plugin_error::exec_error);
-    BOOST_TEST(message["errorCategory"].template get<std::string>() == "plugin");
-}
-
-BOOST_AUTO_TEST_SUITE_END()
-
-BOOST_AUTO_TEST_SUITE_END()
-
-} // !irccd
--- a/tests/src/plugin-unload-command/CMakeLists.txt	Thu Dec 14 21:51:22 2017 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,23 +0,0 @@
-#
-# CMakeLists.txt -- CMake build system for irccd
-#
-# Copyright (c) 2013-2017 David Demelier <markand@malikania.fr>
-#
-# Permission to use, copy, modify, and/or distribute this software for any
-# purpose with or without fee is hereby granted, provided that the above
-# copyright notice and this permission notice appear in all copies.
-#
-# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
-# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
-# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
-# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
-# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
-# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
-# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
-#
-
-irccd_define_test(
-    NAME plugin-unload-command
-    SOURCES main.cpp
-    LIBRARIES libirccd libirccdctl
-)
--- a/tests/src/plugin-unload-command/main.cpp	Thu Dec 14 21:51:22 2017 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,139 +0,0 @@
-/*
- * main.cpp -- test plugin-unload remote command
- *
- * Copyright (c) 2013-2017 David Demelier <markand@malikania.fr>
- *
- * Permission to use, copy, modify, and/or distribute this software for any
- * purpose with or without fee is hereby granted, provided that the above
- * copyright notice and this permission notice appear in all copies.
- *
- * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
- * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
- * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
- * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
- * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
- * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
- * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
- */
-
-#define BOOST_TEST_MODULE "plugin-unload"
-#include <boost/test/unit_test.hpp>
-
-#include <irccd/daemon/command.hpp>
-#include <irccd/daemon/plugin_service.hpp>
-
-#include <irccd/test/command_test.hpp>
-
-namespace irccd {
-
-namespace {
-
-class custom_plugin : public plugin {
-public:
-    bool unloaded{false};
-
-    custom_plugin()
-        : plugin("test", "")
-    {
-    }
-
-    void on_unload(irccd &) override
-    {
-        unloaded = true;
-    }
-};
-
-class broken_plugin : public plugin {
-public:
-    using plugin::plugin;
-
-    void on_unload(irccd&) override
-    {
-        throw std::runtime_error("broken");
-    }
-};
-
-class plugin_unload_test : public command_test<plugin_unload_command> {
-protected:
-    std::shared_ptr<custom_plugin> plugin_;
-
-    plugin_unload_test()
-        : plugin_(std::make_shared<custom_plugin>())
-    {
-        daemon_->plugins().add(plugin_);
-        daemon_->plugins().add(std::make_unique<broken_plugin>("broken", ""));
-    }
-};
-
-} // !namespace
-
-BOOST_FIXTURE_TEST_SUITE(plugin_unload_test_suite, plugin_unload_test)
-
-BOOST_AUTO_TEST_CASE(basic)
-{
-    ctl_->send({
-        { "command",    "plugin-unload" },
-        { "plugin",     "test"          }
-    });
-
-    wait_for([&] () {
-        return plugin_->unloaded;
-    });
-
-    BOOST_TEST(plugin_->unloaded);
-}
-
-BOOST_AUTO_TEST_SUITE(errors)
-
-BOOST_AUTO_TEST_CASE(not_found)
-{
-    boost::system::error_code result;
-    nlohmann::json message;
-
-    ctl_->send({
-        { "command",    "plugin-unload" },
-        { "plugin",     "unknown"       }
-    });
-    ctl_->recv([&] (auto rresult, auto rmessage) {
-        result = rresult;
-        message = rmessage;
-    });
-
-    wait_for([&] {
-        return result;
-    });
-
-    BOOST_TEST(result == plugin_error::not_found);
-    BOOST_TEST(message["error"].template get<int>() == plugin_error::not_found);
-    BOOST_TEST(message["errorCategory"].template get<std::string>() == "plugin");
-}
-
-BOOST_AUTO_TEST_CASE(exec_error)
-{
-    boost::system::error_code result;
-    nlohmann::json message;
-
-    ctl_->send({
-        { "command",    "plugin-unload" },
-        { "plugin",     "broken"        }
-    });
-    ctl_->recv([&] (auto rresult, auto rmessage) {
-        result = rresult;
-        message = rmessage;
-    });
-
-    wait_for([&] {
-        return result;
-    });
-
-    BOOST_TEST(result == plugin_error::exec_error);
-    BOOST_TEST(message["error"].template get<int>() == plugin_error::exec_error);
-    BOOST_TEST(message["errorCategory"].template get<std::string>() == "plugin");
-    BOOST_TEST(!daemon_->plugins().has("broken"));
-}
-
-BOOST_AUTO_TEST_SUITE_END()
-
-BOOST_AUTO_TEST_SUITE_END()
-
-} // !irccd
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/src/plugins/CMakeLists.txt	Fri Dec 15 15:37:58 2017 +0100
@@ -0,0 +1,24 @@
+#
+# CMakeLists.txt -- CMake build system for irccd
+#
+# Copyright (c) 2013-2017 David Demelier <markand@malikania.fr>
+#
+# Permission to use, copy, modify, and/or distribute this software for any
+# purpose with or without fee is hereby granted, provided that the above
+# copyright notice and this permission notice appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+#
+
+add_subdirectory(ask)
+add_subdirectory(auth)
+add_subdirectory(hangman)
+add_subdirectory(history)
+add_subdirectory(logger)
+add_subdirectory(plugin)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/src/plugins/ask/CMakeLists.txt	Fri Dec 15 15:37:58 2017 +0100
@@ -0,0 +1,28 @@
+#
+# CMakeLists.txt -- CMake build system for irccd
+#
+# Copyright (c) 2013-2017 David Demelier <markand@malikania.fr>
+#
+# Permission to use, copy, modify, and/or distribute this software for any
+# purpose with or without fee is hereby granted, provided that the above
+# copyright notice and this permission notice appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+#
+
+irccd_define_test(
+    NAME plugin-ask
+    SOURCES
+        ${CMAKE_CURRENT_SOURCE_DIR}/main.cpp
+        ${CMAKE_CURRENT_SOURCE_DIR}/answers.conf
+    LIBRARIES libirccd
+    FLAGS
+        PLUGIN_NAME="ask"
+        PLUGIN_PATH="${CMAKE_SOURCE_DIR}/plugins/ask/ask.js"
+)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/src/plugins/ask/answers.conf	Fri Dec 15 15:37:58 2017 +0100
@@ -0,0 +1,2 @@
+NO
+YES
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/src/plugins/ask/main.cpp	Fri Dec 15 15:37:58 2017 +0100
@@ -0,0 +1,76 @@
+/*
+ * main.cpp -- test ask plugin
+ *
+ * Copyright (c) 2013-2017 David Demelier <markand@malikania.fr>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#define BOOST_TEST_MODULE "Ask plugin"
+#include <boost/test/unit_test.hpp>
+
+#include <irccd/daemon/irccd.hpp>
+#include <irccd/daemon/server.hpp>
+
+#include <irccd/test/plugin_test.hpp>
+
+namespace irccd {
+
+class ask_test : public plugin_test {
+public:
+    inline ask_test()
+        : plugin_test(PLUGIN_NAME, PLUGIN_PATH)
+    {
+        plugin_->set_config({
+            { "file", CMAKE_CURRENT_SOURCE_DIR "/answers.conf" }
+        });
+        plugin_->on_load(irccd_);
+    }
+};
+
+BOOST_FIXTURE_TEST_SUITE(ask_test_suite, ask_test)
+
+BOOST_AUTO_TEST_CASE(basic)
+{
+    bool no = false;
+    bool yes = false;
+
+    /*
+     * Invoke the plugin 1000 times, it will be very unlucky to not have both
+     * answers in that amount of tries.
+     */
+    for (int i = 0; i < 1000; ++i) {
+        plugin_->on_command(irccd_, {server_, "tester", "#dummy", ""});
+
+        auto cmd = server_->cqueue().front();
+
+        BOOST_REQUIRE_EQUAL(cmd["command"].get<std::string>(), "message");
+        BOOST_REQUIRE_EQUAL(cmd["target"].get<std::string>(), "#dummy");
+
+        auto msg = cmd["message"].get<std::string>();
+
+        if (msg == "tester, YES")
+            yes = true;
+        if (msg == "tester, NO")
+            no = true;
+
+        server_->cqueue().clear();
+    }
+
+    BOOST_REQUIRE(no);
+    BOOST_REQUIRE(yes);
+}
+
+BOOST_AUTO_TEST_SUITE_END()
+
+} // !irccd
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/src/plugins/auth/CMakeLists.txt	Fri Dec 15 15:37:58 2017 +0100
@@ -0,0 +1,26 @@
+#
+# CMakeLists.txt -- CMake build system for irccd
+#
+# Copyright (c) 2013-2017 David Demelier <markand@malikania.fr>
+#
+# Permission to use, copy, modify, and/or distribute this software for any
+# purpose with or without fee is hereby granted, provided that the above
+# copyright notice and this permission notice appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+#
+
+irccd_define_test(
+    NAME plugin-auth
+    SOURCES main.cpp
+    LIBRARIES libirccd
+    FLAGS
+        PLUGIN_NAME="auth"
+        PLUGIN_PATH="${CMAKE_SOURCE_DIR}/plugins/auth/auth.js"
+)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/src/plugins/auth/main.cpp	Fri Dec 15 15:37:58 2017 +0100
@@ -0,0 +1,93 @@
+/*
+ * main.cpp -- test auth plugin
+ *
+ * Copyright (c) 2013-2017 David Demelier <markand@malikania.fr>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#define BOOST_TEST_MODULE "Auth plugin"
+#include <boost/test/unit_test.hpp>
+
+#include <irccd/daemon/irccd.hpp>
+#include <irccd/daemon/server.hpp>
+
+#include <irccd/test/plugin_test.hpp>
+
+namespace irccd {
+
+class auth_test : public plugin_test {
+protected:
+    std::shared_ptr<journal_server> nickserv1_;
+    std::shared_ptr<journal_server> nickserv2_;
+    std::shared_ptr<journal_server> quakenet_;
+
+public:
+    auth_test()
+        : plugin_test(PLUGIN_NAME, PLUGIN_PATH)
+        , nickserv1_(std::make_shared<journal_server>(service_, "nickserv1"))
+        , nickserv2_(std::make_shared<journal_server>(service_, "nickserv2"))
+        , quakenet_(std::make_shared<journal_server>(service_, "quakenet"))
+    {
+        plugin_->set_config({
+            { "nickserv1.type", "nickserv" },
+            { "nickserv1.password", "plopation" },
+            { "nickserv2.type", "nickserv" },
+            { "nickserv2.password", "something" },
+            { "nickserv2.username", "jean" },
+            { "quakenet.type", "quakenet" },
+            { "quakenet.password", "hello" },
+            { "quakenet.username", "mario" }
+        });
+        plugin_->on_load(irccd_);
+    }
+};
+
+BOOST_FIXTURE_TEST_SUITE(auth_test_suite, auth_test)
+
+BOOST_AUTO_TEST_CASE(nickserv1)
+{
+    plugin_->on_connect(irccd_, {nickserv1_});
+
+    auto cmd = nickserv1_->cqueue().front();
+
+    BOOST_REQUIRE_EQUAL(cmd["command"].get<std::string>(), "message");
+    BOOST_REQUIRE_EQUAL(cmd["target"].get<std::string>(), "NickServ");
+    BOOST_REQUIRE_EQUAL(cmd["message"].get<std::string>(), "identify plopation");
+}
+
+BOOST_AUTO_TEST_CASE(nickserv2)
+{
+    plugin_->on_connect(irccd_, {nickserv2_});
+
+    auto cmd = nickserv2_->cqueue().front();
+
+    BOOST_REQUIRE_EQUAL(cmd["command"].get<std::string>(), "message");
+    BOOST_REQUIRE_EQUAL(cmd["target"].get<std::string>(), "NickServ");
+    BOOST_REQUIRE_EQUAL(cmd["message"].get<std::string>(), "identify jean something");
+}
+
+BOOST_AUTO_TEST_CASE(quakenet)
+{
+    plugin_->on_connect(irccd_, {quakenet_});
+
+    auto cmd = quakenet_->cqueue().front();
+
+    BOOST_REQUIRE_EQUAL(cmd["command"].get<std::string>(), "message");
+    BOOST_REQUIRE_EQUAL(cmd["target"].get<std::string>(), "Q@CServe.quakenet.org");
+    BOOST_REQUIRE_EQUAL(cmd["message"].get<std::string>(), "AUTH mario hello");
+}
+
+BOOST_AUTO_TEST_SUITE_END()
+
+} // !irccd
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/src/plugins/hangman/CMakeLists.txt	Fri Dec 15 15:37:58 2017 +0100
@@ -0,0 +1,29 @@
+#
+# CMakeLists.txt -- CMake build system for irccd
+#
+# Copyright (c) 2013-2017 David Demelier <markand@malikania.fr>
+#
+# Permission to use, copy, modify, and/or distribute this software for any
+# purpose with or without fee is hereby granted, provided that the above
+# copyright notice and this permission notice appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+#
+
+irccd_define_test(
+    NAME plugin-hangman
+    SOURCES
+        main.cpp
+        words.conf
+        wordlist_fix_644.conf
+    LIBRARIES libirccd
+    FLAGS
+        PLUGIN_NAME="hangman"
+        PLUGIN_PATH="${CMAKE_SOURCE_DIR}/plugins/hangman/hangman.js"
+)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/src/plugins/hangman/main.cpp	Fri Dec 15 15:37:58 2017 +0100
@@ -0,0 +1,380 @@
+/*
+ * main.cpp -- test hangman plugin
+ *
+ * Copyright (c) 2013-2017 David Demelier <markand@malikania.fr>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include <unordered_map>
+#include <unordered_set>
+
+#define BOOST_TEST_MODULE "Hangman plugin"
+#include <boost/test/unit_test.hpp>
+
+#include <irccd/daemon/irccd.hpp>
+#include <irccd/daemon/server.hpp>
+
+#include <irccd/test/plugin_test.hpp>
+
+namespace irccd {
+
+class hangman_test : public plugin_test {
+public:
+    hangman_test()
+        : plugin_test(PLUGIN_NAME, PLUGIN_PATH)
+    {
+        plugin_->set_formats({
+            { "asked", "asked=#{plugin}:#{command}:#{server}:#{channel}:#{origin}:#{nickname}:#{letter}" },
+            { "dead", "dead=#{plugin}:#{command}:#{server}:#{channel}:#{origin}:#{nickname}:#{word}" },
+            { "found", "found=#{plugin}:#{command}:#{server}:#{channel}:#{origin}:#{nickname}:#{word}" },
+            { "start", "start=#{plugin}:#{command}:#{server}:#{channel}:#{origin}:#{nickname}:#{word}" },
+            { "running", "running=#{plugin}:#{command}:#{server}:#{channel}:#{origin}:#{nickname}:#{word}" },
+            { "win", "win=#{plugin}:#{command}:#{server}:#{channel}:#{origin}:#{nickname}:#{word}" },
+            { "wrong-letter", "wrong-letter=#{plugin}:#{command}:#{server}:#{channel}:#{origin}:#{nickname}:#{letter}" },
+            { "wrong-player", "wrong-player=#{plugin}:#{command}:#{server}:#{channel}:#{origin}:#{nickname}:#{letter}" },
+            { "wrong-word", "wrong-word=#{plugin}:#{command}:#{server}:#{channel}:#{origin}:#{nickname}:#{word}" }
+        });
+    }
+
+    void load(plugin_config config = {})
+    {
+        // Add file if not there.
+        if (config.count("file") == 0)
+            config.emplace("file", CMAKE_CURRENT_SOURCE_DIR "/words.conf");
+
+        plugin_->set_config(config);
+        plugin_->on_load(irccd_);
+    }
+};
+
+BOOST_FIXTURE_TEST_SUITE(hangman_test_suite, hangman_test)
+
+BOOST_AUTO_TEST_CASE(asked)
+{
+    load({{ "collaborative", "false" }});
+
+    plugin_->on_command(irccd_, {server_, "jean!jean@localhost", "#hangman", ""});
+
+    auto cmd = server_->cqueue().back();
+
+    BOOST_TEST(cmd["command"].get<std::string>() == "message");
+    BOOST_TEST(cmd["target"].get<std::string>() == "#hangman");
+    BOOST_TEST(cmd["message"].get<std::string>() == "start=hangman:!hangman:test:#hangman:jean!jean@localhost:jean:_ _ _");
+
+    plugin_->on_message(irccd_, {server_, "jean!jean@localhost", "#hangman", "s"});
+    cmd = server_->cqueue().back();
+
+    BOOST_TEST(cmd["command"].get<std::string>() == "message");
+    BOOST_TEST(cmd["target"].get<std::string>() == "#hangman");
+    BOOST_TEST(cmd["message"].get<std::string>() == "found=hangman:!hangman:test:#hangman:jean!jean@localhost:jean:s _ _");
+
+    plugin_->on_message(irccd_, {server_, "jean!jean@localhost", "#hangman", "s"});
+    cmd = server_->cqueue().back();
+
+    BOOST_TEST(cmd["command"].get<std::string>() == "message");
+    BOOST_TEST(cmd["target"].get<std::string>() == "#hangman");
+    BOOST_TEST(cmd["message"].get<std::string>() == "asked=hangman:!hangman:test:#hangman:jean!jean@localhost:jean:s");
+}
+
+BOOST_AUTO_TEST_CASE(dead)
+{
+    load({{ "collaborative", "false" }});
+
+    plugin_->on_command(irccd_, {server_, "jean!jean@localhost", "#hangman", ""});
+    plugin_->on_message(irccd_, {server_, "jean!jean@localhost", "#hangman", "a"});
+    plugin_->on_message(irccd_, {server_, "jean!jean@localhost", "#hangman", "b"});
+    plugin_->on_message(irccd_, {server_, "jean!jean@localhost", "#hangman", "c"});
+    plugin_->on_message(irccd_, {server_, "jean!jean@localhost", "#hangman", "d"});
+    plugin_->on_message(irccd_, {server_, "jean!jean@localhost", "#hangman", "e"});
+    plugin_->on_message(irccd_, {server_, "jean!jean@localhost", "#hangman", "f"});
+    plugin_->on_message(irccd_, {server_, "jean!jean@localhost", "#hangman", "g"});
+    plugin_->on_message(irccd_, {server_, "jean!jean@localhost", "#hangman", "h"});
+    plugin_->on_message(irccd_, {server_, "jean!jean@localhost", "#hangman", "i"});
+    plugin_->on_message(irccd_, {server_, "jean!jean@localhost", "#hangman", "j"});
+
+    auto cmd = server_->cqueue().back();
+
+    BOOST_TEST(cmd["command"].get<std::string>() == "message");
+    BOOST_TEST(cmd["target"].get<std::string>() == "#hangman");
+    BOOST_TEST(cmd["message"].get<std::string>() == "dead=hangman:!hangman:test:#hangman:jean!jean@localhost:jean:sky");
+}
+
+BOOST_AUTO_TEST_CASE(found)
+{
+    load({{ "collaborative", "false" }});
+
+    plugin_->on_command(irccd_, {server_, "jean!jean@localhost", "#hangman", ""});
+    plugin_->on_message(irccd_, {server_, "jean!jean@localhost", "#hangman", "s"});
+
+    auto cmd = server_->cqueue().back();
+
+    BOOST_TEST(cmd["command"].get<std::string>() == "message");
+    BOOST_TEST(cmd["target"].get<std::string>() == "#hangman");
+    BOOST_TEST(cmd["message"].get<std::string>() == "found=hangman:!hangman:test:#hangman:jean!jean@localhost:jean:s _ _");
+}
+
+BOOST_AUTO_TEST_CASE(start)
+{
+    load();
+
+    plugin_->on_command(irccd_, {server_, "jean!jean@localhost", "#hangman", ""});
+
+    auto cmd = server_->cqueue().back();
+
+    BOOST_TEST(cmd["command"].get<std::string>() == "message");
+    BOOST_TEST(cmd["target"].get<std::string>() == "#hangman");
+    BOOST_TEST(cmd["message"].get<std::string>() == "start=hangman:!hangman:test:#hangman:jean!jean@localhost:jean:_ _ _");
+}
+
+BOOST_AUTO_TEST_CASE(win1)
+{
+    load({{ "collaborative", "false" }});
+
+    plugin_->on_command(irccd_, {server_, "jean!jean@localhost", "#hangman", ""});
+    plugin_->on_message(irccd_, {server_, "jean!jean@localhost", "#hangman", "s"});
+    plugin_->on_message(irccd_, {server_, "jean!jean@localhost", "#hangman", "k"});
+    plugin_->on_message(irccd_, {server_, "jean!jean@localhost", "#hangman", "y"});
+
+    auto cmd = server_->cqueue().back();
+
+    BOOST_TEST(cmd["command"].get<std::string>() == "message");
+    BOOST_TEST(cmd["target"].get<std::string>() == "#hangman");
+    BOOST_TEST(cmd["message"].get<std::string>() == "win=hangman:!hangman:test:#hangman:jean!jean@localhost:jean:sky");
+}
+
+BOOST_AUTO_TEST_CASE(win2)
+{
+    load({{ "collaborative", "false" }});
+
+    plugin_->on_command(irccd_, {server_, "jean!jean@localhost", "#hangman", ""});
+    plugin_->on_command(irccd_, {server_, "jean!jean@localhost", "#hangman", "sky"});
+
+    auto cmd = server_->cqueue().back();
+
+    BOOST_TEST(cmd["command"].get<std::string>() == "message");
+    BOOST_TEST(cmd["target"].get<std::string>() == "#hangman");
+    BOOST_TEST(cmd["message"].get<std::string>() == "win=hangman:!hangman:test:#hangman:jean!jean@localhost:jean:sky");
+}
+
+BOOST_AUTO_TEST_CASE(wrong_letter)
+{
+    load();
+
+    plugin_->on_command(irccd_, {server_, "jean!jean@localhost", "#hangman", ""});
+    plugin_->on_message(irccd_, {server_, "jean!jean@localhost", "#hangman", "x"});
+
+    auto cmd = server_->cqueue().back();
+
+    BOOST_TEST(cmd["command"].get<std::string>() == "message");
+    BOOST_TEST(cmd["target"].get<std::string>() == "#hangman");
+    BOOST_TEST(cmd["message"].get<std::string>() == "wrong-letter=hangman:!hangman:test:#hangman:jean!jean@localhost:jean:x");
+}
+
+BOOST_AUTO_TEST_CASE(wrongWord)
+{
+    load();
+
+    plugin_->on_command(irccd_, {server_, "jean!jean@localhost", "#hangman", ""});
+    plugin_->on_command(irccd_, {server_, "jean!jean@localhost", "#hangman", "cheese"});
+
+    auto cmd = server_->cqueue().back();
+
+    BOOST_TEST(cmd["command"].get<std::string>() == "message");
+    BOOST_TEST(cmd["target"].get<std::string>() == "#hangman");
+    BOOST_TEST(cmd["message"].get<std::string>() == "wrong-word=hangman:!hangman:test:#hangman:jean!jean@localhost:jean:cheese");
+}
+
+BOOST_AUTO_TEST_CASE(collaborative_disabled)
+{
+    // Disable collaborative mode.
+    load({{ "collaborative", "false" }});
+
+    plugin_->on_command(irccd_, {server_, "jean!jean@localhost", "#hangman", ""});
+    plugin_->on_message(irccd_, {server_, "jean!jean@localhost", "#hangman", "s"});
+
+    auto cmd = server_->cqueue().back();
+
+    BOOST_TEST(cmd["command"].get<std::string>() == "message");
+    BOOST_TEST(cmd["target"].get<std::string>() == "#hangman");
+    BOOST_TEST(cmd["message"].get<std::string>() == "found=hangman:!hangman:test:#hangman:jean!jean@localhost:jean:s _ _");
+
+    plugin_->on_message(irccd_, {server_, "jean!jean@localhost", "#hangman", "k"});
+    cmd = server_->cqueue().back();
+
+    BOOST_TEST(cmd["command"].get<std::string>() == "message");
+    BOOST_TEST(cmd["target"].get<std::string>() == "#hangman");
+    BOOST_TEST(cmd["message"].get<std::string>() == "found=hangman:!hangman:test:#hangman:jean!jean@localhost:jean:s k _");
+}
+
+BOOST_AUTO_TEST_CASE(collaborative_enabled)
+{
+    // Enable collaborative mode.
+    load({{ "collaborative", "true" }});
+
+    plugin_->on_command(irccd_, {server_, "jean!jean@localhost", "#hangman", ""});
+    plugin_->on_message(irccd_, {server_, "jean!jean@localhost", "#hangman", "s"});
+
+    auto cmd = server_->cqueue().back();
+
+    BOOST_TEST(cmd["command"].get<std::string>() == "message");
+    BOOST_TEST(cmd["target"].get<std::string>() == "#hangman");
+    BOOST_TEST(cmd["message"].get<std::string>() == "found=hangman:!hangman:test:#hangman:jean!jean@localhost:jean:s _ _");
+
+    plugin_->on_message(irccd_, {server_, "jean!jean@localhost", "#hangman", "k"});
+    cmd = server_->cqueue().back();
+
+    BOOST_TEST(cmd["command"].get<std::string>() == "message");
+    BOOST_TEST(cmd["target"].get<std::string>() == "#hangman");
+    BOOST_TEST(cmd["message"].get<std::string>() == "wrong-player=hangman:!hangman:test:#hangman:jean!jean@localhost:jean:k");
+
+    plugin_->on_message(irccd_, {server_, "francis!francis@localhost", "#hangman", "k"});
+    cmd = server_->cqueue().back();
+
+    BOOST_TEST(cmd["command"].get<std::string>() == "message");
+    BOOST_TEST(cmd["target"].get<std::string>() == "#hangman");
+    BOOST_TEST(cmd["message"].get<std::string>() == "found=hangman:!hangman:test:#hangman:francis!francis@localhost:francis:s k _");
+}
+
+BOOST_AUTO_TEST_CASE(case_fix_642)
+{
+    load();
+
+    plugin_->on_command(irccd_, {server_, "jean!jean@localhost", "#hangman", ""});
+    plugin_->on_message(irccd_, {server_, "jean!jean@localhost", "#HANGMAN", "s"});
+
+    auto cmd = server_->cqueue().back();
+
+    BOOST_TEST(cmd["command"].get<std::string>() == "message");
+    BOOST_TEST(cmd["target"].get<std::string>() == "#hangman");
+    BOOST_TEST(cmd["message"].get<std::string>() == "found=hangman:!hangman:test:#hangman:jean!jean@localhost:jean:s _ _");
+
+    plugin_->on_message(irccd_, {server_, "jean!jean@localhost", "#HaNGMaN", "k"});
+    cmd = server_->cqueue().back();
+
+    BOOST_TEST(cmd["command"].get<std::string>() == "message");
+    BOOST_TEST(cmd["target"].get<std::string>() == "#hangman");
+    BOOST_TEST(cmd["message"].get<std::string>() == "wrong-player=hangman:!hangman:test:#hangman:jean!jean@localhost:jean:k");
+
+    plugin_->on_message(irccd_, {server_, "francis!francis@localhost", "#hAngmAn", "k"});
+    cmd = server_->cqueue().back();
+
+    BOOST_TEST(cmd["command"].get<std::string>() == "message");
+    BOOST_TEST(cmd["target"].get<std::string>() == "#hangman");
+    BOOST_TEST(cmd["message"].get<std::string>() == "found=hangman:!hangman:test:#hangman:francis!francis@localhost:francis:s k _");
+}
+
+BOOST_AUTO_TEST_CASE(query)
+{
+    load();
+
+    // Query mode is never collaborative.
+    plugin_->on_command(irccd_, {server_, "jean!jean@localhost", "irccd", ""});
+
+    auto cmd = server_->cqueue().back();
+
+    BOOST_TEST(cmd["command"].get<std::string>() == "message");
+    BOOST_TEST(cmd["target"].get<std::string>() == "jean!jean@localhost");
+    BOOST_TEST(cmd["message"].get<std::string>() == "start=hangman:!hangman:test:jean!jean@localhost:jean!jean@localhost:jean:_ _ _");
+
+    plugin_->on_message(irccd_, {server_, "jean!jean@localhost", "irccd", "s"});
+    cmd = server_->cqueue().back();
+
+    BOOST_TEST(cmd["command"].get<std::string>() == "message");
+    BOOST_TEST(cmd["target"].get<std::string>() == "jean!jean@localhost");
+    BOOST_TEST(cmd["message"].get<std::string>() == "found=hangman:!hangman:test:jean!jean@localhost:jean!jean@localhost:jean:s _ _");
+
+    plugin_->on_message(irccd_, {server_, "jean!jean@localhost", "irccd", "k"});
+    cmd = server_->cqueue().back();
+
+    BOOST_TEST(cmd["command"].get<std::string>() == "message");
+    BOOST_TEST(cmd["target"].get<std::string>() == "jean!jean@localhost");
+    BOOST_TEST(cmd["message"].get<std::string>() == "found=hangman:!hangman:test:jean!jean@localhost:jean!jean@localhost:jean:s k _");
+
+    plugin_->on_command(irccd_, {server_, "jean!jean@localhost", "irccd", "sky"});
+    cmd = server_->cqueue().back();
+
+    BOOST_TEST(cmd["command"].get<std::string>() == "message");
+    BOOST_TEST(cmd["target"].get<std::string>() == "jean!jean@localhost");
+    BOOST_TEST(cmd["message"].get<std::string>() == "win=hangman:!hangman:test:jean!jean@localhost:jean!jean@localhost:jean:sky");
+}
+
+BOOST_AUTO_TEST_CASE(running)
+{
+    load();
+
+    plugin_->on_command(irccd_, {server_, "jean!jean@localhost", "#hangman", ""});
+    plugin_->on_message(irccd_, {server_, "jean!jean@localhost", "#hangman", "y"});
+    plugin_->on_command(irccd_, {server_, "jean!jean@localhost", "#hangman", ""});
+
+    auto cmd = server_->cqueue().back();
+
+    BOOST_TEST(cmd["command"].get<std::string>() == "message");
+    BOOST_TEST(cmd["target"].get<std::string>() == "#hangman");
+    BOOST_TEST(cmd["message"].get<std::string>() == "running=hangman:!hangman:test:#hangman:jean!jean@localhost:jean:_ _ y");
+}
+
+BOOST_AUTO_TEST_CASE(wordlist_fix_644)
+{
+    /*
+     * To be sure that the selection use the same list, we create a list of
+     * three words that has different size to determine which one was selected.
+     *
+     * Then we run 3 games and verify that the old selection is not the same
+     * as the current.
+     *
+     * This is not very accurate but it's better than nothing.
+     */
+    load({{ "file", CMAKE_CURRENT_SOURCE_DIR "/wordlist_fix_644.conf" }});
+
+    std::unordered_map<unsigned, std::string> words{
+        { 5, "abc"     },
+        { 7, "abcd"    },
+        { 9, "abcde"   }
+    };
+    std::unordered_set<unsigned> found;
+
+    plugin_->set_formats({
+        { "start", "#{word}" }
+    });
+
+    unsigned last, current;
+
+    // 1. Initial game + finish.
+    plugin_->on_command(irccd_, {server_, "jean!jean@localhost", "#hangman", ""});
+    last = server_->cqueue().back()["message"].get<std::string>().length();
+    found.insert(last);
+    plugin_->on_command(irccd_, {server_, "jean!jean@localhost", "#hangman", words[last]});
+
+    // 2. Current must not be the last one.
+    plugin_->on_command(irccd_, {server_, "jean!jean@localhost", "#hangman", ""});
+    current = server_->cqueue().back()["message"].get<std::string>().length();
+
+    BOOST_TEST(last != current);
+    BOOST_TEST(0U == found.count(current));
+
+    found.insert(current);
+    last = current;
+    plugin_->on_command(irccd_, {server_, "jean!jean@localhost", "#hangman", words[current]});
+
+    // 3. Last word must be the one that is kept into the map.
+    plugin_->on_command(irccd_, {server_, "jean!jean@localhost", "#hangman", ""});
+    current = server_->cqueue().back()["message"].get<std::string>().length();
+
+    BOOST_TEST(last != current);
+    BOOST_TEST(0U == found.count(current));
+}
+
+BOOST_AUTO_TEST_SUITE_END()
+
+} // !irccd
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/src/plugins/hangman/wordlist_fix_644.conf	Fri Dec 15 15:37:58 2017 +0100
@@ -0,0 +1,3 @@
+abc
+abcd
+abcde
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/src/plugins/hangman/words.conf	Fri Dec 15 15:37:58 2017 +0100
@@ -0,0 +1,1 @@
+sky
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/src/plugins/history/CMakeLists.txt	Fri Dec 15 15:37:58 2017 +0100
@@ -0,0 +1,26 @@
+#
+# CMakeLists.txt -- CMake build system for irccd
+#
+# Copyright (c) 2013-2017 David Demelier <markand@malikania.fr>
+#
+# Permission to use, copy, modify, and/or distribute this software for any
+# purpose with or without fee is hereby granted, provided that the above
+# copyright notice and this permission notice appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+#
+
+irccd_define_test(
+    NAME plugin-history
+    SOURCES main.cpp
+    LIBRARIES libirccd
+    FLAGS
+        PLUGIN_NAME="history"
+        PLUGIN_PATH="${CMAKE_SOURCE_DIR}/plugins/history/history.js"
+)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/src/plugins/history/broken-conf.json	Fri Dec 15 15:37:58 2017 +0100
@@ -0,0 +1,1 @@
+this is not a json file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/src/plugins/history/main.cpp	Fri Dec 15 15:37:58 2017 +0100
@@ -0,0 +1,149 @@
+/*
+ * main.cpp -- test history plugin
+ *
+ * Copyright (c) 2013-2017 David Demelier <markand@malikania.fr>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include <regex>
+
+#define BOOST_TEST_MODULE "History plugin"
+#include <boost/test/unit_test.hpp>
+
+#include <irccd/daemon/irccd.hpp>
+#include <irccd/daemon/server.hpp>
+
+#include <irccd/test/plugin_test.hpp>
+
+namespace irccd {
+
+class history_test : public plugin_test {
+public:
+    history_test()
+        : plugin_test(PLUGIN_NAME, PLUGIN_PATH)
+    {
+        plugin_->set_formats({
+            { "error", "error=#{plugin}:#{command}:#{server}:#{channel}:#{origin}:#{nickname}" },
+            { "seen", "seen=#{plugin}:#{command}:#{server}:#{channel}:#{origin}:#{nickname}:#{target}:%H:%M" },
+            { "said", "said=#{plugin}:#{command}:#{server}:#{channel}:#{origin}:#{nickname}:#{target}:#{message}:%H:%M" },
+            { "unknown", "unknown=#{plugin}:#{command}:#{server}:#{channel}:#{origin}:#{nickname}:#{target}" },
+        });
+    }
+
+    void load(plugin_config config = {})
+    {
+        // Add file if not there.
+        if (config.count("file") == 0)
+            config.emplace("file", CMAKE_CURRENT_SOURCE_DIR "/words.conf");
+
+        plugin_->set_config(config);
+        plugin_->on_load(irccd_);
+    }
+};
+
+BOOST_FIXTURE_TEST_SUITE(history_test_suite, history_test)
+
+BOOST_AUTO_TEST_CASE(format_error)
+{
+    load({{"file", CMAKE_CURRENT_SOURCE_DIR "/broken-conf.json"}});
+
+    plugin_->on_command(irccd_, {server_, "jean!jean@localhost", "#history", "seen francis"});
+
+    auto cmd = server_->cqueue().front();
+
+    BOOST_REQUIRE_EQUAL(cmd["command"].get<std::string>(), "message");
+    BOOST_REQUIRE_EQUAL(cmd["target"].get<std::string>(), "#history");
+    BOOST_REQUIRE_EQUAL(cmd["message"].get<std::string>(), "error=history:!history:test:#history:jean!jean@localhost:jean");
+}
+
+BOOST_AUTO_TEST_CASE(format_seen)
+{
+    const std::regex rule("seen=history:!history:test:#history:destructor!dst@localhost:destructor:jean:\\d{2}:\\d{2}");
+
+    remove(CMAKE_CURRENT_BINARY_DIR "/seen.json");
+    load({{ "file", CMAKE_CURRENT_BINARY_DIR "/seen.json" }});
+
+    plugin_->on_message(irccd_, {server_, "jean!jean@localhost", "#history", "hello"});
+    plugin_->on_command(irccd_, {server_, "destructor!dst@localhost", "#history", "seen jean"});
+
+    auto cmd = server_->cqueue().front();
+
+    BOOST_REQUIRE_EQUAL(cmd["command"].get<std::string>(), "message");
+    BOOST_REQUIRE_EQUAL(cmd["target"].get<std::string>(), "#history");
+    BOOST_REQUIRE(std::regex_match(cmd["message"].get<std::string>(), rule));
+}
+
+BOOST_AUTO_TEST_CASE(format_said)
+{
+    std::regex rule("said=history:!history:test:#history:destructor!dst@localhost:destructor:jean:hello:\\d{2}:\\d{2}");
+
+    remove(CMAKE_CURRENT_BINARY_DIR "/said.json");
+    load({{ "file", CMAKE_CURRENT_BINARY_DIR "/said.json" }});
+
+    plugin_->on_message(irccd_, {server_, "jean!jean@localhost", "#history", "hello"});
+    plugin_->on_command(irccd_, {server_, "destructor!dst@localhost", "#history", "said jean"});
+
+    auto cmd = server_->cqueue().front();
+
+    BOOST_REQUIRE_EQUAL(cmd["command"].get<std::string>(), "message");
+    BOOST_REQUIRE_EQUAL(cmd["target"].get<std::string>(), "#history");
+    BOOST_REQUIRE(std::regex_match(cmd["message"].get<std::string>(), rule));
+}
+
+BOOST_AUTO_TEST_CASE(format_unknown)
+{
+    remove(CMAKE_CURRENT_BINARY_DIR "/unknown.json");
+    load({{ "file", CMAKE_CURRENT_BINARY_DIR "/unknown.json" }});
+
+    plugin_->on_message(irccd_, {server_, "jean!jean@localhost", "#history", "hello"});
+    plugin_->on_command(irccd_, {server_, "destructor!dst@localhost", "#history", "seen nobody"});
+
+    auto cmd = server_->cqueue().front();
+
+    BOOST_REQUIRE_EQUAL(cmd["command"].get<std::string>(), "message");
+    BOOST_REQUIRE_EQUAL(cmd["target"].get<std::string>(), "#history");
+    BOOST_REQUIRE_EQUAL(cmd["message"].get<std::string>(), "unknown=history:!history:test:#history:destructor!dst@localhost:destructor:nobody");
+}
+
+BOOST_AUTO_TEST_CASE(fix_642)
+{
+    const std::regex rule("said=history:!history:test:#history:destructor!dst@localhost:destructor:jean:hello:\\d{2}:\\d{2}");
+
+    remove(CMAKE_CURRENT_BINARY_DIR "/case.json");
+    load({{"file", CMAKE_CURRENT_BINARY_DIR "/case.json"}});
+
+    plugin_->on_message(irccd_, {server_, "JeaN!JeaN@localhost", "#history", "hello"});
+
+    // Full caps.
+    plugin_->on_command(irccd_, {server_, "destructor!dst@localhost", "#HISTORY", "said JEAN"});
+
+    auto cmd = server_->cqueue().front();
+
+    BOOST_REQUIRE_EQUAL(cmd["command"].get<std::string>(), "message");
+    BOOST_REQUIRE_EQUAL(cmd["target"].get<std::string>(), "#history");
+    BOOST_REQUIRE(std::regex_match(cmd["message"].get<std::string>(), rule));
+
+    // Random caps.
+    plugin_->on_command(irccd_, {server_, "destructor!dst@localhost", "#HiSToRy", "said JeaN"});
+
+    cmd = server_->cqueue().back();
+
+    BOOST_REQUIRE_EQUAL(cmd["command"].get<std::string>(), "message");
+    BOOST_REQUIRE_EQUAL(cmd["target"].get<std::string>(), "#history");
+    BOOST_REQUIRE(std::regex_match(cmd["message"].get<std::string>(), rule));
+}
+
+BOOST_AUTO_TEST_SUITE_END()
+
+} // !irccd
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/src/plugins/logger/CMakeLists.txt	Fri Dec 15 15:37:58 2017 +0100
@@ -0,0 +1,26 @@
+#
+# CMakeLists.txt -- CMake build system for irccd
+#
+# Copyright (c) 2013-2017 David Demelier <markand@malikania.fr>
+#
+# Permission to use, copy, modify, and/or distribute this software for any
+# purpose with or without fee is hereby granted, provided that the above
+# copyright notice and this permission notice appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+#
+
+irccd_define_test(
+    NAME plugin-logger
+    SOURCES main.cpp
+    LIBRARIES libirccd
+    FLAGS
+        PLUGIN_NAME="logger"
+        PLUGIN_PATH="${CMAKE_SOURCE_DIR}/plugins/logger/logger.js"
+)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/src/plugins/logger/main.cpp	Fri Dec 15 15:37:58 2017 +0100
@@ -0,0 +1,155 @@
+/*
+ * main.cpp -- test logger plugin
+ *
+ * Copyright (c) 2013-2017 David Demelier <markand@malikania.fr>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include <fstream>
+#include <iterator>
+
+#define BOOST_TEST_MODULE "Logger plugin"
+#include <boost/test/unit_test.hpp>
+
+#include <irccd/daemon/irccd.hpp>
+#include <irccd/daemon/server.hpp>
+
+#include <irccd/test/plugin_test.hpp>
+
+namespace irccd {
+
+class logger_test : public plugin_test {
+protected:
+    std::string last() const
+    {
+        std::ifstream file(CMAKE_CURRENT_BINARY_DIR "/log.txt");
+
+        return std::string(std::istreambuf_iterator<char>(file.rdbuf()), {});
+    }
+
+public:
+    logger_test()
+        : plugin_test(PLUGIN_NAME, PLUGIN_PATH)
+    {
+        remove(CMAKE_CURRENT_BINARY_DIR "/log.txt");
+
+        plugin_->set_formats({
+            { "join", "join=#{server}:#{channel}:#{origin}:#{nickname}" },
+            { "kick", "kick=#{server}:#{channel}:#{origin}:#{nickname}:#{target}:#{reason}" },
+            { "me", "me=#{server}:#{channel}:#{origin}:#{nickname}:#{message}" },
+            { "message", "message=#{server}:#{channel}:#{origin}:#{nickname}:#{message}" },
+            { "mode", "mode=#{server}:#{origin}:#{channel}:#{mode}:#{limit}:#{user}:#{mask}" },
+            { "notice", "notice=#{server}:#{origin}:#{channel}:#{message}" },
+            { "part", "part=#{server}:#{channel}:#{origin}:#{nickname}:#{reason}" },
+            { "query", "query=#{server}:#{origin}:#{nickname}:#{message}" },
+            { "topic", "topic=#{server}:#{channel}:#{origin}:#{nickname}:#{topic}" },
+        });
+    }
+
+    void load(plugin_config config = plugin_config())
+    {
+        if (config.count("path") == 0)
+            config.emplace("path", CMAKE_CURRENT_BINARY_DIR "/log.txt");
+
+        plugin_->set_config(config);
+        plugin_->on_load(irccd_);
+    }
+};
+
+BOOST_FIXTURE_TEST_SUITE(logger_test_suite, logger_test)
+
+BOOST_AUTO_TEST_CASE(format_join)
+{
+    load();
+
+    plugin_->on_join(irccd_, {server_, "jean!jean@localhost", "#staff"});
+
+    BOOST_REQUIRE_EQUAL("join=test:#staff:jean!jean@localhost:jean\n", last());
+}
+
+BOOST_AUTO_TEST_CASE(format_kick)
+{
+    load();
+
+    plugin_->on_kick(irccd_, {server_, "jean!jean@localhost", "#staff", "badboy", "please do not flood"});
+
+    BOOST_REQUIRE_EQUAL("kick=test:#staff:jean!jean@localhost:jean:badboy:please do not flood\n", last());
+}
+
+BOOST_AUTO_TEST_CASE(format_me)
+{
+    load();
+
+    plugin_->on_me(irccd_, {server_, "jean!jean@localhost", "#staff", "is drinking water"});
+
+    BOOST_REQUIRE_EQUAL("me=test:#staff:jean!jean@localhost:jean:is drinking water\n", last());
+}
+
+BOOST_AUTO_TEST_CASE(format_message)
+{
+    load();
+
+    plugin_->on_message(irccd_, {server_, "jean!jean@localhost", "#staff", "hello guys"});
+
+    BOOST_REQUIRE_EQUAL("message=test:#staff:jean!jean@localhost:jean:hello guys\n", last());
+}
+
+BOOST_AUTO_TEST_CASE(format_mode)
+{
+    load();
+
+    plugin_->on_mode(irccd_, {server_, "jean!jean@localhost", "chris", "+i", "l", "u", "m"});
+
+    BOOST_REQUIRE_EQUAL("mode=test:jean!jean@localhost:chris:+i:l:u:m\n", last());
+}
+
+BOOST_AUTO_TEST_CASE(format_notice)
+{
+    load();
+
+    plugin_->on_notice(irccd_, {server_, "jean!jean@localhost", "chris", "tu veux voir mon chat ?"});
+
+    BOOST_REQUIRE_EQUAL("notice=test:jean!jean@localhost:chris:tu veux voir mon chat ?\n", last());
+}
+
+BOOST_AUTO_TEST_CASE(format_part)
+{
+    load();
+
+    plugin_->on_part(irccd_, {server_, "jean!jean@localhost", "#staff", "too noisy here"});
+
+    BOOST_REQUIRE_EQUAL("part=test:#staff:jean!jean@localhost:jean:too noisy here\n", last());
+}
+
+BOOST_AUTO_TEST_CASE(format_topic)
+{
+    load();
+
+    plugin_->on_topic(irccd_, {server_, "jean!jean@localhost", "#staff", "oh yeah yeaaaaaaaah"});
+
+    BOOST_REQUIRE_EQUAL("topic=test:#staff:jean!jean@localhost:jean:oh yeah yeaaaaaaaah\n", last());
+}
+
+BOOST_AUTO_TEST_CASE(fix_642)
+{
+    load();
+
+    plugin_->on_message(irccd_, {server_, "jean!jean@localhost", "#STAFF", "hello guys"});
+
+    BOOST_REQUIRE_EQUAL("message=test:#staff:jean!jean@localhost:jean:hello guys\n", last());
+}
+
+BOOST_AUTO_TEST_SUITE_END()
+
+} // !irccd
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/src/plugins/plugin/CMakeLists.txt	Fri Dec 15 15:37:58 2017 +0100
@@ -0,0 +1,26 @@
+#
+# CMakeLists.txt -- CMake build system for irccd
+#
+# Copyright (c) 2013-2017 David Demelier <markand@malikania.fr>
+#
+# Permission to use, copy, modify, and/or distribute this software for any
+# purpose with or without fee is hereby granted, provided that the above
+# copyright notice and this permission notice appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+#
+
+irccd_define_test(
+    NAME plugin-plugin
+    SOURCES main.cpp
+    LIBRARIES libirccd
+    FLAGS
+        PLUGIN_NAME="plugin"
+        PLUGIN_PATH="${CMAKE_SOURCE_DIR}/plugins/plugin/plugin.js"
+)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/src/plugins/plugin/main.cpp	Fri Dec 15 15:37:58 2017 +0100
@@ -0,0 +1,127 @@
+/*
+ * main.cpp -- test plugin plugin
+ *
+ * Copyright (c) 2013-2017 David Demelier <markand@malikania.fr>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#define BOOST_TEST_MODULE "Plugin plugin"
+#include <boost/test/unit_test.hpp>
+
+#include <irccd/string_util.hpp>
+
+#include <irccd/daemon/irccd.hpp>
+#include <irccd/daemon/plugin_service.hpp>
+#include <irccd/daemon/server.hpp>
+
+#include <irccd/test/plugin_test.hpp>
+
+namespace irccd {
+
+class fake_plugin : public plugin {
+public:
+    fake_plugin()
+        : plugin("fake", "")
+    {
+        set_author("jean");
+        set_version("0.0.0.0.0.1");
+        set_license("BEER");
+        set_summary("Fake White Beer 2000");
+    }
+};
+
+class test_fixture : public plugin_test {
+public:
+    test_fixture()
+        : plugin_test(PLUGIN_NAME, PLUGIN_PATH)
+    {
+        irccd_.plugins().add(std::make_shared<fake_plugin>());
+
+        plugin_->set_formats({
+            { "usage", "usage=#{plugin}:#{command}:#{server}:#{channel}:#{origin}:#{nickname}" },
+            { "info", "info=#{plugin}:#{command}:#{server}:#{channel}:#{origin}:#{nickname}:#{author}:#{license}:#{name}:#{summary}:#{version}" },
+            { "not-found", "not-found=#{plugin}:#{command}:#{server}:#{channel}:#{origin}:#{nickname}:#{name}" },
+            { "too-long", "too-long=#{plugin}:#{command}:#{server}:#{channel}:#{origin}:#{nickname}" }
+        });
+        plugin_->on_load(irccd_);
+    }
+};
+
+BOOST_FIXTURE_TEST_SUITE(test_fixture_suite, test_fixture)
+
+BOOST_AUTO_TEST_CASE(format_usage)
+{
+    nlohmann::json cmd;
+
+    plugin_->on_command(irccd_, {server_, "jean!jean@localhost", "#staff", ""});
+    cmd = server_->cqueue().front();
+
+    BOOST_REQUIRE_EQUAL(cmd["command"].get<std::string>(), "message");
+    BOOST_REQUIRE_EQUAL(cmd["target"].get<std::string>(), "#staff");
+    BOOST_REQUIRE_EQUAL(cmd["message"].get<std::string>(), "usage=plugin:!plugin:test:#staff:jean!jean@localhost:jean");
+
+    plugin_->on_command(irccd_, {server_, "jean!jean@localhost", "#staff", "fail"});
+    cmd = server_->cqueue().front();
+
+    BOOST_REQUIRE_EQUAL(cmd["command"].get<std::string>(), "message");
+    BOOST_REQUIRE_EQUAL(cmd["target"].get<std::string>(), "#staff");
+    BOOST_REQUIRE_EQUAL(cmd["message"].get<std::string>(), "usage=plugin:!plugin:test:#staff:jean!jean@localhost:jean");
+
+    plugin_->on_command(irccd_, {server_, "jean!jean@localhost", "#staff", "info"});
+    cmd = server_->cqueue().front();
+
+    BOOST_REQUIRE_EQUAL(cmd["command"].get<std::string>(), "message");
+    BOOST_REQUIRE_EQUAL(cmd["target"].get<std::string>(), "#staff");
+    BOOST_REQUIRE_EQUAL(cmd["message"].get<std::string>(), "usage=plugin:!plugin:test:#staff:jean!jean@localhost:jean");
+}
+
+BOOST_AUTO_TEST_CASE(format_info)
+{
+    plugin_->on_command(irccd_, {server_, "jean!jean@localhost", "#staff", "info fake"});
+
+    auto cmd = server_->cqueue().front();
+
+    BOOST_REQUIRE_EQUAL(cmd["command"].get<std::string>(), "message");
+    BOOST_REQUIRE_EQUAL(cmd["target"].get<std::string>(), "#staff");
+    BOOST_REQUIRE_EQUAL(cmd["message"].get<std::string>(), "info=plugin:!plugin:test:#staff:jean!jean@localhost:jean:jean:BEER:fake:Fake White Beer 2000:0.0.0.0.0.1");
+}
+
+BOOST_AUTO_TEST_CASE(format_not_found)
+{
+    plugin_->on_command(irccd_, {server_, "jean!jean@localhost", "#staff", "info doesnotexistsihope"});
+
+    auto cmd = server_->cqueue().front();
+
+    BOOST_REQUIRE_EQUAL(cmd["command"].get<std::string>(), "message");
+    BOOST_REQUIRE_EQUAL(cmd["target"].get<std::string>(), "#staff");
+    BOOST_REQUIRE_EQUAL(cmd["message"].get<std::string>(), "not-found=plugin:!plugin:test:#staff:jean!jean@localhost:jean:doesnotexistsihope");
+}
+
+BOOST_AUTO_TEST_CASE(format_too_long)
+{
+    for (int i = 0; i < 100; ++i)
+        irccd_.plugins().add(std::make_shared<plugin>(string_util::sprintf("plugin-n-%d", i), ""));
+
+    plugin_->on_command(irccd_, {server_, "jean!jean@localhost", "#staff", "list"});
+
+    auto cmd = server_->cqueue().front();
+
+    BOOST_REQUIRE_EQUAL(cmd["command"].get<std::string>(), "message");
+    BOOST_REQUIRE_EQUAL(cmd["target"].get<std::string>(), "#staff");
+    BOOST_REQUIRE_EQUAL(cmd["message"].get<std::string>(), "too-long=plugin:!plugin:test:#staff:jean!jean@localhost:jean");
+}
+
+BOOST_AUTO_TEST_SUITE_END()
+
+} // !irccd
--- a/tests/src/rule-add-command/CMakeLists.txt	Thu Dec 14 21:51:22 2017 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,23 +0,0 @@
-#
-# CMakeLists.txt -- CMake build system for irccd
-#
-# Copyright (c) 2013-2017 David Demelier <markand@malikania.fr>
-#
-# Permission to use, copy, modify, and/or distribute this software for any
-# purpose with or without fee is hereby granted, provided that the above
-# copyright notice and this permission notice appear in all copies.
-#
-# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
-# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
-# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
-# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
-# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
-# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
-# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
-#
-
-irccd_define_test(
-    NAME rule-add-command
-    SOURCES main.cpp
-    LIBRARIES libirccd libirccdctl
-)
--- a/tests/src/rule-add-command/main.cpp	Thu Dec 14 21:51:22 2017 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,203 +0,0 @@
-/*
- * main.cpp -- test rule-add remote command
- *
- * Copyright (c) 2013-2017 David Demelier <markand@malikania.fr>
- *
- * Permission to use, copy, modify, and/or distribute this software for any
- * purpose with or without fee is hereby granted, provided that the above
- * copyright notice and this permission notice appear in all copies.
- *
- * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
- * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
- * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
- * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
- * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
- * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
- * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
- */
-
-#define BOOST_TEST_MODULE "rule-add"
-#include <boost/test/unit_test.hpp>
-
-#include <irccd/json_util.hpp>
-
-#include <irccd/daemon/command.hpp>
-#include <irccd/daemon/rule_service.hpp>
-
-#include <irccd/test/command_test.hpp>
-
-namespace irccd {
-
-namespace {
-
-using rule_add_test = command_test<rule_add_command, rule_list_command>;
-
-} // !namespace
-
-BOOST_FIXTURE_TEST_SUITE(rule_add_test_suite, rule_add_test)
-
-BOOST_AUTO_TEST_CASE(basic)
-{
-    nlohmann::json result;
-
-    ctl_->send({
-        { "command",    "rule-add"          },
-        { "servers",    { "s1", "s2" }      },
-        { "channels",   { "c1", "c2" }      },
-        { "plugins",    { "p1", "p2" }      },
-        { "events",     { "onMessage" }     },
-        { "action",     "accept"            },
-        { "index",      0                   }
-    });
-    ctl_->recv([&] (auto, auto msg) {
-        result = msg;
-    });
-
-    wait_for([&] () {
-        return result.is_object();
-    });
-
-    BOOST_TEST(result.is_object());
-
-    result = nullptr;
-    ctl_->send({{"command", "rule-list"}});
-    ctl_->recv([&] (auto, auto msg) {
-        result = msg;
-    });
-
-    wait_for([&] () {
-        return result.is_object();
-    });
-
-    BOOST_TEST(result.is_object());
-
-    auto servers = result["list"][0]["servers"];
-    auto channels = result["list"][0]["channels"];
-    auto plugins = result["list"][0]["plugins"];
-    auto events = result["list"][0]["events"];
-
-    BOOST_TEST(json_util::contains(servers, "s1"));
-    BOOST_TEST(json_util::contains(servers, "s2"));
-    BOOST_TEST(json_util::contains(channels, "c1"));
-    BOOST_TEST(json_util::contains(channels, "c2"));
-    BOOST_TEST(json_util::contains(plugins, "p1"));
-    BOOST_TEST(json_util::contains(plugins, "p2"));
-    BOOST_TEST(json_util::contains(events, "onMessage"));
-    BOOST_TEST(result["list"][0]["action"].get<std::string>() == "accept");
-}
-
-BOOST_AUTO_TEST_CASE(append)
-{
-    nlohmann::json result;
-
-    ctl_->send({
-        { "command",    "rule-add"          },
-        { "servers",    { "s1" }            },
-        { "channels",   { "c1" }            },
-        { "plugins",    { "p1" }            },
-        { "events",     { "onMessage" }     },
-        { "action",     "accept"            },
-        { "index",      0                   }
-    });
-    ctl_->recv([&] (auto, auto msg) {
-        result = msg;
-    });
-
-    wait_for([&] () {
-        return result.is_object();
-    });
-
-    BOOST_TEST(result.is_object());
-
-    result = nullptr;
-    ctl_->send({
-        { "command",    "rule-add"          },
-        { "servers",    { "s2" }            },
-        { "channels",   { "c2" }            },
-        { "plugins",    { "p2" }            },
-        { "events",     { "onMessage" }     },
-        { "action",     "drop"              },
-        { "index",      1                   }
-    });
-    ctl_->recv([&] (auto, auto msg) {
-        result = msg;
-    });
-
-    wait_for([&] () {
-        return result.is_object();
-    });
-
-    BOOST_TEST(result.is_object());
-
-    result = nullptr;
-    ctl_->send({{"command", "rule-list"}});
-    ctl_->recv([&] (auto, auto msg) {
-        result = msg;
-    });
-
-    wait_for([&] () {
-        return result.is_object();
-    });
-
-    BOOST_TEST(result.is_object());
-    BOOST_TEST(result["list"].size() == 2U);
-
-    // Rule 0.
-    {
-        auto servers = result["list"][0]["servers"];
-        auto channels = result["list"][0]["channels"];
-        auto plugins = result["list"][0]["plugins"];
-        auto events = result["list"][0]["events"];
-
-        BOOST_TEST(json_util::contains(servers, "s1"));
-        BOOST_TEST(json_util::contains(channels, "c1"));
-        BOOST_TEST(json_util::contains(plugins, "p1"));
-        BOOST_TEST(json_util::contains(events, "onMessage"));
-        BOOST_TEST(result["list"][0]["action"].get<std::string>() == "accept");
-    }
-
-    // Rule 1.
-    {
-        auto servers = result["list"][1]["servers"];
-        auto channels = result["list"][1]["channels"];
-        auto plugins = result["list"][1]["plugins"];
-        auto events = result["list"][1]["events"];
-
-        BOOST_TEST(json_util::contains(servers, "s2"));
-        BOOST_TEST(json_util::contains(channels, "c2"));
-        BOOST_TEST(json_util::contains(plugins, "p2"));
-        BOOST_TEST(json_util::contains(events, "onMessage"));
-        BOOST_TEST(result["list"][1]["action"].get<std::string>() == "drop");
-    }
-}
-
-BOOST_AUTO_TEST_SUITE(errors)
-
-BOOST_AUTO_TEST_CASE(invalid_action)
-{
-    boost::system::error_code result;
-    nlohmann::json message;
-
-    ctl_->send({
-        { "command",    "rule-add"  },
-        { "action",     "unknown"   }
-    });
-    ctl_->recv([&] (auto rresult, auto rmessage) {
-        result = rresult;
-        message = rmessage;
-    });
-
-    wait_for([&] {
-        return result;
-    });
-
-    BOOST_TEST(result == rule_error::invalid_action);
-    BOOST_TEST(message["error"].template get<int>() == rule_error::invalid_action);
-    BOOST_TEST(message["errorCategory"].template get<std::string>() == "rule");
-}
-
-BOOST_AUTO_TEST_SUITE_END()
-
-BOOST_AUTO_TEST_SUITE_END()
-
-} // !irccd
--- a/tests/src/rule-edit-command/CMakeLists.txt	Thu Dec 14 21:51:22 2017 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,23 +0,0 @@
-#
-# CMakeLists.txt -- CMake build system for irccd
-#
-# Copyright (c) 2013-2017 David Demelier <markand@malikania.fr>
-#
-# Permission to use, copy, modify, and/or distribute this software for any
-# purpose with or without fee is hereby granted, provided that the above
-# copyright notice and this permission notice appear in all copies.
-#
-# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
-# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
-# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
-# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
-# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
-# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
-# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
-#
-
-irccd_define_test(
-    NAME rule-edit-command
-    SOURCES main.cpp
-    LIBRARIES libirccd libirccdctl
-)
--- a/tests/src/rule-edit-command/main.cpp	Thu Dec 14 21:51:22 2017 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,646 +0,0 @@
-/*
- * main.cpp -- test rule-edit remote command
- *
- * Copyright (c) 2013-2017 David Demelier <markand@malikania.fr>
- *
- * Permission to use, copy, modify, and/or distribute this software for any
- * purpose with or without fee is hereby granted, provided that the above
- * copyright notice and this permission notice appear in all copies.
- *
- * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
- * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
- * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
- * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
- * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
- * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
- * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
- */
-
-#define BOOST_TEST_MODULE "rule-edit"
-#include <boost/test/unit_test.hpp>
-
-#include <irccd/json_util.hpp>
-
-#include <irccd/daemon/command.hpp>
-#include <irccd/daemon/rule_service.hpp>
-
-#include <irccd/test/command_test.hpp>
-
-namespace irccd {
-
-namespace {
-
-class rule_edit_test : public command_test<rule_edit_command, rule_info_command> {
-public:
-    rule_edit_test()
-    {
-        daemon_->rules().add(rule(
-            { "s1", "s2" },
-            { "c1", "c2" },
-            { "o1", "o2" },
-            { "p1", "p2" },
-            { "onMessage", "onCommand" },
-            rule::action_type::drop
-        ));
-    }
-};
-
-} // !namespace
-
-BOOST_FIXTURE_TEST_SUITE(rule_edit_test_suite, rule_edit_test)
-
-BOOST_AUTO_TEST_CASE(add_server)
-{
-    nlohmann::json result;
-
-    ctl_->send({
-        { "command",        "rule-edit"     },
-        { "add-servers",    { "new-s3" }    },
-        { "index",          0               }
-    });
-    ctl_->recv([&] (auto, auto msg) {
-        result = msg;
-    });
-
-    wait_for([&] () {
-        return result.is_object();
-    });
-
-    BOOST_TEST(result.is_object());
-
-    result = nullptr;
-    ctl_->send({
-        { "command",        "rule-info"     },
-        { "index",          0               }
-    });
-    ctl_->recv([&] (auto, auto msg) {
-        result = msg;
-    });
-
-    wait_for([&] () {
-        return result.is_object();
-    });
-
-    BOOST_TEST(result.is_object());
-    BOOST_TEST(json_util::contains(result["servers"], "s1"));
-    BOOST_TEST(json_util::contains(result["servers"], "s2"));
-    BOOST_TEST(json_util::contains(result["servers"], "new-s3"));
-    BOOST_TEST(json_util::contains(result["channels"], "c1"));
-    BOOST_TEST(json_util::contains(result["channels"], "c2"));
-    BOOST_TEST(json_util::contains(result["plugins"], "p1"));
-    BOOST_TEST(json_util::contains(result["plugins"], "p2"));
-    BOOST_TEST(json_util::contains(result["events"], "onMessage"));
-    BOOST_TEST(json_util::contains(result["events"], "onCommand"));
-    BOOST_TEST(result["action"].get<std::string>() == "drop");
-}
-
-BOOST_AUTO_TEST_CASE(add_channel)
-{
-    nlohmann::json result;
-
-    ctl_->send({
-        { "command",        "rule-edit"     },
-        { "add-channels",   { "new-c3" }    },
-        { "index",          0               }
-    });
-    ctl_->recv([&] (auto, auto msg) {
-        result = msg;
-    });
-
-    wait_for([&] () {
-        return result.is_object();
-    });
-
-    BOOST_TEST(result.is_object());
-
-    result = nullptr;
-    ctl_->send({
-        { "command",        "rule-info"     },
-        { "index",          0               }
-    });
-    ctl_->recv([&] (auto, auto msg) {
-        result = msg;
-    });
-
-    wait_for([&] () {
-        return result.is_object();
-    });
-
-    BOOST_TEST(result.is_object());
-    BOOST_TEST(json_util::contains(result["servers"], "s1"));
-    BOOST_TEST(json_util::contains(result["servers"], "s2"));
-    BOOST_TEST(json_util::contains(result["channels"], "c1"));
-    BOOST_TEST(json_util::contains(result["channels"], "c2"));
-    BOOST_TEST(json_util::contains(result["channels"], "new-c3"));
-    BOOST_TEST(json_util::contains(result["plugins"], "p1"));
-    BOOST_TEST(json_util::contains(result["plugins"], "p2"));
-    BOOST_TEST(json_util::contains(result["events"], "onMessage"));
-    BOOST_TEST(json_util::contains(result["events"], "onCommand"));
-    BOOST_TEST(result["action"].get<std::string>() == "drop");
-}
-
-BOOST_AUTO_TEST_CASE(add_plugin)
-{
-    nlohmann::json result;
-
-    ctl_->send({
-        { "command",        "rule-edit"     },
-        { "add-plugins",    { "new-p3" }    },
-        { "index",          0               }
-    });
-    ctl_->recv([&] (auto, auto msg) {
-        result = msg;
-    });
-
-    wait_for([&] () {
-        return result.is_object();
-    });
-
-    BOOST_TEST(result.is_object());
-
-    result = nullptr;
-    ctl_->send({
-        { "command",        "rule-info"     },
-        { "index",          0               }
-    });
-    ctl_->recv([&] (auto, auto msg) {
-        result = msg;
-    });
-
-    wait_for([&] () {
-        return result.is_object();
-    });
-
-    BOOST_TEST(result.is_object());
-    BOOST_TEST(json_util::contains(result["servers"], "s1"));
-    BOOST_TEST(json_util::contains(result["servers"], "s2"));
-    BOOST_TEST(json_util::contains(result["channels"], "c1"));
-    BOOST_TEST(json_util::contains(result["channels"], "c2"));
-    BOOST_TEST(json_util::contains(result["plugins"], "p1"));
-    BOOST_TEST(json_util::contains(result["plugins"], "p2"));
-    BOOST_TEST(json_util::contains(result["plugins"], "new-p3"));
-    BOOST_TEST(json_util::contains(result["events"], "onMessage"));
-    BOOST_TEST(json_util::contains(result["events"], "onCommand"));
-    BOOST_TEST(result["action"].get<std::string>() == "drop");
-}
-
-BOOST_AUTO_TEST_CASE(add_event)
-{
-    nlohmann::json result;
-
-    ctl_->send({
-        { "command",        "rule-edit"     },
-        { "add-events",     { "onQuery" }   },
-        { "index",          0               }
-    });
-    ctl_->recv([&] (auto, auto msg) {
-        result = msg;
-    });
-
-    wait_for([&] () {
-        return result.is_object();
-    });
-
-    BOOST_TEST(result.is_object());
-
-    result = nullptr;
-    ctl_->send({
-        { "command",        "rule-info"     },
-        { "index",          0               }
-    });
-    ctl_->recv([&] (auto, auto msg) {
-        result = msg;
-    });
-
-    wait_for([&] () {
-        return result.is_object();
-    });
-
-    BOOST_TEST(result.is_object());
-    BOOST_TEST(json_util::contains(result["servers"], "s1"));
-    BOOST_TEST(json_util::contains(result["servers"], "s2"));
-    BOOST_TEST(json_util::contains(result["channels"], "c1"));
-    BOOST_TEST(json_util::contains(result["channels"], "c2"));
-    BOOST_TEST(json_util::contains(result["plugins"], "p1"));
-    BOOST_TEST(json_util::contains(result["plugins"], "p2"));
-    BOOST_TEST(json_util::contains(result["events"], "onMessage"));
-    BOOST_TEST(json_util::contains(result["events"], "onCommand"));
-    BOOST_TEST(json_util::contains(result["events"], "onQuery"));
-    BOOST_TEST(result["action"].get<std::string>() == "drop");
-}
-
-BOOST_AUTO_TEST_CASE(add_event_and_server)
-{
-    nlohmann::json result;
-
-    ctl_->send({
-        { "command",        "rule-edit"     },
-        { "add-servers",    { "new-s3" }    },
-        { "add-events",     { "onQuery" }   },
-        { "index",          0               }
-    });
-    ctl_->recv([&] (auto, auto msg) {
-        result = msg;
-    });
-
-    wait_for([&] () {
-        return result.is_object();
-    });
-
-    BOOST_TEST(result.is_object());
-
-    result = nullptr;
-    ctl_->send({
-        { "command",        "rule-info"     },
-        { "index",          0               }
-    });
-    ctl_->recv([&] (auto, auto msg) {
-        result = msg;
-    });
-
-    wait_for([&] () {
-        return result.is_object();
-    });
-
-    BOOST_TEST(result.is_object());
-    BOOST_TEST(json_util::contains(result["servers"], "s1"));
-    BOOST_TEST(json_util::contains(result["servers"], "s2"));
-    BOOST_TEST(json_util::contains(result["servers"], "new-s3"));
-    BOOST_TEST(json_util::contains(result["channels"], "c1"));
-    BOOST_TEST(json_util::contains(result["channels"], "c2"));
-    BOOST_TEST(json_util::contains(result["plugins"], "p1"));
-    BOOST_TEST(json_util::contains(result["plugins"], "p2"));
-    BOOST_TEST(json_util::contains(result["events"], "onMessage"));
-    BOOST_TEST(json_util::contains(result["events"], "onCommand"));
-    BOOST_TEST(json_util::contains(result["events"], "onQuery"));
-    BOOST_TEST(result["action"].get<std::string>() == "drop");
-}
-
-BOOST_AUTO_TEST_CASE(change_action)
-{
-    nlohmann::json result;
-
-    ctl_->send({
-        { "command",        "rule-edit"     },
-        { "action",         "accept"        },
-        { "index",          0               }
-    });
-    ctl_->recv([&] (auto, auto msg) {
-        result = msg;
-    });
-
-    wait_for([&] () {
-        return result.is_object();
-    });
-
-    BOOST_TEST(result.is_object());
-
-    result = nullptr;
-    ctl_->send({
-        { "command",        "rule-info"     },
-        { "index",          0               }
-    });
-    ctl_->recv([&] (auto, auto msg) {
-        result = msg;
-    });
-
-    wait_for([&] () {
-        return result.is_object();
-    });
-
-    BOOST_TEST(result.is_object());
-    BOOST_TEST(json_util::contains(result["servers"], "s1"));
-    BOOST_TEST(json_util::contains(result["servers"], "s2"));
-    BOOST_TEST(json_util::contains(result["channels"], "c1"));
-    BOOST_TEST(json_util::contains(result["channels"], "c2"));
-    BOOST_TEST(json_util::contains(result["plugins"], "p1"));
-    BOOST_TEST(json_util::contains(result["plugins"], "p2"));
-    BOOST_TEST(json_util::contains(result["events"], "onMessage"));
-    BOOST_TEST(json_util::contains(result["events"], "onCommand"));
-    BOOST_TEST(result["action"].get<std::string>() == "accept");
-}
-
-BOOST_AUTO_TEST_CASE(remove_server)
-{
-    nlohmann::json result;
-
-    ctl_->send({
-        { "command",        "rule-edit"     },
-        { "remove-servers", { "s2" }        },
-        { "index",          0               }
-    });
-    ctl_->recv([&] (auto, auto msg) {
-        result = msg;
-    });
-
-    wait_for([&] () {
-        return result.is_object();
-    });
-
-    BOOST_TEST(result.is_object());
-
-    result = nullptr;
-    ctl_->send({
-        { "command",        "rule-info"     },
-        { "index",          0               }
-    });
-    ctl_->recv([&] (auto, auto msg) {
-        result = msg;
-    });
-
-    wait_for([&] () {
-        return result.is_object();
-    });
-
-    BOOST_TEST(result.is_object());
-    BOOST_TEST(json_util::contains(result["servers"], "s1"));
-    BOOST_TEST(!json_util::contains(result["servers"], "s2"));
-    BOOST_TEST(json_util::contains(result["channels"], "c1"));
-    BOOST_TEST(json_util::contains(result["channels"], "c2"));
-    BOOST_TEST(json_util::contains(result["plugins"], "p1"));
-    BOOST_TEST(json_util::contains(result["plugins"], "p2"));
-    BOOST_TEST(json_util::contains(result["events"], "onMessage"));
-    BOOST_TEST(json_util::contains(result["events"], "onCommand"));
-    BOOST_TEST(result["action"].get<std::string>() == "drop");
-}
-
-BOOST_AUTO_TEST_CASE(remove_channel)
-{
-    nlohmann::json result;
-
-    ctl_->send({
-        { "command",        "rule-edit"     },
-        { "remove-channels", { "c2" }       },
-        { "index",          0               }
-    });
-    ctl_->recv([&] (auto, auto msg) {
-        result = msg;
-    });
-
-    wait_for([&] () {
-        return result.is_object();
-    });
-
-    BOOST_TEST(result.is_object());
-
-    result = nullptr;
-    ctl_->send({
-        { "command",        "rule-info"     },
-        { "index",          0               }
-    });
-    ctl_->recv([&] (auto, auto msg) {
-        result = msg;
-    });
-
-    wait_for([&] () {
-        return result.is_object();
-    });
-
-    BOOST_TEST(result.is_object());
-    BOOST_TEST(json_util::contains(result["servers"], "s1"));
-    BOOST_TEST(json_util::contains(result["servers"], "s2"));
-    BOOST_TEST(json_util::contains(result["channels"], "c1"));
-    BOOST_TEST(!json_util::contains(result["channels"], "c2"));
-    BOOST_TEST(json_util::contains(result["plugins"], "p1"));
-    BOOST_TEST(json_util::contains(result["plugins"], "p2"));
-    BOOST_TEST(json_util::contains(result["events"], "onMessage"));
-    BOOST_TEST(json_util::contains(result["events"], "onCommand"));
-    BOOST_TEST(result["action"].get<std::string>() == "drop");
-}
-
-BOOST_AUTO_TEST_CASE(remove_plugin)
-{
-    nlohmann::json result;
-
-    ctl_->send({
-        { "command",        "rule-edit"     },
-        { "remove-plugins", { "p2" }        },
-        { "index",          0               }
-    });
-    ctl_->recv([&] (auto, auto msg) {
-        result = msg;
-    });
-
-    wait_for([&] () {
-        return result.is_object();
-    });
-
-    BOOST_TEST(result.is_object());
-
-    result = nullptr;
-    ctl_->send({
-        { "command",        "rule-info"     },
-        { "index",          0               }
-    });
-    ctl_->recv([&] (auto, auto msg) {
-        result = msg;
-    });
-
-    wait_for([&] () {
-        return result.is_object();
-    });
-
-    BOOST_TEST(result.is_object());
-    BOOST_TEST(json_util::contains(result["servers"], "s1"));
-    BOOST_TEST(json_util::contains(result["servers"], "s2"));
-    BOOST_TEST(json_util::contains(result["channels"], "c1"));
-    BOOST_TEST(json_util::contains(result["channels"], "c2"));
-    BOOST_TEST(json_util::contains(result["plugins"], "p1"));
-    BOOST_TEST(!json_util::contains(result["plugins"], "p2"));
-    BOOST_TEST(json_util::contains(result["events"], "onMessage"));
-    BOOST_TEST(json_util::contains(result["events"], "onCommand"));
-    BOOST_TEST(result["action"].get<std::string>() == "drop");
-}
-
-BOOST_AUTO_TEST_CASE(remove_event)
-{
-    nlohmann::json result;
-
-    ctl_->send({
-        { "command",        "rule-edit"     },
-        { "remove-events",  { "onCommand" } },
-        { "index",          0               }
-    });
-    ctl_->recv([&] (auto, auto msg) {
-        result = msg;
-    });
-
-    wait_for([&] () {
-        return result.is_object();
-    });
-
-    BOOST_TEST(result.is_object());
-
-    result = nullptr;
-    ctl_->send({
-        { "command",        "rule-info"     },
-        { "index",          0               }
-    });
-    ctl_->recv([&] (auto, auto msg) {
-        result = msg;
-    });
-
-    wait_for([&] () {
-        return result.is_object();
-    });
-
-    BOOST_TEST(result.is_object());
-    BOOST_TEST(json_util::contains(result["servers"], "s1"));
-    BOOST_TEST(json_util::contains(result["servers"], "s2"));
-    BOOST_TEST(json_util::contains(result["channels"], "c1"));
-    BOOST_TEST(json_util::contains(result["channels"], "c2"));
-    BOOST_TEST(json_util::contains(result["plugins"], "p1"));
-    BOOST_TEST(json_util::contains(result["plugins"], "p2"));
-    BOOST_TEST(json_util::contains(result["events"], "onMessage"));
-    BOOST_TEST(!json_util::contains(result["events"], "onCommand"));
-    BOOST_TEST(result["action"].get<std::string>() == "drop");
-}
-
-BOOST_AUTO_TEST_CASE(remove_event_and_server)
-{
-    nlohmann::json result;
-
-    ctl_->send({
-        { "command",        "rule-edit"     },
-        { "remove-servers", { "s2" }        },
-        { "remove-events",  { "onCommand" } },
-        { "index",          0               }
-    });
-    ctl_->recv([&] (auto, auto msg) {
-        result = msg;
-    });
-
-    wait_for([&] () {
-        return result.is_object();
-    });
-
-    BOOST_TEST(result.is_object());
-
-    result = nullptr;
-    ctl_->send({
-        { "command",        "rule-info"     },
-        { "index",          0               }
-    });
-    ctl_->recv([&] (auto, auto msg) {
-        result = msg;
-    });
-
-    wait_for([&] () {
-        return result.is_object();
-    });
-
-    BOOST_TEST(result.is_object());
-    BOOST_TEST(json_util::contains(result["servers"], "s1"));
-    BOOST_TEST(!json_util::contains(result["servers"], "s2"));
-    BOOST_TEST(json_util::contains(result["channels"], "c1"));
-    BOOST_TEST(json_util::contains(result["channels"], "c2"));
-    BOOST_TEST(json_util::contains(result["plugins"], "p1"));
-    BOOST_TEST(json_util::contains(result["plugins"], "p2"));
-    BOOST_TEST(json_util::contains(result["events"], "onMessage"));
-    BOOST_TEST(!json_util::contains(result["events"], "onCommand"));
-    BOOST_TEST(result["action"].get<std::string>() == "drop");
-}
-
-BOOST_AUTO_TEST_SUITE(errors)
-
-BOOST_AUTO_TEST_CASE(invalid_index_1)
-{
-    boost::system::error_code result;
-    nlohmann::json message;
-
-    ctl_->send({
-        { "command",    "rule-edit" },
-        { "index",      -100        },
-        { "action",     "drop"      }
-    });
-    ctl_->recv([&] (auto rresult, auto rmessage) {
-        result = rresult;
-        message = rmessage;
-    });
-
-    wait_for([&] {
-        return result;
-    });
-
-    BOOST_TEST(result == rule_error::invalid_index);
-    BOOST_TEST(message["error"].template get<int>() == rule_error::invalid_index);
-    BOOST_TEST(message["errorCategory"].template get<std::string>() == "rule");
-}
-
-BOOST_AUTO_TEST_CASE(invalid_index_2)
-{
-    boost::system::error_code result;
-    nlohmann::json message;
-
-    ctl_->send({
-        { "command",    "rule-edit" },
-        { "index",      100         },
-        { "action",     "drop"      }
-    });
-    ctl_->recv([&] (auto rresult, auto rmessage) {
-        result = rresult;
-        message = rmessage;
-    });
-
-    wait_for([&] {
-        return result;
-    });
-
-    BOOST_TEST(result == rule_error::invalid_index);
-    BOOST_TEST(message["error"].template get<int>() == rule_error::invalid_index);
-    BOOST_TEST(message["errorCategory"].template get<std::string>() == "rule");
-}
-
-BOOST_AUTO_TEST_CASE(invalid_index_3)
-{
-    boost::system::error_code result;
-    nlohmann::json message;
-
-    ctl_->send({
-        { "command",    "rule-edit" },
-        { "index",      "notaint"   },
-        { "action",     "drop"      }
-    });
-    ctl_->recv([&] (auto rresult, auto rmessage) {
-        result = rresult;
-        message = rmessage;
-    });
-
-    wait_for([&] {
-        return result;
-    });
-
-    BOOST_TEST(result == rule_error::invalid_index);
-    BOOST_TEST(message["error"].template get<int>() == rule_error::invalid_index);
-    BOOST_TEST(message["errorCategory"].template get<std::string>() == "rule");
-}
-
-BOOST_AUTO_TEST_CASE(invalid_action)
-{
-    boost::system::error_code result;
-    nlohmann::json message;
-
-    ctl_->send({
-        { "command",    "rule-edit" },
-        { "index",      0           },
-        { "action",     "unknown"   }
-    });
-    ctl_->recv([&] (auto rresult, auto rmessage) {
-        result = rresult;
-        message = rmessage;
-    });
-
-    wait_for([&] {
-        return result;
-    });
-
-    BOOST_TEST(result == rule_error::invalid_action);
-    BOOST_TEST(message["error"].template get<int>() == rule_error::invalid_action);
-    BOOST_TEST(message["errorCategory"].template get<std::string>() == "rule");
-}
-
-BOOST_AUTO_TEST_SUITE_END()
-
-BOOST_AUTO_TEST_SUITE_END()
-
-} // !irccd
--- a/tests/src/rule-info-command/CMakeLists.txt	Thu Dec 14 21:51:22 2017 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,23 +0,0 @@
-#
-# CMakeLists.txt -- CMake build system for irccd
-#
-# Copyright (c) 2013-2017 David Demelier <markand@malikania.fr>
-#
-# Permission to use, copy, modify, and/or distribute this software for any
-# purpose with or without fee is hereby granted, provided that the above
-# copyright notice and this permission notice appear in all copies.
-#
-# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
-# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
-# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
-# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
-# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
-# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
-# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
-#
-
-irccd_define_test(
-    NAME rule-info-command
-    SOURCES main.cpp
-    LIBRARIES libirccd libirccdctl
-)
--- a/tests/src/rule-info-command/main.cpp	Thu Dec 14 21:51:22 2017 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,169 +0,0 @@
-/*
- * main.cpp -- test rule-info remote command
- *
- * Copyright (c) 2013-2017 David Demelier <markand@malikania.fr>
- *
- * Permission to use, copy, modify, and/or distribute this software for any
- * purpose with or without fee is hereby granted, provided that the above
- * copyright notice and this permission notice appear in all copies.
- *
- * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
- * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
- * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
- * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
- * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
- * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
- * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
- */
-
-#define BOOST_TEST_MODULE "rule-info"
-#include <boost/test/unit_test.hpp>
-
-#include <irccd/json_util.hpp>
-
-#include <irccd/daemon/command.hpp>
-#include <irccd/daemon/rule_service.hpp>
-
-#include <irccd/test/command_test.hpp>
-
-namespace irccd {
-
-namespace {
-
-class rule_info_test : public command_test<rule_info_command> {
-public:
-    rule_info_test()
-    {
-        daemon_->rules().add(rule(
-            { "s1", "s2" },
-            { "c1", "c2" },
-            { "o1", "o2" },
-            { "p1", "p2" },
-            { "onMessage", "onCommand" },
-            rule::action_type::drop
-        ));
-        daemon_->rules().add(rule(
-            { "s1", },
-            { "c1", },
-            { "o1", },
-            { "p1", },
-            { "onMessage", },
-            rule::action_type::accept
-        ));
-    }
-};
-
-} // !namespace
-
-BOOST_FIXTURE_TEST_SUITE(rule_info_test_suite, rule_info_test)
-
-BOOST_AUTO_TEST_CASE(basic)
-{
-    nlohmann::json result;
-
-    ctl_->send({
-        { "command",    "rule-info" },
-        { "index",      0           }
-    });
-    ctl_->recv([&] (auto, auto msg) {
-        result = msg;
-    });
-
-    wait_for([&] () {
-        return result.is_object();
-    });
-
-    BOOST_TEST(result.is_object());
-
-    auto servers = result["servers"];
-    auto channels = result["channels"];
-    auto plugins = result["plugins"];
-    auto events = result["events"];
-
-    BOOST_TEST(json_util::contains(servers, "s1"));
-    BOOST_TEST(json_util::contains(servers, "s2"));
-    BOOST_TEST(json_util::contains(channels, "c1"));
-    BOOST_TEST(json_util::contains(channels, "c2"));
-    BOOST_TEST(json_util::contains(plugins, "p1"));
-    BOOST_TEST(json_util::contains(plugins, "p2"));
-    BOOST_TEST(json_util::contains(events, "onMessage"));
-    BOOST_TEST(json_util::contains(events, "onCommand"));
-    BOOST_TEST(result["action"].get<std::string>() == "drop");
-}
-
-BOOST_AUTO_TEST_SUITE(errors)
-
-BOOST_AUTO_TEST_CASE(invalid_index_1)
-{
-    boost::system::error_code result;
-    nlohmann::json message;
-
-    ctl_->send({
-        { "command",    "rule-info" },
-        { "index",      -100        }
-    });
-    ctl_->recv([&] (auto rresult, auto rmessage) {
-        result = rresult;
-        message = rmessage;
-    });
-
-    wait_for([&] {
-        return result;
-    });
-
-    BOOST_TEST(result == rule_error::invalid_index);
-    BOOST_TEST(message["error"].template get<int>() == rule_error::invalid_index);
-    BOOST_TEST(message["errorCategory"].template get<std::string>() == "rule");
-}
-
-BOOST_AUTO_TEST_CASE(invalid_index_2)
-{
-    boost::system::error_code result;
-    nlohmann::json message;
-
-    ctl_->send({
-        { "command",    "rule-info" },
-        { "index",      100         }
-    });
-    ctl_->recv([&] (auto rresult, auto rmessage) {
-        result = rresult;
-        message = rmessage;
-    });
-
-    wait_for([&] {
-        return result;
-    });
-
-    BOOST_TEST(result == rule_error::invalid_index);
-    BOOST_TEST(message["error"].template get<int>() == rule_error::invalid_index);
-    BOOST_TEST(message["errorCategory"].template get<std::string>() == "rule");
-}
-
-BOOST_AUTO_TEST_CASE(invalid_index_3)
-{
-    boost::system::error_code result;
-    nlohmann::json message;
-
-    ctl_->send({
-        { "command",    "rule-info" },
-        { "index",      "notaint"   }
-    });
-    ctl_->recv([&] (auto rresult, auto rmessage) {
-        result = rresult;
-        message = rmessage;
-    });
-
-    wait_for([&] {
-        return result;
-    });
-
-    BOOST_TEST(result == rule_error::invalid_index);
-    BOOST_TEST(message["error"].template get<int>() == rule_error::invalid_index);
-    BOOST_TEST(message["errorCategory"].template get<std::string>() == "rule");
-}
-
-BOOST_AUTO_TEST_SUITE_END()
-
-BOOST_AUTO_TEST_SUITE_END()
-
-} // !irccd
--- a/tests/src/rule-list-command/CMakeLists.txt	Thu Dec 14 21:51:22 2017 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,23 +0,0 @@
-#
-# CMakeLists.txt -- CMake build system for irccd
-#
-# Copyright (c) 2013-2017 David Demelier <markand@malikania.fr>
-#
-# Permission to use, copy, modify, and/or distribute this software for any
-# purpose with or without fee is hereby granted, provided that the above
-# copyright notice and this permission notice appear in all copies.
-#
-# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
-# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
-# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
-# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
-# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
-# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
-# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
-#
-
-irccd_define_test(
-    NAME rule-list-command
-    SOURCES main.cpp
-    LIBRARIES libirccd libirccdctl
-)
--- a/tests/src/rule-list-command/main.cpp	Thu Dec 14 21:51:22 2017 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,131 +0,0 @@
-/*
- * main.cpp -- test rule-list remote command
- *
- * Copyright (c) 2013-2017 David Demelier <markand@malikania.fr>
- *
- * Permission to use, copy, modify, and/or distribute this software for any
- * purpose with or without fee is hereby granted, provided that the above
- * copyright notice and this permission notice appear in all copies.
- *
- * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
- * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
- * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
- * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
- * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
- * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
- * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
- */
-
-#define BOOST_TEST_MODULE "rule-list"
-#include <boost/test/unit_test.hpp>
-
-#include <irccd/json_util.hpp>
-
-#include <irccd/daemon/command.hpp>
-#include <irccd/daemon/rule_service.hpp>
-
-#include <irccd/test/command_test.hpp>
-
-namespace irccd {
-
-namespace {
-
-class rule_list_test : public command_test<rule_list_command> {
-public:
-    rule_list_test()
-    {
-        daemon_->rules().add(rule(
-            { "s1", "s2" },
-            { "c1", "c2" },
-            { "o1", "o2" },
-            { "p1", "p2" },
-            { "onMessage", "onCommand" },
-            rule::action_type::drop
-        ));
-        daemon_->rules().add(rule(
-            { "s1", },
-            { "c1", },
-            { "o1", },
-            { "p1", },
-            { "onMessage", },
-            rule::action_type::accept
-        ));
-    }
-};
-
-} // !namespace
-
-BOOST_FIXTURE_TEST_SUITE(rule_list_test_suite, rule_list_test)
-
-BOOST_AUTO_TEST_CASE(basic)
-{
-    nlohmann::json result;
-
-    ctl_->send({{"command", "rule-list"}});
-    ctl_->recv([&] (auto, auto msg) {
-        result = msg;
-    });
-    wait_for([&] () {
-        return result.is_object();
-    });
-
-    BOOST_TEST(result.is_object());
-    BOOST_TEST(result["list"].is_array());
-    BOOST_TEST(result["list"].size() == 2U);
-
-    // Rule 0.
-    {
-        auto servers = result["list"][0]["servers"];
-        auto channels = result["list"][0]["channels"];
-        auto plugins = result["list"][0]["plugins"];
-        auto events = result["list"][0]["events"];
-
-        BOOST_TEST(json_util::contains(servers, "s1"));
-        BOOST_TEST(json_util::contains(servers, "s2"));
-        BOOST_TEST(json_util::contains(channels, "c1"));
-        BOOST_TEST(json_util::contains(channels, "c2"));
-        BOOST_TEST(json_util::contains(plugins, "p1"));
-        BOOST_TEST(json_util::contains(plugins, "p2"));
-        BOOST_TEST(json_util::contains(events, "onMessage"));
-        BOOST_TEST(json_util::contains(events, "onCommand"));
-        BOOST_TEST(result["list"][0]["action"].get<std::string>() == "drop");
-    }
-
-    // Rule 1.
-    {
-        auto servers = result["list"][1]["servers"];
-        auto channels = result["list"][1]["channels"];
-        auto plugins = result["list"][1]["plugins"];
-        auto events = result["list"][1]["events"];
-
-        BOOST_TEST(json_util::contains(servers, "s1"));
-        BOOST_TEST(json_util::contains(channels, "c1"));
-        BOOST_TEST(json_util::contains(plugins, "p1"));
-        BOOST_TEST(json_util::contains(events, "onMessage"));
-        BOOST_TEST(result["list"][1]["action"].get<std::string>() == "accept");
-    }
-}
-
-BOOST_AUTO_TEST_CASE(empty)
-{
-    nlohmann::json result;
-
-    daemon_->rules().remove(0);
-    daemon_->rules().remove(0);
-
-    ctl_->send({{"command", "rule-list"}});
-    ctl_->recv([&] (auto, auto msg) {
-        result = msg;
-    });
-    wait_for([&] () {
-        return result.is_object();
-    });
-
-    BOOST_TEST(result.is_object());
-    BOOST_TEST(result["list"].is_array());
-    BOOST_TEST(result["list"].empty());
-}
-
-BOOST_AUTO_TEST_SUITE_END()
-
-} // !irccd
--- a/tests/src/rule-move-command/CMakeLists.txt	Thu Dec 14 21:51:22 2017 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,23 +0,0 @@
-#
-# CMakeLists.txt -- CMake build system for irccd
-#
-# Copyright (c) 2013-2017 David Demelier <markand@malikania.fr>
-#
-# Permission to use, copy, modify, and/or distribute this software for any
-# purpose with or without fee is hereby granted, provided that the above
-# copyright notice and this permission notice appear in all copies.
-#
-# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
-# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
-# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
-# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
-# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
-# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
-# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
-#
-
-irccd_define_test(
-    NAME rule-move-command
-    SOURCES main.cpp
-    LIBRARIES libirccd libirccdctl
-)
--- a/tests/src/rule-move-command/main.cpp	Thu Dec 14 21:51:22 2017 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,490 +0,0 @@
-/*
- * main.cpp -- test rule-move remote command
- *
- * Copyright (c) 2013-2017 David Demelier <markand@malikania.fr>
- *
- * Permission to use, copy, modify, and/or distribute this software for any
- * purpose with or without fee is hereby granted, provided that the above
- * copyright notice and this permission notice appear in all copies.
- *
- * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
- * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
- * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
- * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
- * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
- * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
- * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
- */
-
-#define BOOST_TEST_MODULE "rule-move"
-#include <boost/test/unit_test.hpp>
-
-#include <irccd/json_util.hpp>
-
-#include <irccd/daemon/command.hpp>
-#include <irccd/daemon/rule_service.hpp>
-
-#include <irccd/test/command_test.hpp>
-
-namespace irccd {
-
-namespace {
-
-class rule_move_test : public command_test<rule_move_command, rule_list_command> {
-public:
-    rule_move_test()
-    {
-        daemon_->rules().add(rule(
-            { "s0" },
-            { "c0" },
-            { "o0" },
-            { "p0" },
-            { "onMessage" },
-            rule::action_type::drop
-        ));
-        daemon_->rules().add(rule(
-            { "s1", },
-            { "c1", },
-            { "o1", },
-            { "p1", },
-            { "onMessage", },
-            rule::action_type::accept
-        ));
-        daemon_->rules().add(rule(
-            { "s2", },
-            { "c2", },
-            { "o2", },
-            { "p2", },
-            { "onMessage", },
-            rule::action_type::accept
-        ));
-    }
-};
-
-} // !namespace
-
-BOOST_FIXTURE_TEST_SUITE(rule_move_test_suite, rule_move_test)
-
-BOOST_AUTO_TEST_CASE(backward)
-{
-    nlohmann::json result;
-
-    ctl_->send({
-        { "command",    "rule-move" },
-        { "from",       2           },
-        { "to",         0           }
-    });
-    ctl_->recv([&] (auto, auto msg) {
-        result = msg;
-    });
-
-    wait_for([&] () {
-        return result.is_object();
-    });
-
-    BOOST_TEST(result.is_object());
-
-    result = nullptr;
-    ctl_->send({{ "command", "rule-list" }});
-    ctl_->recv([&] (auto, auto msg) {
-        result = msg;
-    });
-
-    wait_for([&] () {
-        return result.is_object();
-    });
-
-    BOOST_TEST(result.is_object());
-
-    // Rule 2.
-    {
-        auto servers = result["list"][0]["servers"];
-        auto channels = result["list"][0]["channels"];
-        auto plugins = result["list"][0]["plugins"];
-        auto events = result["list"][0]["events"];
-
-        BOOST_TEST(json_util::contains(servers, "s2"));
-        BOOST_TEST(json_util::contains(channels, "c2"));
-        BOOST_TEST(json_util::contains(plugins, "p2"));
-        BOOST_TEST(json_util::contains(events, "onMessage"));
-        BOOST_TEST(result["list"][0]["action"].get<std::string>() == "accept");
-    }
-
-    // Rule 0.
-    {
-        auto servers = result["list"][1]["servers"];
-        auto channels = result["list"][1]["channels"];
-        auto plugins = result["list"][1]["plugins"];
-        auto events = result["list"][1]["events"];
-
-        BOOST_TEST(json_util::contains(servers, "s0"));
-        BOOST_TEST(json_util::contains(channels, "c0"));
-        BOOST_TEST(json_util::contains(plugins, "p0"));
-        BOOST_TEST(json_util::contains(events, "onMessage"));
-        BOOST_TEST(result["list"][1]["action"].get<std::string>() == "drop");
-    }
-
-    // Rule 1.
-    {
-        auto servers = result["list"][2]["servers"];
-        auto channels = result["list"][2]["channels"];
-        auto plugins = result["list"][2]["plugins"];
-        auto events = result["list"][2]["events"];
-
-        BOOST_TEST(json_util::contains(servers, "s1"));
-        BOOST_TEST(json_util::contains(channels, "c1"));
-        BOOST_TEST(json_util::contains(plugins, "p1"));
-        BOOST_TEST(json_util::contains(events, "onMessage"));
-        BOOST_TEST(result["list"][2]["action"].get<std::string>() == "accept");
-    }
-}
-
-BOOST_AUTO_TEST_CASE(upward)
-{
-    nlohmann::json result;
-
-    ctl_->send({
-        { "command",    "rule-move" },
-        { "from",       0           },
-        { "to",         2           }
-    });
-    ctl_->recv([&] (auto, auto msg) {
-        result = msg;
-    });
-
-    wait_for([&] () {
-        return result.is_object();
-    });
-
-    BOOST_TEST(result.is_object());
-
-    result = nullptr;
-    ctl_->send({{ "command", "rule-list" }});
-    ctl_->recv([&] (auto, auto msg) {
-        result = msg;
-    });
-
-    wait_for([&] () {
-        return result.is_object();
-    });
-
-    BOOST_TEST(result.is_object());
-
-    // Rule 1.
-    {
-        auto servers = result["list"][0]["servers"];
-        auto channels = result["list"][0]["channels"];
-        auto plugins = result["list"][0]["plugins"];
-        auto events = result["list"][0]["events"];
-
-        BOOST_TEST(json_util::contains(servers, "s1"));
-        BOOST_TEST(json_util::contains(channels, "c1"));
-        BOOST_TEST(json_util::contains(plugins, "p1"));
-        BOOST_TEST(json_util::contains(events, "onMessage"));
-        BOOST_TEST(result["list"][0]["action"].get<std::string>() == "accept");
-    }
-
-    // Rule 2.
-    {
-        auto servers = result["list"][1]["servers"];
-        auto channels = result["list"][1]["channels"];
-        auto plugins = result["list"][1]["plugins"];
-        auto events = result["list"][1]["events"];
-
-        BOOST_TEST(json_util::contains(servers, "s2"));
-        BOOST_TEST(json_util::contains(channels, "c2"));
-        BOOST_TEST(json_util::contains(plugins, "p2"));
-        BOOST_TEST(json_util::contains(events, "onMessage"));
-        BOOST_TEST(result["list"][1]["action"].get<std::string>() == "accept");
-    }
-
-    // Rule 0.
-    {
-        auto servers = result["list"][2]["servers"];
-        auto channels = result["list"][2]["channels"];
-        auto plugins = result["list"][2]["plugins"];
-        auto events = result["list"][2]["events"];
-
-        BOOST_TEST(json_util::contains(servers, "s0"));
-        BOOST_TEST(json_util::contains(channels, "c0"));
-        BOOST_TEST(json_util::contains(plugins, "p0"));
-        BOOST_TEST(json_util::contains(events, "onMessage"));
-        BOOST_TEST(result["list"][2]["action"].get<std::string>() == "drop");
-    }
-}
-
-BOOST_AUTO_TEST_CASE(same)
-{
-    nlohmann::json result;
-
-    ctl_->send({
-        { "command",    "rule-move" },
-        { "from",       1           },
-        { "to",         1           }
-    });
-    ctl_->recv([&] (auto, auto msg) {
-        result = msg;
-    });
-
-    wait_for([&] () {
-        return result.is_object();
-    });
-
-    BOOST_TEST(result.is_object());
-
-    result = nullptr;
-    ctl_->send({{ "command", "rule-list" }});
-    ctl_->recv([&] (auto, auto msg) {
-        result = msg;
-    });
-
-    wait_for([&] () {
-        return result.is_object();
-    });
-
-    BOOST_TEST(result.is_object());
-
-    // Rule 0.
-    {
-        auto servers = result["list"][0]["servers"];
-        auto channels = result["list"][0]["channels"];
-        auto plugins = result["list"][0]["plugins"];
-        auto events = result["list"][0]["events"];
-
-        BOOST_TEST(json_util::contains(servers, "s0"));
-        BOOST_TEST(json_util::contains(channels, "c0"));
-        BOOST_TEST(json_util::contains(plugins, "p0"));
-        BOOST_TEST(json_util::contains(events, "onMessage"));
-        BOOST_TEST(result["list"][0]["action"].get<std::string>() == "drop");
-    }
-
-    // Rule 1.
-    {
-        auto servers = result["list"][1]["servers"];
-        auto channels = result["list"][1]["channels"];
-        auto plugins = result["list"][1]["plugins"];
-        auto events = result["list"][1]["events"];
-
-        BOOST_TEST(json_util::contains(servers, "s1"));
-        BOOST_TEST(json_util::contains(channels, "c1"));
-        BOOST_TEST(json_util::contains(plugins, "p1"));
-        BOOST_TEST(json_util::contains(events, "onMessage"));
-        BOOST_TEST(result["list"][1]["action"].get<std::string>() == "accept");
-    }
-
-    // Rule 2.
-    {
-        auto servers = result["list"][2]["servers"];
-        auto channels = result["list"][2]["channels"];
-        auto plugins = result["list"][2]["plugins"];
-        auto events = result["list"][2]["events"];
-
-        BOOST_TEST(json_util::contains(servers, "s2"));
-        BOOST_TEST(json_util::contains(channels, "c2"));
-        BOOST_TEST(json_util::contains(plugins, "p2"));
-        BOOST_TEST(json_util::contains(events, "onMessage"));
-        BOOST_TEST(result["list"][2]["action"].get<std::string>() == "accept");
-    }
-}
-
-BOOST_AUTO_TEST_CASE(beyond)
-{
-    nlohmann::json result;
-
-    ctl_->send({
-        { "command",    "rule-move" },
-        { "from",       0           },
-        { "to",         123         }
-    });
-    ctl_->recv([&] (auto, auto msg) {
-        result = msg;
-    });
-
-    wait_for([&] () {
-        return result.is_object();
-    });
-
-    BOOST_TEST(result.is_object());
-
-    result = nullptr;
-    ctl_->send({{ "command", "rule-list" }});
-    ctl_->recv([&] (auto, auto msg) {
-        result = msg;
-    });
-
-    wait_for([&] () {
-        return result.is_object();
-    });
-
-    BOOST_TEST(result.is_object());
-
-    // Rule 1.
-    {
-        auto servers = result["list"][0]["servers"];
-        auto channels = result["list"][0]["channels"];
-        auto plugins = result["list"][0]["plugins"];
-        auto events = result["list"][0]["events"];
-
-        BOOST_TEST(json_util::contains(servers, "s1"));
-        BOOST_TEST(json_util::contains(channels, "c1"));
-        BOOST_TEST(json_util::contains(plugins, "p1"));
-        BOOST_TEST(json_util::contains(events, "onMessage"));
-        BOOST_TEST(result["list"][0]["action"].get<std::string>() == "accept");
-    }
-
-    // Rule 2.
-    {
-        auto servers = result["list"][1]["servers"];
-        auto channels = result["list"][1]["channels"];
-        auto plugins = result["list"][1]["plugins"];
-        auto events = result["list"][1]["events"];
-
-        BOOST_TEST(json_util::contains(servers, "s2"));
-        BOOST_TEST(json_util::contains(channels, "c2"));
-        BOOST_TEST(json_util::contains(plugins, "p2"));
-        BOOST_TEST(json_util::contains(events, "onMessage"));
-        BOOST_TEST(result["list"][1]["action"].get<std::string>() == "accept");
-    }
-
-    // Rule 0.
-    {
-        auto servers = result["list"][2]["servers"];
-        auto channels = result["list"][2]["channels"];
-        auto plugins = result["list"][2]["plugins"];
-        auto events = result["list"][2]["events"];
-
-        BOOST_TEST(json_util::contains(servers, "s0"));
-        BOOST_TEST(json_util::contains(channels, "c0"));
-        BOOST_TEST(json_util::contains(plugins, "p0"));
-        BOOST_TEST(json_util::contains(events, "onMessage"));
-        BOOST_TEST(result["list"][2]["action"].get<std::string>() == "drop");
-    }
-}
-
-BOOST_AUTO_TEST_SUITE(errors)
-
-BOOST_AUTO_TEST_CASE(invalid_index_1_from)
-{
-    boost::system::error_code result;
-    nlohmann::json message;
-
-    ctl_->send({
-        { "command",    "rule-move" },
-        { "from",       -100        },
-        { "to",         0           }
-    });
-    ctl_->recv([&] (auto rresult, auto rmessage) {
-        result = rresult;
-        message = rmessage;
-    });
-
-    wait_for([&] {
-        return result;
-    });
-
-    BOOST_TEST(result == rule_error::invalid_index);
-    BOOST_TEST(message["error"].template get<int>() == rule_error::invalid_index);
-    BOOST_TEST(message["errorCategory"].template get<std::string>() == "rule");
-}
-
-BOOST_AUTO_TEST_CASE(invalid_index_1_to)
-{
-    boost::system::error_code result;
-    nlohmann::json message;
-
-    ctl_->send({
-        { "command",    "rule-move" },
-        { "from",       0           },
-        { "to",         -100        }
-    });
-    ctl_->recv([&] (auto rresult, auto rmessage) {
-        result = rresult;
-        message = rmessage;
-    });
-
-    wait_for([&] {
-        return result;
-    });
-
-    BOOST_TEST(result == rule_error::invalid_index);
-    BOOST_TEST(message["error"].template get<int>() == rule_error::invalid_index);
-    BOOST_TEST(message["errorCategory"].template get<std::string>() == "rule");
-}
-
-BOOST_AUTO_TEST_CASE(invalid_index_2_from)
-{
-    boost::system::error_code result;
-    nlohmann::json message;
-
-    ctl_->send({
-        { "command",    "rule-move" },
-        { "from",       100         },
-        { "to",         0           }
-    });
-    ctl_->recv([&] (auto rresult, auto rmessage) {
-        result = rresult;
-        message = rmessage;
-    });
-
-    wait_for([&] {
-        return result;
-    });
-
-    BOOST_TEST(result == rule_error::invalid_index);
-    BOOST_TEST(message["error"].template get<int>() == rule_error::invalid_index);
-    BOOST_TEST(message["errorCategory"].template get<std::string>() == "rule");
-}
-
-BOOST_AUTO_TEST_CASE(invalid_index_3_from)
-{
-    boost::system::error_code result;
-    nlohmann::json message;
-
-    ctl_->send({
-        { "command",    "rule-move" },
-        { "from",       "notaint"   },
-        { "to",         0           }
-    });
-    ctl_->recv([&] (auto rresult, auto rmessage) {
-        result = rresult;
-        message = rmessage;
-    });
-
-    wait_for([&] {
-        return result;
-    });
-
-    BOOST_TEST(result == rule_error::invalid_index);
-    BOOST_TEST(message["error"].template get<int>() == rule_error::invalid_index);
-    BOOST_TEST(message["errorCategory"].template get<std::string>() == "rule");
-}
-
-BOOST_AUTO_TEST_CASE(invalid_index_3_to)
-{
-    boost::system::error_code result;
-    nlohmann::json message;
-
-    ctl_->send({
-        { "command",    "rule-move" },
-        { "from",       0           },
-        { "to",         "notaint"   }
-    });
-    ctl_->recv([&] (auto rresult, auto rmessage) {
-        result = rresult;
-        message = rmessage;
-    });
-
-    wait_for([&] {
-        return result;
-    });
-
-    BOOST_TEST(result == rule_error::invalid_index);
-    BOOST_TEST(message["error"].template get<int>() == rule_error::invalid_index);
-    BOOST_TEST(message["errorCategory"].template get<std::string>() == "rule");
-}
-
-BOOST_AUTO_TEST_SUITE_END()
-
-BOOST_AUTO_TEST_SUITE_END()
-
-} // !irccd
--- a/tests/src/rule-remove-command/CMakeLists.txt	Thu Dec 14 21:51:22 2017 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,23 +0,0 @@
-#
-# CMakeLists.txt -- CMake build system for irccd
-#
-# Copyright (c) 2013-2017 David Demelier <markand@malikania.fr>
-#
-# Permission to use, copy, modify, and/or distribute this software for any
-# purpose with or without fee is hereby granted, provided that the above
-# copyright notice and this permission notice appear in all copies.
-#
-# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
-# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
-# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
-# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
-# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
-# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
-# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
-#
-
-irccd_define_test(
-    NAME rule-remove-command
-    SOURCES main.cpp
-    LIBRARIES libirccd libirccdctl
-)
--- a/tests/src/rule-remove-command/main.cpp	Thu Dec 14 21:51:22 2017 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,204 +0,0 @@
-/*
- * main.cpp -- test rule-remove remote command
- *
- * Copyright (c) 2013-2017 David Demelier <markand@malikania.fr>
- *
- * Permission to use, copy, modify, and/or distribute this software for any
- * purpose with or without fee is hereby granted, provided that the above
- * copyright notice and this permission notice appear in all copies.
- *
- * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
- * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
- * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
- * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
- * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
- * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
- * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
- */
-
-#define BOOST_TEST_MODULE "rule-remove"
-#include <boost/test/unit_test.hpp>
-
-#include <irccd/json_util.hpp>
-
-#include <irccd/daemon/command.hpp>
-#include <irccd/daemon/rule_service.hpp>
-
-#include <irccd/test/command_test.hpp>
-
-namespace irccd {
-
-namespace {
-
-class rule_remove_test : public command_test<rule_remove_command, rule_list_command> {
-public:
-    rule_remove_test()
-    {
-        daemon_->rules().add(rule(
-            { "s1", "s2" },
-            { "c1", "c2" },
-            { "o1", "o2" },
-            { "p1", "p2" },
-            { "onMessage", "onCommand" },
-            rule::action_type::drop
-        ));
-        daemon_->rules().add(rule(
-            { "s1", },
-            { "c1", },
-            { "o1", },
-            { "p1", },
-            { "onMessage", },
-            rule::action_type::accept
-        ));
-    }
-};
-
-} // !namespace
-
-BOOST_FIXTURE_TEST_SUITE(rule_remove_test_suite, rule_remove_test)
-
-BOOST_AUTO_TEST_CASE(basic)
-{
-    nlohmann::json result;
-
-    ctl_->send({
-        { "command",    "rule-remove"   },
-        { "index",      1               }
-    });
-    ctl_->recv([&] (auto, auto msg) {
-        result = msg;
-    });
-
-    wait_for([&] () {
-        return result.is_object();
-    });
-
-    BOOST_TEST(result.is_object());
-
-    result = nullptr;
-    ctl_->send({{ "command", "rule-list" }});
-    ctl_->recv([&] (auto, auto msg) {
-        result = msg;
-    });
-
-    wait_for([&] () {
-        return result.is_object();
-    });
-
-    BOOST_TEST(result["list"].is_array());
-    BOOST_TEST(result["list"].size() == 1U);
-
-    auto servers = result["list"][0]["servers"];
-    auto channels = result["list"][0]["channels"];
-    auto plugins = result["list"][0]["plugins"];
-    auto events = result["list"][0]["events"];
-
-    BOOST_TEST(json_util::contains(servers, "s1"));
-    BOOST_TEST(json_util::contains(servers, "s2"));
-    BOOST_TEST(json_util::contains(channels, "c1"));
-    BOOST_TEST(json_util::contains(channels, "c2"));
-    BOOST_TEST(json_util::contains(plugins, "p1"));
-    BOOST_TEST(json_util::contains(plugins, "p2"));
-    BOOST_TEST(json_util::contains(events, "onMessage"));
-    BOOST_TEST(json_util::contains(events, "onCommand"));
-    BOOST_TEST(result["list"][0]["action"].get<std::string>() == "drop");
-}
-
-BOOST_AUTO_TEST_CASE(empty)
-{
-    nlohmann::json result;
-
-    daemon_->rules().remove(0);
-    daemon_->rules().remove(0);
-
-    ctl_->send({
-        { "command",    "rule-remove"   },
-        { "index",      1               }
-    });
-    ctl_->recv([&] (auto, auto msg) {
-        result = msg;
-    });
-
-    wait_for([&] () {
-        return result.is_object();
-    });
-
-    BOOST_TEST(result.is_object());
-}
-
-BOOST_AUTO_TEST_SUITE(errors)
-
-BOOST_AUTO_TEST_CASE(invalid_index_1)
-{
-    boost::system::error_code result;
-    nlohmann::json message;
-
-    ctl_->send({
-        { "command",    "rule-remove"   },
-        { "index",      -100            }
-    });
-    ctl_->recv([&] (auto rresult, auto rmessage) {
-        result = rresult;
-        message = rmessage;
-    });
-
-    wait_for([&] {
-        return result;
-    });
-
-    BOOST_TEST(result == rule_error::invalid_index);
-    BOOST_TEST(message["error"].template get<int>() == rule_error::invalid_index);
-    BOOST_TEST(message["errorCategory"].template get<std::string>() == "rule");
-}
-
-BOOST_AUTO_TEST_CASE(invalid_index_2)
-{
-    boost::system::error_code result;
-    nlohmann::json message;
-
-    ctl_->send({
-        { "command",    "rule-remove"   },
-        { "index",      100             }
-    });
-    ctl_->recv([&] (auto rresult, auto rmessage) {
-        result = rresult;
-        message = rmessage;
-    });
-
-    wait_for([&] {
-        return result;
-    });
-
-    BOOST_TEST(result == rule_error::invalid_index);
-    BOOST_TEST(message["error"].template get<int>() == rule_error::invalid_index);
-    BOOST_TEST(message["errorCategory"].template get<std::string>() == "rule");
-}
-
-BOOST_AUTO_TEST_CASE(invalid_index_3)
-{
-    boost::system::error_code result;
-    nlohmann::json message;
-
-    ctl_->send({
-        { "command",    "rule-remove"   },
-        { "index",      "notaint"       }
-    });
-    ctl_->recv([&] (auto rresult, auto rmessage) {
-        result = rresult;
-        message = rmessage;
-    });
-
-    wait_for([&] {
-        return result;
-    });
-
-    BOOST_TEST(result == rule_error::invalid_index);
-    BOOST_TEST(message["error"].template get<int>() == rule_error::invalid_index);
-    BOOST_TEST(message["errorCategory"].template get<std::string>() == "rule");
-}
-
-BOOST_AUTO_TEST_SUITE_END()
-
-BOOST_AUTO_TEST_SUITE_END()
-
-} // !irccd
--- a/tests/src/rules/CMakeLists.txt	Thu Dec 14 21:51:22 2017 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,23 +0,0 @@
-#
-# CMakeLists.txt -- CMake build system for irccd
-#
-# Copyright (c) 2013-2017 David Demelier <markand@malikania.fr>
-#
-# Permission to use, copy, modify, and/or distribute this software for any
-# purpose with or without fee is hereby granted, provided that the above
-# copyright notice and this permission notice appear in all copies.
-#
-# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
-# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
-# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
-# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
-# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
-# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
-# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
-#
-
-irccd_define_test(
-    NAME rules
-    SOURCES main.cpp
-    LIBRARIES libirccd
-)
--- a/tests/src/rules/main.cpp	Thu Dec 14 21:51:22 2017 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,246 +0,0 @@
-/*
- * main.cpp -- test irccd rules
- *
- * Copyright (c) 2013-2017 David Demelier <markand@malikania.fr>
- *
- * Permission to use, copy, modify, and/or distribute this software for any
- * purpose with or without fee is hereby granted, provided that the above
- * copyright notice and this permission notice appear in all copies.
- *
- * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
- * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
- * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
- * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
- * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
- * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
- * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
- */
-
-#define BOOST_TEST_MODULE "Rules"
-#include <boost/test/unit_test.hpp>
-
-#include <irccd/daemon/irccd.hpp>
-#include <irccd/daemon/logger.hpp>
-#include <irccd/daemon/rule_service.hpp>
-
-namespace irccd {
-
-/*
- * Simulate the following rules configuration:
- *
- * #
- * # On all servers, each channel #staff can't use the onCommand event,
- * # everything else is allowed.
- * #
- * [rule]       #1
- * servers      = ""
- * channels     = "#staff"
- * events       = "onCommand"
- * action       = drop
- *
- * #
- * # However, the same onCommand on #staff is allowed on server "unsafe"
- * #
- * [rule]       #2
- * servers      = "unsafe"
- * channels     = "#staff"
- * events       = "onCommand"
- * action       = accept
- *
- * #
- * # Plugin game is only allowed on server "malikania" and "localhost",
- * # channel "#games" and events "onMessage, onCommand".
- * #
- * # The first rule #3-1 disable the plugin game for every server, it is
- * # reenabled again with the #3-2.
- * #
- * [rule]       #3-1
- * plugins      = "game"
- * action       = drop
- *
- * [rule]       #3-2
- * servers      = "malikania localhost"
- * channels     = "#games"
- * plugins      = "game"
- * events       = "onMessage onCommand"
- * action       = accept
- */
-class rules_test {
-protected:
-    boost::asio::io_service service_;
-    irccd daemon_{service_};
-    rule_service rules_{daemon_};
-
-    rules_test()
-    {
-        daemon_.set_log(std::make_unique<silent_logger>());
-
-        // #1
-        {
-            rules_.add({
-                rule::set{                }, // Servers
-                rule::set{ "#staff"       }, // Channels
-                rule::set{                }, // Origins
-                rule::set{                }, // Plugins
-                rule::set{ "onCommand"    }, // Events
-                rule::action_type::drop
-            });
-        }
-
-        // #2
-        {
-            rules_.add({
-                rule::set{ "unsafe"       },
-                rule::set{ "#staff"       },
-                rule::set{                },
-                rule::set{                },
-                rule::set{ "onCommand"    },
-                rule::action_type::accept
-            });
-        }
-
-        // #3-1
-        {
-            rules_.add({
-                rule::set{},
-                rule::set{},
-                rule::set{},
-                rule::set{"game"},
-                rule::set{},
-                rule::action_type::drop
-            });
-        }
-
-        // #3-2
-        {
-            rules_.add({
-                rule::set{ "malikania", "localhost"   },
-                rule::set{ "#games"                   },
-                rule::set{                            },
-                rule::set{ "game"                     },
-                rule::set{ "onCommand", "onMessage"   },
-                rule::action_type::accept
-            });
-        }
-    }
-};
-
-BOOST_FIXTURE_TEST_SUITE(rules_test_suite, rules_test)
-
-BOOST_AUTO_TEST_CASE(basic_match1)
-{
-    rule m;
-
-    /*
-     * [rule]
-     */
-    BOOST_REQUIRE(m.match("freenode", "#test", "a", "", ""));
-    BOOST_REQUIRE(m.match("", "", "", "", ""));
-}
-
-BOOST_AUTO_TEST_CASE(basic_match2)
-{
-    rule m(rule::set{"freenode"});
-
-    /*
-     * [rule]
-     * servers    = "freenode"
-     */
-
-    BOOST_REQUIRE(m.match("freenode", "#test", "a", "", ""));
-    BOOST_REQUIRE(!m.match("malikania", "#test", "a", "", ""));
-    BOOST_REQUIRE(m.match("freenode", "", "jean", "", "onMessage"));
-}
-
-BOOST_AUTO_TEST_CASE(basic_match3)
-{
-    rule m(rule::set{"freenode"}, rule::set{"#staff"});
-
-    /*
-     * [rule]
-     * servers    = "freenode"
-     * channels    = "#staff"
-     */
-
-    BOOST_REQUIRE(m.match("freenode", "#staff", "a", "", ""));
-    BOOST_REQUIRE(!m.match("freenode", "#test", "a", "", ""));
-    BOOST_REQUIRE(!m.match("malikania", "#staff", "a", "", ""));
-}
-
-BOOST_AUTO_TEST_CASE(basic_match4)
-{
-    rule m(rule::set{"malikania"}, rule::set{"#staff"}, rule::set{"a"});
-
-    /*
-     * [rule]
-     * servers    = "malikania"
-     * channels    = "#staff"
-     * plugins    = "a"
-     */
-
-    BOOST_REQUIRE(m.match("malikania", "#staff", "a", "",""));
-    BOOST_REQUIRE(!m.match("malikania", "#staff", "b", "", ""));
-    BOOST_REQUIRE(!m.match("freenode", "#staff", "a", "", ""));
-}
-
-BOOST_AUTO_TEST_CASE(complex_match1)
-{
-    rule m(rule::set{"malikania", "freenode"});
-
-    /*
-     * [rule]
-     * servers    = "malikania freenode"
-     */
-
-    BOOST_REQUIRE(m.match("malikania", "", "", "", ""));
-    BOOST_REQUIRE(m.match("freenode", "", "", "", ""));
-    BOOST_REQUIRE(!m.match("no", "", "", "", ""));
-}
-
-BOOST_AUTO_TEST_CASE(basic_solve)
-{
-    /* Allowed */
-    BOOST_REQUIRE(rules_.solve("malikania", "#staff", "", "a", "onMessage"));
-
-    /* Allowed */
-    BOOST_REQUIRE(rules_.solve("freenode", "#staff", "", "b", "onTopic"));
-
-    /* Not allowed */
-    BOOST_REQUIRE(!rules_.solve("malikania", "#staff", "", "", "onCommand"));
-
-    /* Not allowed */
-    BOOST_REQUIRE(!rules_.solve("freenode", "#staff", "", "c", "onCommand"));
-
-    /* Allowed */
-    BOOST_REQUIRE(rules_.solve("unsafe", "#staff", "", "c", "onCommand"));
-}
-
-BOOST_AUTO_TEST_CASE(games_solve)
-{
-    /* Allowed */
-    BOOST_REQUIRE(rules_.solve("malikania", "#games", "", "game", "onMessage"));
-
-    /* Allowed */
-    BOOST_REQUIRE(rules_.solve("localhost", "#games", "", "game", "onMessage"));
-
-    /* Allowed */
-    BOOST_REQUIRE(rules_.solve("malikania", "#games", "", "game", "onCommand"));
-
-    /* Not allowed */
-    BOOST_REQUIRE(!rules_.solve("malikania", "#games", "", "game", "onQuery"));
-
-    /* Not allowed */
-    BOOST_REQUIRE(!rules_.solve("freenode", "#no", "", "game", "onMessage"));
-
-    /* Not allowed */
-    BOOST_REQUIRE(!rules_.solve("malikania", "#test", "", "game", "onMessage"));
-}
-
-BOOST_AUTO_TEST_CASE(fix_645)
-{
-    BOOST_REQUIRE(!rules_.solve("MALIKANIA", "#STAFF", "", "SYSTEM", "onCommand"));
-}
-
-BOOST_AUTO_TEST_SUITE_END()
-
-} // !irccd
--- a/tests/src/server-connect-command/CMakeLists.txt	Thu Dec 14 21:51:22 2017 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,23 +0,0 @@
-#
-# CMakeLists.txt -- CMake build system for irccd
-#
-# Copyright (c) 2013-2017 David Demelier <markand@malikania.fr>
-#
-# Permission to use, copy, modify, and/or distribute this software for any
-# purpose with or without fee is hereby granted, provided that the above
-# copyright notice and this permission notice appear in all copies.
-#
-# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
-# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
-# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
-# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
-# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
-# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
-# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
-#
-
-irccd_define_test(
-    NAME server-connect-command
-    SOURCES main.cpp
-    LIBRARIES libirccd libirccdctl
-)
--- a/tests/src/server-connect-command/main.cpp	Thu Dec 14 21:51:22 2017 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,336 +0,0 @@
-/*
- * main.cpp -- test server-connect remote command
- *
- * Copyright (c) 2013-2017 David Demelier <markand@malikania.fr>
- *
- * Permission to use, copy, modify, and/or distribute this software for any
- * purpose with or without fee is hereby granted, provided that the above
- * copyright notice and this permission notice appear in all copies.
- *
- * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
- * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
- * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
- * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
- * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
- * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
- * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
- */
-
-#define BOOST_TEST_MODULE "server-connect"
-#include <boost/test/unit_test.hpp>
-
-#include <irccd/daemon/server_service.hpp>
-
-#include <irccd/test/command_test.hpp>
-#include <irccd/test/journal_server.hpp>
-
-namespace irccd {
-
-BOOST_FIXTURE_TEST_SUITE(server_connect_test_suite, command_test<server_connect_command>)
-
-BOOST_AUTO_TEST_CASE(minimal)
-{
-    nlohmann::json result;
-
-    ctl_->send({
-        { "command",    "server-connect"    },
-        { "name",       "local"             },
-        { "host",       "irc.example.org"   }
-    });
-    ctl_->recv([&] (auto, auto msg) {
-        result = msg;
-    });
-
-    wait_for([&] () {
-        return result.is_object();
-    });
-
-    auto s = daemon_->servers().get("local");
-
-    BOOST_TEST(s);
-    BOOST_TEST(s->name() == "local");
-    BOOST_TEST(s->host() == "irc.example.org");
-    BOOST_TEST(s->port() == 6667U);
-}
-
-#if defined(HAVE_SSL)
-
-BOOST_AUTO_TEST_CASE(full)
-{
-    nlohmann::json result;
-
-    ctl_->send({
-        { "command",    "server-connect"    },
-        { "name",       "local2"            },
-        { "host",       "irc.example2.org"  },
-        { "password",   "nonono"            },
-        { "nickname",   "francis"           },
-        { "realname",   "the_francis"       },
-        { "username",   "frc"               },
-        { "ctcpVersion", "ultra bot"        },
-        { "commandChar", "::"               },
-        { "port",       18000               },
-        { "ssl",        true                },
-        { "sslVerify",  true                },
-        { "autoRejoin", true                },
-        { "joinInvite", true                }
-    });
-    ctl_->recv([&] (auto, auto msg) {
-        result = msg;
-    });
-
-    wait_for([&] () {
-        return result.is_object();
-    });
-
-    auto s = daemon_->servers().get("local2");
-
-    BOOST_TEST(s);
-    BOOST_TEST(s->name() == "local2");
-    BOOST_TEST(s->host() == "irc.example2.org");
-    BOOST_TEST(s->port() == 18000U);
-    BOOST_TEST(s->password() == "nonono");
-    BOOST_TEST(s->nickname() == "francis");
-    BOOST_TEST(s->realname() == "the_francis");
-    BOOST_TEST(s->username() == "frc");
-    BOOST_TEST(s->command_char() == "::");
-    BOOST_TEST(s->ctcp_version() == "ultra bot");
-    BOOST_TEST(s->flags() & server::ssl);
-    BOOST_TEST(s->flags() & server::ssl_verify);
-    BOOST_TEST(s->flags() & server::auto_rejoin);
-    BOOST_TEST(s->flags() & server::join_invite);
-}
-
-#endif // !HAVE_SSL
-
-BOOST_AUTO_TEST_SUITE(errors)
-
-BOOST_AUTO_TEST_CASE(already_exists)
-{
-    boost::system::error_code result;
-    nlohmann::json message;
-
-    daemon_->servers().add(std::make_unique<journal_server>(service_, "local"));
-    ctl_->send({
-        { "command",    "server-connect"    },
-        { "name",       "local"             },
-        { "host",       "127.0.0.1"         }
-    });
-    ctl_->recv([&] (auto rresult, auto rmessage) {
-        result = rresult;
-        message = rmessage;
-    });
-
-    wait_for([&] {
-        return result;
-    });
-
-    BOOST_TEST(result == server_error::already_exists);
-    BOOST_TEST(message["error"].template get<int>() == server_error::already_exists);
-    BOOST_TEST(message["errorCategory"].template get<std::string>() == "server");
-}
-
-BOOST_AUTO_TEST_CASE(invalid_hostname_1)
-{
-    boost::system::error_code result;
-    nlohmann::json message;
-
-    ctl_->send({
-        { "command",    "server-connect"    },
-        { "name",       "new"               },
-    });
-    ctl_->recv([&] (auto rresult, auto rmessage) {
-        result = rresult;
-        message = rmessage;
-    });
-
-    wait_for([&] {
-        return result;
-    });
-
-    BOOST_TEST(result == server_error::invalid_hostname);
-    BOOST_TEST(message["error"].template get<int>() == server_error::invalid_hostname);
-    BOOST_TEST(message["errorCategory"].template get<std::string>() == "server");
-}
-
-BOOST_AUTO_TEST_CASE(invalid_hostname_2)
-{
-    boost::system::error_code result;
-    nlohmann::json message;
-
-    ctl_->send({
-        { "command",    "server-connect"    },
-        { "name",       "new"               },
-        { "host",       123456              }
-    });
-    ctl_->recv([&] (auto rresult, auto rmessage) {
-        result = rresult;
-        message = rmessage;
-    });
-
-    wait_for([&] {
-        return result;
-    });
-
-    BOOST_TEST(result == server_error::invalid_hostname);
-    BOOST_TEST(message["error"].template get<int>() == server_error::invalid_hostname);
-    BOOST_TEST(message["errorCategory"].template get<std::string>() == "server");
-}
-
-BOOST_AUTO_TEST_CASE(invalid_identifier_1)
-{
-    boost::system::error_code result;
-    nlohmann::json message;
-
-    ctl_->send({
-        { "command",    "server-connect"    },
-        { "name",       ""                  },
-        { "host",       "127.0.0.1"         }
-    });
-    ctl_->recv([&] (auto rresult, auto rmessage) {
-        result = rresult;
-        message = rmessage;
-    });
-
-    wait_for([&] {
-        return result;
-    });
-
-    BOOST_TEST(result == server_error::invalid_identifier);
-    BOOST_TEST(message["error"].template get<int>() == server_error::invalid_identifier);
-    BOOST_TEST(message["errorCategory"].template get<std::string>() == "server");
-}
-
-BOOST_AUTO_TEST_CASE(invalid_identifier_2)
-{
-    boost::system::error_code result;
-    nlohmann::json message;
-
-    ctl_->send({
-        { "command",    "server-connect"    },
-        { "name",       123456              },
-        { "host",       "127.0.0.1"         }
-    });
-    ctl_->recv([&] (auto rresult, auto rmessage) {
-        result = rresult;
-        message = rmessage;
-    });
-
-    wait_for([&] {
-        return result;
-    });
-
-    BOOST_TEST(result == server_error::invalid_identifier);
-    BOOST_TEST(message["error"].template get<int>() == server_error::invalid_identifier);
-    BOOST_TEST(message["errorCategory"].template get<std::string>() == "server");
-}
-
-BOOST_AUTO_TEST_CASE(invalid_port_1)
-{
-    boost::system::error_code result;
-    nlohmann::json message;
-
-    ctl_->send({
-        { "command",    "server-connect"    },
-        { "name",       "new"               },
-        { "host",       "127.0.0.1"         },
-        { "port",       "notaint"           }
-    });
-    ctl_->recv([&] (auto rresult, auto rmessage) {
-        result = rresult;
-        message = rmessage;
-    });
-
-    wait_for([&] {
-        return result;
-    });
-
-    BOOST_TEST(result == server_error::invalid_port);
-    BOOST_TEST(message["error"].template get<int>() == server_error::invalid_port);
-    BOOST_TEST(message["errorCategory"].template get<std::string>() == "server");
-}
-
-BOOST_AUTO_TEST_CASE(invalid_port_2)
-{
-    boost::system::error_code result;
-    nlohmann::json message;
-
-    ctl_->send({
-        { "command",    "server-connect"    },
-        { "name",       "new"               },
-        { "host",       "127.0.0.1"         },
-        { "port",       -123                }
-    });
-    ctl_->recv([&] (auto rresult, auto rmessage) {
-        result = rresult;
-        message = rmessage;
-    });
-
-    wait_for([&] {
-        return result;
-    });
-
-    BOOST_TEST(result == server_error::invalid_port);
-    BOOST_TEST(message["error"].template get<int>() == server_error::invalid_port);
-    BOOST_TEST(message["errorCategory"].template get<std::string>() == "server");
-}
-
-BOOST_AUTO_TEST_CASE(invalid_port_3)
-{
-    boost::system::error_code result;
-    nlohmann::json message;
-
-    ctl_->send({
-        { "command",    "server-connect"    },
-        { "name",       "new"               },
-        { "host",       "127.0.0.1"         },
-        { "port",       1000000             }
-    });
-    ctl_->recv([&] (auto rresult, auto rmessage) {
-        result = rresult;
-        message = rmessage;
-    });
-
-    wait_for([&] {
-        return result;
-    });
-
-    BOOST_TEST(result == server_error::invalid_port);
-    BOOST_TEST(message["error"].template get<int>() == server_error::invalid_port);
-    BOOST_TEST(message["errorCategory"].template get<std::string>() == "server");
-}
-
-#if !defined(HAVE_SSL)
-
-BOOST_AUTO_TEST_CASE(ssl_disabled)
-{
-    boost::system::error_code result;
-    nlohmann::json message;
-
-    ctl_->send({
-        { "command",    "server-connect"    },
-        { "name",       "new"               },
-        { "host",       "127.0.0.1"         },
-        { "ssl",        true                }
-    });
-    ctl_->recv([&] (auto rresult, auto rmessage) {
-        result = rresult;
-        message = rmessage;
-    });
-
-    wait_for([&] {
-        return result;
-    });
-
-    BOOST_TEST(result == server_error::ssl_disabled);
-    BOOST_TEST(message["error"].template get<int>() == server_error::ssl_disabled);
-    BOOST_TEST(message["errorCategory"].template get<std::string>() == "server");
-}
-
-#endif
-
-BOOST_AUTO_TEST_SUITE_END()
-
-BOOST_AUTO_TEST_SUITE_END()
-
-} // !irccd
--- a/tests/src/server-disconnect-command/CMakeLists.txt	Thu Dec 14 21:51:22 2017 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,23 +0,0 @@
-#
-# CMakeLists.txt -- CMake build system for irccd
-#
-# Copyright (c) 2013-2017 David Demelier <markand@malikania.fr>
-#
-# Permission to use, copy, modify, and/or distribute this software for any
-# purpose with or without fee is hereby granted, provided that the above
-# copyright notice and this permission notice appear in all copies.
-#
-# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
-# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
-# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
-# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
-# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
-# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
-# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
-#
-
-irccd_define_test(
-    NAME server-disconnect-command
-    SOURCES main.cpp
-    LIBRARIES libirccd libirccdctl
-)
--- a/tests/src/server-disconnect-command/main.cpp	Thu Dec 14 21:51:22 2017 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,135 +0,0 @@
-/*
- * main.cpp -- test server-disconnect remote command
- *
- * Copyright (c) 2013-2017 David Demelier <markand@malikania.fr>
- *
- * Permission to use, copy, modify, and/or distribute this software for any
- * purpose with or without fee is hereby granted, provided that the above
- * copyright notice and this permission notice appear in all copies.
- *
- * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
- * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
- * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
- * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
- * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
- * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
- * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
- */
-
-#define BOOST_TEST_MODULE "server-disconnect"
-#include <boost/test/unit_test.hpp>
-
-#include <irccd/daemon/server_service.hpp>
-
-#include <irccd/test/journal_server.hpp>
-#include <irccd/test/command_test.hpp>
-
-namespace irccd {
-
-namespace {
-
-class server_disconnect_test : public command_test<server_disconnect_command> {
-protected:
-    server_disconnect_test()
-    {
-        daemon_->servers().add(std::make_unique<journal_server>(service_, "s1"));
-        daemon_->servers().add(std::make_unique<journal_server>(service_, "s2"));
-    }
-};
-
-} // !namespace
-
-BOOST_FIXTURE_TEST_SUITE(server_disconnect_test_suite, server_disconnect_test)
-
-BOOST_AUTO_TEST_CASE(one)
-{
-    nlohmann::json result;
-
-    ctl_->send({
-        { "command",    "server-disconnect" },
-        { "server",     "s1"                }
-    });
-    ctl_->recv([&] (auto, auto msg) {
-        result = msg;
-    });
-
-    wait_for([&] () {
-        return result.is_object();
-    });
-
-    BOOST_TEST(result["command"].get<std::string>() == "server-disconnect");
-    BOOST_TEST(!daemon_->servers().has("s1"));
-    BOOST_TEST(daemon_->servers().has("s2"));
-}
-
-BOOST_AUTO_TEST_CASE(all)
-{
-    nlohmann::json result;
-
-    ctl_->send({{"command", "server-disconnect"}});
-    ctl_->recv([&] (auto, auto msg) {
-        result = msg;
-    });
-
-    wait_for([&] () {
-        return result.is_object();
-    });
-
-    BOOST_TEST(result["command"].get<std::string>() == "server-disconnect");
-    BOOST_TEST(!daemon_->servers().has("s1"));
-    BOOST_TEST(!daemon_->servers().has("s2"));
-}
-
-BOOST_AUTO_TEST_SUITE(errors)
-
-BOOST_AUTO_TEST_CASE(invalid_identifier)
-{
-    boost::system::error_code result;
-    nlohmann::json message;
-
-    ctl_->send({
-        { "command",    "server-disconnect" },
-        { "server",     123456              }
-    });
-    ctl_->recv([&] (auto rresult, auto rmessage) {
-        result = rresult;
-        message = rmessage;
-    });
-
-    wait_for([&] {
-        return result;
-    });
-
-    BOOST_TEST(result == server_error::invalid_identifier);
-    BOOST_TEST(message["error"].template get<int>() == server_error::invalid_identifier);
-    BOOST_TEST(message["errorCategory"].template get<std::string>() == "server");
-}
-
-BOOST_AUTO_TEST_CASE(not_found)
-{
-    boost::system::error_code result;
-    nlohmann::json message;
-
-    ctl_->send({
-        { "command",    "server-disconnect" },
-        { "server",     "unknown"           }
-    });
-    ctl_->recv([&] (auto rresult, auto rmessage) {
-        result = rresult;
-        message = rmessage;
-    });
-
-    wait_for([&] {
-        return result;
-    });
-
-    BOOST_TEST(result == server_error::not_found);
-    BOOST_TEST(message["error"].template get<int>() == server_error::not_found);
-    BOOST_TEST(message["errorCategory"].template get<std::string>() == "server");
-}
-
-BOOST_AUTO_TEST_SUITE_END()
-
-BOOST_AUTO_TEST_SUITE_END()
-
-} // !irccd
--- a/tests/src/server-info-command/CMakeLists.txt	Thu Dec 14 21:51:22 2017 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,23 +0,0 @@
-#
-# CMakeLists.txt -- CMake build system for irccd
-#
-# Copyright (c) 2013-2017 David Demelier <markand@malikania.fr>
-#
-# Permission to use, copy, modify, and/or distribute this software for any
-# purpose with or without fee is hereby granted, provided that the above
-# copyright notice and this permission notice appear in all copies.
-#
-# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
-# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
-# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
-# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
-# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
-# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
-# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
-#
-
-irccd_define_test(
-    NAME server-info-command
-    SOURCES main.cpp
-    LIBRARIES libirccd libirccdctl
-)
--- a/tests/src/server-info-command/main.cpp	Thu Dec 14 21:51:22 2017 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,145 +0,0 @@
-/*
- * main.cpp -- test server-info remote command
- *
- * Copyright (c) 2013-2017 David Demelier <markand@malikania.fr>
- *
- * Permission to use, copy, modify, and/or distribute this software for any
- * purpose with or without fee is hereby granted, provided that the above
- * copyright notice and this permission notice appear in all copies.
- *
- * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
- * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
- * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
- * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
- * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
- * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
- * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
- */
-
-#define BOOST_TEST_MODULE "server-info"
-#include <boost/test/unit_test.hpp>
-
-#include <irccd/daemon/server_service.hpp>
-
-#include <irccd/test/command_test.hpp>
-#include <irccd/test/journal_server.hpp>
-
-namespace irccd {
-
-BOOST_FIXTURE_TEST_SUITE(server_info_test_suite, command_test<server_info_command>)
-
-BOOST_AUTO_TEST_CASE(basic)
-{
-    auto server = std::make_unique<journal_server>(service_, "test");
-
-    server->set_host("example.org");
-    server->set_port(8765);
-    server->set_password("none");
-    server->set_nickname("pascal");
-    server->set_username("psc");
-    server->set_realname("Pascal le grand frere");
-    server->set_ctcp_version("yeah");
-    server->set_command_char("@");
-    server->set_reconnect_tries(80);
-    server->set_ping_timeout(20000);
-
-    nlohmann::json result;
-
-    daemon_->servers().add(std::move(server));
-    ctl_->send({
-        { "command",    "server-info"       },
-        { "server",     "test"              },
-    });
-    ctl_->recv([&] (auto, auto msg) {
-        result = msg;
-    });
-
-    wait_for([&] () {
-        return result.is_object();
-    });
-
-    BOOST_TEST(result.is_object());
-    BOOST_TEST(result["host"].get<std::string>() == "example.org");
-    BOOST_TEST(result["name"].get<std::string>() == "test");
-    BOOST_TEST(result["nickname"].get<std::string>() == "pascal");
-    BOOST_TEST(result["port"].get<int>() == 8765);
-    BOOST_TEST(result["realname"].get<std::string>() == "Pascal le grand frere");
-    BOOST_TEST(result["username"].get<std::string>() == "psc");
-}
-
-BOOST_AUTO_TEST_SUITE(errors)
-
-BOOST_AUTO_TEST_CASE(invalid_identifier_1)
-{
-    boost::system::error_code result;
-    nlohmann::json message;
-
-    ctl_->send({
-        { "command",    "server-info"   },
-        { "server",     123456          }
-    });
-    ctl_->recv([&] (auto rresult, auto rmessage) {
-        result = rresult;
-        message = rmessage;
-    });
-
-    wait_for([&] {
-        return result;
-    });
-
-    BOOST_TEST(result == server_error::invalid_identifier);
-    BOOST_TEST(message["error"].template get<int>() == server_error::invalid_identifier);
-    BOOST_TEST(message["errorCategory"].template get<std::string>() == "server");
-}
-
-BOOST_AUTO_TEST_CASE(invalid_identifier_2)
-{
-    boost::system::error_code result;
-    nlohmann::json message;
-
-    ctl_->send({
-        { "command",    "server-info"   },
-        { "server",     ""              }
-    });
-    ctl_->recv([&] (auto rresult, auto rmessage) {
-        result = rresult;
-        message = rmessage;
-    });
-
-    wait_for([&] {
-        return result;
-    });
-
-    BOOST_TEST(result == server_error::invalid_identifier);
-    BOOST_TEST(message["error"].template get<int>() == server_error::invalid_identifier);
-    BOOST_TEST(message["errorCategory"].template get<std::string>() == "server");
-}
-
-BOOST_AUTO_TEST_CASE(not_found)
-{
-    boost::system::error_code result;
-    nlohmann::json message;
-
-    ctl_->send({
-        { "command",    "server-info"   },
-        { "server",     "unknown"       }
-    });
-    ctl_->recv([&] (auto rresult, auto rmessage) {
-        result = rresult;
-        message = rmessage;
-    });
-
-    wait_for([&] {
-        return result;
-    });
-
-    BOOST_TEST(result == server_error::not_found);
-    BOOST_TEST(message["error"].template get<int>() == server_error::not_found);
-    BOOST_TEST(message["errorCategory"].template get<std::string>() == "server");
-}
-
-BOOST_AUTO_TEST_SUITE_END()
-
-BOOST_AUTO_TEST_SUITE_END()
-
-} // !irccd
--- a/tests/src/server-invite-command/CMakeLists.txt	Thu Dec 14 21:51:22 2017 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,23 +0,0 @@
-#
-# CMakeLists.txt -- CMake build system for irccd
-#
-# Copyright (c) 2013-2017 David Demelier <markand@malikania.fr>
-#
-# Permission to use, copy, modify, and/or distribute this software for any
-# purpose with or without fee is hereby granted, provided that the above
-# copyright notice and this permission notice appear in all copies.
-#
-# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
-# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
-# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
-# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
-# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
-# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
-# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
-#
-
-irccd_define_test(
-    NAME server-invite-command
-    SOURCES main.cpp
-    LIBRARIES libirccd libirccdctl
-)
--- a/tests/src/server-invite-command/main.cpp	Thu Dec 14 21:51:22 2017 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,246 +0,0 @@
-/*
- * main.cpp -- test server-invite remote command
- *
- * Copyright (c) 2013-2017 David Demelier <markand@malikania.fr>
- *
- * Permission to use, copy, modify, and/or distribute this software for any
- * purpose with or without fee is hereby granted, provided that the above
- * copyright notice and this permission notice appear in all copies.
- *
- * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
- * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
- * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
- * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
- * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
- * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
- * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
- */
-
-#define BOOST_TEST_MODULE "server-invite"
-#include <boost/test/unit_test.hpp>
-
-#include <irccd/daemon/server_service.hpp>
-
-#include <irccd/test/command_test.hpp>
-#include <irccd/test/journal_server.hpp>
-
-namespace irccd {
-
-namespace {
-
-class server_invite_test : public command_test<server_invite_command> {
-protected:
-    std::shared_ptr<journal_server> server_{new journal_server(service_, "test")};
-
-    server_invite_test()
-    {
-        daemon_->servers().add(server_);
-    }
-};
-
-} // !namespace
-
-BOOST_FIXTURE_TEST_SUITE(server_invite_test_suite, server_invite_test)
-
-BOOST_AUTO_TEST_CASE(basic)
-{
-    ctl_->send({
-        { "command",    "server-invite"     },
-        { "server",     "test"              },
-        { "target",     "francis"           },
-        { "channel",    "#music"            }
-    });
-
-    wait_for([this] () {
-        return !server_->cqueue().empty();
-    });
-
-    auto cmd = server_->cqueue().back();
-
-    BOOST_TEST(cmd["command"].get<std::string>() == "invite");
-    BOOST_TEST(cmd["channel"].get<std::string>() == "#music");
-    BOOST_TEST(cmd["target"].get<std::string>() == "francis");
-}
-
-BOOST_AUTO_TEST_SUITE(errors)
-
-BOOST_AUTO_TEST_CASE(invalid_identifier_1)
-{
-    boost::system::error_code result;
-    nlohmann::json message;
-
-    ctl_->send({
-        { "command",    "server-invite" },
-        { "server",     123456          },
-        { "target",     "francis"       },
-        { "channel",    "#music"        }
-    });
-    ctl_->recv([&] (auto rresult, auto rmessage) {
-        result = rresult;
-        message = rmessage;
-    });
-
-    wait_for([&] {
-        return result;
-    });
-
-    BOOST_TEST(result == server_error::invalid_identifier);
-    BOOST_TEST(message["error"].template get<int>() == server_error::invalid_identifier);
-    BOOST_TEST(message["errorCategory"].template get<std::string>() == "server");
-}
-
-BOOST_AUTO_TEST_CASE(invalid_identifier_2)
-{
-    boost::system::error_code result;
-    nlohmann::json message;
-
-    ctl_->send({
-        { "command",    "server-invite" },
-        { "server",     ""              },
-        { "target",     "francis"       },
-        { "channel",    "#music"        }
-    });
-    ctl_->recv([&] (auto rresult, auto rmessage) {
-        result = rresult;
-        message = rmessage;
-    });
-
-    wait_for([&] {
-        return result;
-    });
-
-    BOOST_TEST(result == server_error::invalid_identifier);
-    BOOST_TEST(message["error"].template get<int>() == server_error::invalid_identifier);
-    BOOST_TEST(message["errorCategory"].template get<std::string>() == "server");
-}
-
-BOOST_AUTO_TEST_CASE(invalid_nickname_1)
-{
-    boost::system::error_code result;
-    nlohmann::json message;
-
-    ctl_->send({
-        { "command",    "server-invite" },
-        { "server",     "test"          },
-        { "target",     ""              },
-        { "channel",    "#music"        }
-    });
-    ctl_->recv([&] (auto rresult, auto rmessage) {
-        result = rresult;
-        message = rmessage;
-    });
-
-    wait_for([&] {
-        return result;
-    });
-
-    BOOST_TEST(result == server_error::invalid_nickname);
-    BOOST_TEST(message["error"].template get<int>() == server_error::invalid_nickname);
-    BOOST_TEST(message["errorCategory"].template get<std::string>() == "server");
-}
-
-BOOST_AUTO_TEST_CASE(invalid_nickname_2)
-{
-    boost::system::error_code result;
-    nlohmann::json message;
-
-    ctl_->send({
-        { "command",    "server-invite" },
-        { "server",     "test"          },
-        { "target",     123456          },
-        { "channel",    "#music"        }
-    });
-    ctl_->recv([&] (auto rresult, auto rmessage) {
-        result = rresult;
-        message = rmessage;
-    });
-
-    wait_for([&] {
-        return result;
-    });
-
-    BOOST_TEST(result == server_error::invalid_nickname);
-    BOOST_TEST(message["error"].template get<int>() == server_error::invalid_nickname);
-    BOOST_TEST(message["errorCategory"].template get<std::string>() == "server");
-}
-
-BOOST_AUTO_TEST_CASE(invalid_channel_1)
-{
-    boost::system::error_code result;
-    nlohmann::json message;
-
-    ctl_->send({
-        { "command",    "server-invite" },
-        { "server",     "test"          },
-        { "target",     "jean"          },
-        { "channel",    ""              }
-    });
-    ctl_->recv([&] (auto rresult, auto rmessage) {
-        result = rresult;
-        message = rmessage;
-    });
-
-    wait_for([&] {
-        return result;
-    });
-
-    BOOST_TEST(result == server_error::invalid_channel);
-    BOOST_TEST(message["error"].template get<int>() == server_error::invalid_channel);
-    BOOST_TEST(message["errorCategory"].template get<std::string>() == "server");
-}
-
-BOOST_AUTO_TEST_CASE(invalid_channel_2)
-{
-    boost::system::error_code result;
-    nlohmann::json message;
-
-    ctl_->send({
-        { "command",    "server-invite" },
-        { "server",     "test"          },
-        { "target",     "jean"          },
-        { "channel",    123456          }
-    });
-    ctl_->recv([&] (auto rresult, auto rmessage) {
-        result = rresult;
-        message = rmessage;
-    });
-
-    wait_for([&] {
-        return result;
-    });
-
-    BOOST_TEST(result == server_error::invalid_channel);
-    BOOST_TEST(message["error"].template get<int>() == server_error::invalid_channel);
-    BOOST_TEST(message["errorCategory"].template get<std::string>() == "server");
-}
-
-BOOST_AUTO_TEST_CASE(not_found)
-{
-    boost::system::error_code result;
-    nlohmann::json message;
-
-    ctl_->send({
-        { "command",    "server-invite" },
-        { "server",     "unknown"       },
-        { "target",     "francis"       },
-        { "channel",    "#music"        }
-    });
-    ctl_->recv([&] (auto rresult, auto rmessage) {
-        result = rresult;
-        message = rmessage;
-    });
-
-    wait_for([&] {
-        return result;
-    });
-
-    BOOST_TEST(result == server_error::not_found);
-    BOOST_TEST(message["error"].template get<int>() == server_error::not_found);
-    BOOST_TEST(message["errorCategory"].template get<std::string>() == "server");
-}
-
-BOOST_AUTO_TEST_SUITE_END()
-
-BOOST_AUTO_TEST_SUITE_END()
-
-} // !irccd
--- a/tests/src/server-join-command/CMakeLists.txt	Thu Dec 14 21:51:22 2017 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,23 +0,0 @@
-#
-# CMakeLists.txt -- CMake build system for irccd
-#
-# Copyright (c) 2013-2017 David Demelier <markand@malikania.fr>
-#
-# Permission to use, copy, modify, and/or distribute this software for any
-# purpose with or without fee is hereby granted, provided that the above
-# copyright notice and this permission notice appear in all copies.
-#
-# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
-# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
-# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
-# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
-# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
-# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
-# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
-#
-
-irccd_define_test(
-    NAME server-join-command
-    SOURCES main.cpp
-    LIBRARIES libirccd libirccdctl
-)
--- a/tests/src/server-join-command/main.cpp	Thu Dec 14 21:51:22 2017 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,210 +0,0 @@
-/*
- * main.cpp -- test server-join remote command
- *
- * Copyright (c) 2013-2017 David Demelier <markand@malikania.fr>
- *
- * Permission to use, copy, modify, and/or distribute this software for any
- * purpose with or without fee is hereby granted, provided that the above
- * copyright notice and this permission notice appear in all copies.
- *
- * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
- * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
- * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
- * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
- * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
- * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
- * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
- */
-
-#define BOOST_TEST_MODULE "server-join"
-#include <boost/test/unit_test.hpp>
-
-#include <irccd/daemon/server_service.hpp>
-
-#include <irccd/test/command_test.hpp>
-#include <irccd/test/journal_server.hpp>
-
-namespace irccd {
-
-namespace {
-
-class server_join_test : public command_test<server_join_command> {
-protected:
-    std::shared_ptr<journal_server> server_{new journal_server(service_, "test")};
-
-    server_join_test()
-    {
-        daemon_->servers().add(server_);
-    }
-};
-
-} // !namespace
-
-BOOST_FIXTURE_TEST_SUITE(server_join_test_suite, server_join_test)
-
-BOOST_AUTO_TEST_CASE(basic)
-{
-    ctl_->send({
-        { "command",    "server-join"       },
-        { "server",     "test"              },
-        { "channel",    "#music"            },
-        { "password",   "plop"              }
-    });
-
-    wait_for([this] () {
-        return !server_->cqueue().empty();
-    });
-
-    auto cmd = server_->cqueue().back();
-
-    BOOST_TEST(cmd["command"].get<std::string>() == "join");
-    BOOST_TEST(cmd["channel"].get<std::string>() == "#music");
-    BOOST_TEST(cmd["password"].get<std::string>() == "plop");
-}
-
-BOOST_AUTO_TEST_CASE(nopassword)
-{
-    ctl_->send({
-        { "command",    "server-join"       },
-        { "server",     "test"              },
-        { "channel",    "#music"            }
-    });
-
-    wait_for([this] () {
-        return !server_->cqueue().empty();
-    });
-
-    auto cmd = server_->cqueue().back();
-
-    BOOST_TEST(cmd["command"].get<std::string>() == "join");
-    BOOST_TEST(cmd["channel"].get<std::string>() == "#music");
-    BOOST_TEST(cmd["password"].get<std::string>() == "");
-}
-
-BOOST_AUTO_TEST_SUITE(errors)
-
-BOOST_AUTO_TEST_CASE(invalid_identifier_1)
-{
-    boost::system::error_code result;
-    nlohmann::json message;
-
-    ctl_->send({
-        { "command",    "server-join"   },
-        { "server",     123456          },
-        { "channel",    "#music"        }
-    });
-    ctl_->recv([&] (auto rresult, auto rmessage) {
-        result = rresult;
-        message = rmessage;
-    });
-
-    wait_for([&] {
-        return result;
-    });
-
-    BOOST_TEST(result == server_error::invalid_identifier);
-    BOOST_TEST(message["error"].template get<int>() == server_error::invalid_identifier);
-    BOOST_TEST(message["errorCategory"].template get<std::string>() == "server");
-}
-
-BOOST_AUTO_TEST_CASE(invalid_identifier_2)
-{
-    boost::system::error_code result;
-    nlohmann::json message;
-
-    ctl_->send({
-        { "command",    "server-join"   },
-        { "server",     ""              },
-        { "channel",    "#music"        }
-    });
-    ctl_->recv([&] (auto rresult, auto rmessage) {
-        result = rresult;
-        message = rmessage;
-    });
-
-    wait_for([&] {
-        return result;
-    });
-
-    BOOST_TEST(result == server_error::invalid_identifier);
-    BOOST_TEST(message["error"].template get<int>() == server_error::invalid_identifier);
-    BOOST_TEST(message["errorCategory"].template get<std::string>() == "server");
-}
-
-BOOST_AUTO_TEST_CASE(invalid_channel_1)
-{
-    boost::system::error_code result;
-    nlohmann::json message;
-
-    ctl_->send({
-        { "command",    "server-join"   },
-        { "server",     "test"          },
-        { "channel",    ""              }
-    });
-    ctl_->recv([&] (auto rresult, auto rmessage) {
-        result = rresult;
-        message = rmessage;
-    });
-
-    wait_for([&] {
-        return result;
-    });
-
-    BOOST_TEST(result == server_error::invalid_channel);
-    BOOST_TEST(message["error"].template get<int>() == server_error::invalid_channel);
-    BOOST_TEST(message["errorCategory"].template get<std::string>() == "server");
-}
-
-BOOST_AUTO_TEST_CASE(invalid_channel_2)
-{
-    boost::system::error_code result;
-    nlohmann::json message;
-
-    ctl_->send({
-        { "command",    "server-join"   },
-        { "server",     "test"          },
-        { "channel",    123456          }
-    });
-    ctl_->recv([&] (auto rresult, auto rmessage) {
-        result = rresult;
-        message = rmessage;
-    });
-
-    wait_for([&] {
-        return result;
-    });
-
-    BOOST_TEST(result == server_error::invalid_channel);
-    BOOST_TEST(message["error"].template get<int>() == server_error::invalid_channel);
-    BOOST_TEST(message["errorCategory"].template get<std::string>() == "server");
-}
-
-BOOST_AUTO_TEST_CASE(not_found)
-{
-    boost::system::error_code result;
-    nlohmann::json message;
-
-    ctl_->send({
-        { "command",    "server-join"   },
-        { "server",     "unknown"       },
-        { "channel",    "#music"        }
-    });
-    ctl_->recv([&] (auto rresult, auto rmessage) {
-        result = rresult;
-        message = rmessage;
-    });
-
-    wait_for([&] {
-        return result;
-    });
-
-    BOOST_TEST(result == server_error::not_found);
-    BOOST_TEST(message["error"].template get<int>() == server_error::not_found);
-    BOOST_TEST(message["errorCategory"].template get<std::string>() == "server");
-}
-
-BOOST_AUTO_TEST_SUITE_END()
-
-BOOST_AUTO_TEST_SUITE_END()
-
-} // !irccd
--- a/tests/src/server-kick-command/CMakeLists.txt	Thu Dec 14 21:51:22 2017 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,23 +0,0 @@
-#
-# CMakeLists.txt -- CMake build system for irccd
-#
-# Copyright (c) 2013-2017 David Demelier <markand@malikania.fr>
-#
-# Permission to use, copy, modify, and/or distribute this software for any
-# purpose with or without fee is hereby granted, provided that the above
-# copyright notice and this permission notice appear in all copies.
-#
-# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
-# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
-# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
-# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
-# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
-# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
-# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
-#
-
-irccd_define_test(
-    NAME server-kick-command
-    SOURCES main.cpp
-    LIBRARIES libirccd libirccdctl
-)
--- a/tests/src/server-kick-command/main.cpp	Thu Dec 14 21:51:22 2017 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,269 +0,0 @@
-/*
- * main.cpp -- test server-kick remote command
- *
- * Copyright (c) 2013-2017 David Demelier <markand@malikania.fr>
- *
- * Permission to use, copy, modify, and/or distribute this software for any
- * purpose with or without fee is hereby granted, provided that the above
- * copyright notice and this permission notice appear in all copies.
- *
- * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
- * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
- * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
- * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
- * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
- * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
- * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
- */
-
-#define BOOST_TEST_MODULE "server-kick"
-#include <boost/test/unit_test.hpp>
-
-#include <irccd/daemon/server_service.hpp>
-
-#include <irccd/test/command_test.hpp>
-#include <irccd/test/journal_server.hpp>
-
-namespace irccd {
-
-namespace {
-
-class server_kick_test : public command_test<server_kick_command> {
-protected:
-    std::shared_ptr<journal_server> server_{new journal_server(service_, "test")};
-
-    server_kick_test()
-    {
-        daemon_->servers().add(server_);
-    }
-};
-
-} // !namespace
-
-BOOST_FIXTURE_TEST_SUITE(server_kick_test_suite, server_kick_test)
-
-BOOST_AUTO_TEST_CASE(basic)
-{
-    ctl_->send({
-        { "command",    "server-kick"       },
-        { "server",     "test"              },
-        { "target",     "francis"           },
-        { "channel",    "#staff"            },
-        { "reason",     "too noisy"         }
-    });
-
-    wait_for([this] () {
-        return !server_->cqueue().empty();
-    });
-
-    auto cmd = server_->cqueue().back();
-
-    BOOST_TEST(cmd["command"].get<std::string>() == "kick");
-    BOOST_TEST(cmd["channel"].get<std::string>() == "#staff");
-    BOOST_TEST(cmd["target"].get<std::string>() == "francis");
-    BOOST_TEST(cmd["reason"].get<std::string>() == "too noisy");
-}
-
-BOOST_AUTO_TEST_CASE(noreason)
-{
-    ctl_->send({
-        { "command",    "server-kick"       },
-        { "server",     "test"              },
-        { "target",     "francis"           },
-        { "channel",    "#staff"            }
-    });
-
-    wait_for([this] () {
-        return !server_->cqueue().empty();
-    });
-
-    auto cmd = server_->cqueue().back();
-
-    BOOST_TEST(cmd["command"].get<std::string>() == "kick");
-    BOOST_TEST(cmd["channel"].get<std::string>() == "#staff");
-    BOOST_TEST(cmd["target"].get<std::string>() == "francis");
-    BOOST_TEST(cmd["reason"].get<std::string>() == "");
-}
-
-BOOST_AUTO_TEST_SUITE(errors)
-
-BOOST_AUTO_TEST_CASE(invalid_identifier_1)
-{
-    boost::system::error_code result;
-    nlohmann::json message;
-
-    ctl_->send({
-        { "command",    "server-kick"   },
-        { "server",     123456          },
-        { "target",     "francis"       },
-        { "channel",    "#music"        }
-    });
-    ctl_->recv([&] (auto rresult, auto rmessage) {
-        result = rresult;
-        message = rmessage;
-    });
-
-    wait_for([&] {
-        return result;
-    });
-
-    BOOST_TEST(result == server_error::invalid_identifier);
-    BOOST_TEST(message["error"].template get<int>() == server_error::invalid_identifier);
-    BOOST_TEST(message["errorCategory"].template get<std::string>() == "server");
-}
-
-BOOST_AUTO_TEST_CASE(invalid_identifier_2)
-{
-    boost::system::error_code result;
-    nlohmann::json message;
-
-    ctl_->send({
-        { "command",    "server-kick"   },
-        { "server",     ""              },
-        { "target",     "francis"       },
-        { "channel",    "#music"        }
-    });
-    ctl_->recv([&] (auto rresult, auto rmessage) {
-        result = rresult;
-        message = rmessage;
-    });
-
-    wait_for([&] {
-        return result;
-    });
-
-    BOOST_TEST(result == server_error::invalid_identifier);
-    BOOST_TEST(message["error"].template get<int>() == server_error::invalid_identifier);
-    BOOST_TEST(message["errorCategory"].template get<std::string>() == "server");
-}
-
-BOOST_AUTO_TEST_CASE(invalid_nickname_1)
-{
-    boost::system::error_code result;
-    nlohmann::json message;
-
-    ctl_->send({
-        { "command",    "server-kick"   },
-        { "server",     "test"          },
-        { "target",     ""              },
-        { "channel",    "#music"        }
-    });
-    ctl_->recv([&] (auto rresult, auto rmessage) {
-        result = rresult;
-        message = rmessage;
-    });
-
-    wait_for([&] {
-        return result;
-    });
-
-    BOOST_TEST(result == server_error::invalid_nickname);
-    BOOST_TEST(message["error"].template get<int>() == server_error::invalid_nickname);
-    BOOST_TEST(message["errorCategory"].template get<std::string>() == "server");
-}
-
-BOOST_AUTO_TEST_CASE(invalid_nickname_2)
-{
-    boost::system::error_code result;
-    nlohmann::json message;
-
-    ctl_->send({
-        { "command",    "server-kick"   },
-        { "server",     "test"          },
-        { "target",     123456          },
-        { "channel",    "#music"        }
-    });
-    ctl_->recv([&] (auto rresult, auto rmessage) {
-        result = rresult;
-        message = rmessage;
-    });
-
-    wait_for([&] {
-        return result;
-    });
-
-    BOOST_TEST(result == server_error::invalid_nickname);
-    BOOST_TEST(message["error"].template get<int>() == server_error::invalid_nickname);
-    BOOST_TEST(message["errorCategory"].template get<std::string>() == "server");
-}
-
-BOOST_AUTO_TEST_CASE(invalid_channel_1)
-{
-    boost::system::error_code result;
-    nlohmann::json message;
-
-    ctl_->send({
-        { "command",    "server-kick"   },
-        { "server",     "test"          },
-        { "target",     "jean"          },
-        { "channel",    ""              }
-    });
-    ctl_->recv([&] (auto rresult, auto rmessage) {
-        result = rresult;
-        message = rmessage;
-    });
-
-    wait_for([&] {
-        return result;
-    });
-
-    BOOST_TEST(result == server_error::invalid_channel);
-    BOOST_TEST(message["error"].template get<int>() == server_error::invalid_channel);
-    BOOST_TEST(message["errorCategory"].template get<std::string>() == "server");
-}
-
-BOOST_AUTO_TEST_CASE(invalid_channel_2)
-{
-    boost::system::error_code result;
-    nlohmann::json message;
-
-    ctl_->send({
-        { "command",    "server-kick"   },
-        { "server",     "test"          },
-        { "target",     "jean"          },
-        { "channel",    123456          }
-    });
-    ctl_->recv([&] (auto rresult, auto rmessage) {
-        result = rresult;
-        message = rmessage;
-    });
-
-    wait_for([&] {
-        return result;
-    });
-
-    BOOST_TEST(result == server_error::invalid_channel);
-    BOOST_TEST(message["error"].template get<int>() == server_error::invalid_channel);
-    BOOST_TEST(message["errorCategory"].template get<std::string>() == "server");
-}
-
-BOOST_AUTO_TEST_CASE(not_found)
-{
-    boost::system::error_code result;
-    nlohmann::json message;
-
-    ctl_->send({
-        { "command",    "server-kick"   },
-        { "server",     "unknown"       },
-        { "target",     "francis"       },
-        { "channel",    "#music"        }
-    });
-    ctl_->recv([&] (auto rresult, auto rmessage) {
-        result = rresult;
-        message = rmessage;
-    });
-
-    wait_for([&] {
-        return result;
-    });
-
-    BOOST_TEST(result == server_error::not_found);
-    BOOST_TEST(message["error"].template get<int>() == server_error::not_found);
-    BOOST_TEST(message["errorCategory"].template get<std::string>() == "server");
-}
-
-BOOST_AUTO_TEST_SUITE_END()
-
-BOOST_AUTO_TEST_SUITE_END()
-
-} // !irccd
--- a/tests/src/server-list-command/CMakeLists.txt	Thu Dec 14 21:51:22 2017 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,23 +0,0 @@
-#
-# CMakeLists.txt -- CMake build system for irccd
-#
-# Copyright (c) 2013-2017 David Demelier <markand@malikania.fr>
-#
-# Permission to use, copy, modify, and/or distribute this software for any
-# purpose with or without fee is hereby granted, provided that the above
-# copyright notice and this permission notice appear in all copies.
-#
-# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
-# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
-# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
-# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
-# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
-# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
-# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
-#
-
-irccd_define_test(
-    NAME server-list-command
-    SOURCES main.cpp
-    LIBRARIES libirccd libirccdctl
-)
--- a/tests/src/server-list-command/main.cpp	Thu Dec 14 21:51:22 2017 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,66 +0,0 @@
-/*
- * main.cpp -- test server-list remote command
- *
- * Copyright (c) 2013-2017 David Demelier <markand@malikania.fr>
- *
- * Permission to use, copy, modify, and/or distribute this software for any
- * purpose with or without fee is hereby granted, provided that the above
- * copyright notice and this permission notice appear in all copies.
- *
- * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
- * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
- * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
- * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
- * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
- * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
- * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
- */
-
-#define BOOST_TEST_MODULE "server-list"
-#include <boost/test/unit_test.hpp>
-
-#include <irccd/daemon/server_service.hpp>
-
-#include <irccd/test/command_test.hpp>
-#include <irccd/test/journal_server.hpp>
-
-namespace irccd {
-
-namespace {
-
-class server_list_test : public command_test<server_list_command> {
-protected:
-    server_list_test()
-    {
-        daemon_->servers().add(std::make_unique<journal_server>(service_, "s1"));
-        daemon_->servers().add(std::make_unique<journal_server>(service_, "s2"));
-    }
-};
-
-} // !namespace
-
-BOOST_FIXTURE_TEST_SUITE(server_list_test_suite, server_list_test)
-
-BOOST_AUTO_TEST_CASE(basic)
-{
-    nlohmann::json result;
-
-    ctl_->send({{"command", "server-list"}});
-    ctl_->recv([&] (auto, auto msg) {
-        result = msg;
-    });
-
-    wait_for([&] () {
-        return result.is_object();
-    });
-
-    BOOST_TEST(result.is_object());
-    BOOST_TEST(result["list"].is_array());
-    BOOST_TEST(result["list"].size() == 2U);
-    BOOST_TEST(result["list"][0].get<std::string>() == "s1");
-    BOOST_TEST(result["list"][1].get<std::string>() == "s2");
-}
-
-BOOST_AUTO_TEST_SUITE_END()
-
-} // !irccd
--- a/tests/src/server-me-command/CMakeLists.txt	Thu Dec 14 21:51:22 2017 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,23 +0,0 @@
-#
-# CMakeLists.txt -- CMake build system for irccd
-#
-# Copyright (c) 2013-2017 David Demelier <markand@malikania.fr>
-#
-# Permission to use, copy, modify, and/or distribute this software for any
-# purpose with or without fee is hereby granted, provided that the above
-# copyright notice and this permission notice appear in all copies.
-#
-# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
-# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
-# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
-# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
-# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
-# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
-# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
-#
-
-irccd_define_test(
-    NAME server-me-command
-    SOURCES main.cpp
-    LIBRARIES libirccd libirccdctl
-)
--- a/tests/src/server-me-command/main.cpp	Thu Dec 14 21:51:22 2017 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,196 +0,0 @@
-/*
- * main.cpp -- test server-me remote command
- *
- * Copyright (c) 2013-2017 David Demelier <markand@malikania.fr>
- *
- * Permission to use, copy, modify, and/or distribute this software for any
- * purpose with or without fee is hereby granted, provided that the above
- * copyright notice and this permission notice appear in all copies.
- *
- * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
- * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
- * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
- * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
- * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
- * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
- * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
- */
-
-#define BOOST_TEST_MODULE "server-me"
-#include <boost/test/unit_test.hpp>
-
-#include <irccd/daemon/server_service.hpp>
-
-#include <irccd/test/command_test.hpp>
-#include <irccd/test/journal_server.hpp>
-
-namespace irccd {
-
-namespace {
-
-class server_me_test : public command_test<server_me_command> {
-protected:
-    std::shared_ptr<journal_server> server_{new journal_server(service_, "test")};
-
-    server_me_test()
-    {
-        daemon_->servers().add(server_);
-    }
-};
-
-} // !namespace
-
-BOOST_FIXTURE_TEST_SUITE(server_me_test_suite, server_me_test)
-
-BOOST_AUTO_TEST_CASE(basic)
-{
-    ctl_->send({
-        { "command",    "server-me"         },
-        { "server",     "test"              },
-        { "target",     "jean"              },
-        { "message",    "hello!"            }
-    });
-
-    wait_for([this] () {
-        return !server_->cqueue().empty();
-    });
-
-    auto cmd = server_->cqueue().back();
-
-    BOOST_TEST(cmd["command"].get<std::string>() == "me");
-    BOOST_TEST(cmd["message"].get<std::string>() == "hello!");
-    BOOST_TEST(cmd["target"].get<std::string>() == "jean");
-}
-
-BOOST_AUTO_TEST_SUITE(errors)
-
-BOOST_AUTO_TEST_CASE(invalid_identifier_1)
-{
-    boost::system::error_code result;
-    nlohmann::json message;
-
-    ctl_->send({
-        { "command",    "server-me" },
-        { "server",     123456      },
-        { "target",     "#music"    },
-        { "message",    "hello!"    }
-    });
-    ctl_->recv([&] (auto rresult, auto rmessage) {
-        result = rresult;
-        message = rmessage;
-    });
-
-    wait_for([&] {
-        return result;
-    });
-
-    BOOST_TEST(result == server_error::invalid_identifier);
-    BOOST_TEST(message["error"].template get<int>() == server_error::invalid_identifier);
-    BOOST_TEST(message["errorCategory"].template get<std::string>() == "server");
-}
-
-BOOST_AUTO_TEST_CASE(invalid_identifier_2)
-{
-    boost::system::error_code result;
-    nlohmann::json message;
-
-    ctl_->send({
-        { "command",    "server-me" },
-        { "server",     ""          },
-        { "target",     "#music"    },
-        { "message",    "hello!"    }
-    });
-    ctl_->recv([&] (auto rresult, auto rmessage) {
-        result = rresult;
-        message = rmessage;
-    });
-
-    wait_for([&] {
-        return result;
-    });
-
-    BOOST_TEST(result == server_error::invalid_identifier);
-    BOOST_TEST(message["error"].template get<int>() == server_error::invalid_identifier);
-    BOOST_TEST(message["errorCategory"].template get<std::string>() == "server");
-}
-
-BOOST_AUTO_TEST_CASE(invalid_channel_1)
-{
-    boost::system::error_code result;
-    nlohmann::json message;
-
-    ctl_->send({
-        { "command",    "server-me" },
-        { "server",     "test"      },
-        { "target",     ""          },
-        { "message",    "hello!"    }
-    });
-    ctl_->recv([&] (auto rresult, auto rmessage) {
-        result = rresult;
-        message = rmessage;
-    });
-
-    wait_for([&] {
-        return result;
-    });
-
-    BOOST_TEST(result == server_error::invalid_channel);
-    BOOST_TEST(message["error"].template get<int>() == server_error::invalid_channel);
-    BOOST_TEST(message["errorCategory"].template get<std::string>() == "server");
-}
-
-BOOST_AUTO_TEST_CASE(invalid_channel_2)
-{
-    boost::system::error_code result;
-    nlohmann::json message;
-
-    ctl_->send({
-        { "command",    "server-me" },
-        { "server",     "test"      },
-        { "target",     123456      },
-        { "message",    "hello!"    }
-    });
-    ctl_->recv([&] (auto rresult, auto rmessage) {
-        result = rresult;
-        message = rmessage;
-    });
-
-    wait_for([&] {
-        return result;
-    });
-
-    BOOST_TEST(result == server_error::invalid_channel);
-    BOOST_TEST(message["error"].template get<int>() == server_error::invalid_channel);
-    BOOST_TEST(message["errorCategory"].template get<std::string>() == "server");
-}
-
-BOOST_AUTO_TEST_CASE(not_found)
-{
-    boost::system::error_code result;
-    nlohmann::json message;
-
-    ctl_->send({
-        { "command",    "server-me" },
-        { "server",     "unknown"   },
-        { "target",     "#music"    },
-        { "message",    "hello!"    }
-    });
-    ctl_->recv([&] (auto rresult, auto rmessage) {
-        result = rresult;
-        message = rmessage;
-    });
-
-    wait_for([&] {
-        return result;
-    });
-
-    BOOST_TEST(result == server_error::not_found);
-    BOOST_TEST(message["error"].template get<int>() == server_error::not_found);
-    BOOST_TEST(message["errorCategory"].template get<std::string>() == "server");
-}
-
-BOOST_AUTO_TEST_SUITE_END()
-
-BOOST_AUTO_TEST_SUITE_END()
-
-} // !irccd
--- a/tests/src/server-message-command/CMakeLists.txt	Thu Dec 14 21:51:22 2017 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,23 +0,0 @@
-#
-# CMakeLists.txt -- CMake build system for irccd
-#
-# Copyright (c) 2013-2017 David Demelier <markand@malikania.fr>
-#
-# Permission to use, copy, modify, and/or distribute this software for any
-# purpose with or without fee is hereby granted, provided that the above
-# copyright notice and this permission notice appear in all copies.
-#
-# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
-# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
-# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
-# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
-# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
-# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
-# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
-#
-
-irccd_define_test(
-    NAME server-message-command
-    SOURCES main.cpp
-    LIBRARIES libirccd libirccdctl
-)
--- a/tests/src/server-message-command/main.cpp	Thu Dec 14 21:51:22 2017 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,196 +0,0 @@
-/*
- * main.cpp -- test server-message remote command
- *
- * Copyright (c) 2013-2017 David Demelier <markand@malikania.fr>
- *
- * Permission to use, copy, modify, and/or distribute this software for any
- * purpose with or without fee is hereby granted, provided that the above
- * copyright notice and this permission notice appear in all copies.
- *
- * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
- * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
- * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
- * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
- * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
- * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
- * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
- */
-
-#define BOOST_TEST_MODULE "server-message"
-#include <boost/test/unit_test.hpp>
-
-#include <irccd/daemon/server_service.hpp>
-
-#include <irccd/test/command_test.hpp>
-#include <irccd/test/journal_server.hpp>
-
-namespace irccd {
-
-namespace {
-
-class server_message_test : public command_test<server_message_command> {
-protected:
-    std::shared_ptr<journal_server> server_{new journal_server(service_, "test")};
-
-    server_message_test()
-    {
-        daemon_->servers().add(server_);
-    }
-};
-
-} // !namespace
-
-BOOST_FIXTURE_TEST_SUITE(server_message_test_suite, server_message_test)
-
-BOOST_AUTO_TEST_CASE(basic)
-{
-    ctl_->send({
-        { "command",    "server-message"    },
-        { "server",     "test"              },
-        { "target",     "#staff"            },
-        { "message",    "plop!"             }
-    });
-
-    wait_for([this] () {
-        return !server_->cqueue().empty();
-    });
-
-    auto cmd = server_->cqueue().back();
-
-    BOOST_TEST(cmd["command"].get<std::string>() == "message");
-    BOOST_TEST(cmd["message"].get<std::string>() == "plop!");
-    BOOST_TEST(cmd["target"].get<std::string>() == "#staff");
-}
-
-BOOST_AUTO_TEST_SUITE(errors)
-
-BOOST_AUTO_TEST_CASE(invalid_identifier_1)
-{
-    boost::system::error_code result;
-    nlohmann::json message;
-
-    ctl_->send({
-        { "command",    "server-message"    },
-        { "server",     123456              },
-        { "target",     "#music"            },
-        { "message",    "plop!"             }
-    });
-    ctl_->recv([&] (auto rresult, auto rmessage) {
-        result = rresult;
-        message = rmessage;
-    });
-
-    wait_for([&] {
-        return result;
-    });
-
-    BOOST_TEST(result == server_error::invalid_identifier);
-    BOOST_TEST(message["error"].template get<int>() == server_error::invalid_identifier);
-    BOOST_TEST(message["errorCategory"].template get<std::string>() == "server");
-}
-
-BOOST_AUTO_TEST_CASE(invalid_identifier_2)
-{
-    boost::system::error_code result;
-    nlohmann::json message;
-
-    ctl_->send({
-        { "command",    "server-message"    },
-        { "server",     ""                  },
-        { "target",     "#music"            },
-        { "message",    "plop!"             }
-    });
-    ctl_->recv([&] (auto rresult, auto rmessage) {
-        result = rresult;
-        message = rmessage;
-    });
-
-    wait_for([&] {
-        return result;
-    });
-
-    BOOST_TEST(result == server_error::invalid_identifier);
-    BOOST_TEST(message["error"].template get<int>() == server_error::invalid_identifier);
-    BOOST_TEST(message["errorCategory"].template get<std::string>() == "server");
-}
-
-BOOST_AUTO_TEST_CASE(invalid_channel_1)
-{
-    boost::system::error_code result;
-    nlohmann::json message;
-
-    ctl_->send({
-        { "command",    "server-message"    },
-        { "server",     "test"              },
-        { "target",     ""                  },
-        { "message",    "plop!"             }
-    });
-    ctl_->recv([&] (auto rresult, auto rmessage) {
-        result = rresult;
-        message = rmessage;
-    });
-
-    wait_for([&] {
-        return result;
-    });
-
-    BOOST_TEST(result == server_error::invalid_channel);
-    BOOST_TEST(message["error"].template get<int>() == server_error::invalid_channel);
-    BOOST_TEST(message["errorCategory"].template get<std::string>() == "server");
-}
-
-BOOST_AUTO_TEST_CASE(invalid_channel_2)
-{
-    boost::system::error_code result;
-    nlohmann::json message;
-
-    ctl_->send({
-        { "command",    "server-message"    },
-        { "server",     "test"              },
-        { "target",     123456              },
-        { "message",    "plop!"             }
-    });
-    ctl_->recv([&] (auto rresult, auto rmessage) {
-        result = rresult;
-        message = rmessage;
-    });
-
-    wait_for([&] {
-        return result;
-    });
-
-    BOOST_TEST(result == server_error::invalid_channel);
-    BOOST_TEST(message["error"].template get<int>() == server_error::invalid_channel);
-    BOOST_TEST(message["errorCategory"].template get<std::string>() == "server");
-}
-
-BOOST_AUTO_TEST_CASE(not_found)
-{
-    boost::system::error_code result;
-    nlohmann::json message;
-
-    ctl_->send({
-        { "command",    "server-message"    },
-        { "server",     "unknown"           },
-        { "target",     "#music"            },
-        { "message",    "plop!"             }
-    });
-    ctl_->recv([&] (auto rresult, auto rmessage) {
-        result = rresult;
-        message = rmessage;
-    });
-
-    wait_for([&] {
-        return result;
-    });
-
-    BOOST_TEST(result == server_error::not_found);
-    BOOST_TEST(message["error"].template get<int>() == server_error::not_found);
-    BOOST_TEST(message["errorCategory"].template get<std::string>() == "server");
-}
-
-BOOST_AUTO_TEST_SUITE_END()
-
-BOOST_AUTO_TEST_SUITE_END()
-
-} // !irccd
--- a/tests/src/server-mode-command/CMakeLists.txt	Thu Dec 14 21:51:22 2017 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,23 +0,0 @@
-#
-# CMakeLists.txt -- CMake build system for irccd
-#
-# Copyright (c) 2013-2017 David Demelier <markand@malikania.fr>
-#
-# Permission to use, copy, modify, and/or distribute this software for any
-# purpose with or without fee is hereby granted, provided that the above
-# copyright notice and this permission notice appear in all copies.
-#
-# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
-# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
-# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
-# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
-# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
-# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
-# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
-#
-
-irccd_define_test(
-    NAME server-mode-command
-    SOURCES main.cpp
-    LIBRARIES libirccd libirccdctl
-)
--- a/tests/src/server-mode-command/main.cpp	Thu Dec 14 21:51:22 2017 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,245 +0,0 @@
-/*
- * main.cpp -- test server-mode remote command
- *
- * Copyright (c) 2013-2017 David Demelier <markand@malikania.fr>
- *
- * Permission to use, copy, modify, and/or distribute this software for any
- * purpose with or without fee is hereby granted, provided that the above
- * copyright notice and this permission notice appear in all copies.
- *
- * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
- * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
- * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
- * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
- * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
- * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
- * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
- */
-
-#define BOOST_TEST_MODULE "server-mode"
-#include <boost/test/unit_test.hpp>
-
-#include <irccd/daemon/server_service.hpp>
-
-#include <irccd/test/command_test.hpp>
-#include <irccd/test/journal_server.hpp>
-
-namespace irccd {
-
-namespace {
-
-class server_mode_test : public command_test<server_mode_command> {
-protected:
-    std::shared_ptr<journal_server> server_{new journal_server(service_, "test")};
-
-    server_mode_test()
-    {
-        daemon_->servers().add(server_);
-    }
-};
-
-} // !namespace
-
-BOOST_FIXTURE_TEST_SUITE(server_mode_test_suite, server_mode_test)
-
-BOOST_AUTO_TEST_CASE(basic)
-{
-    ctl_->send({
-        { "command",    "server-mode"   },
-        { "server",     "test"          },
-        { "channel",    "#irccd"        },
-        { "mode",       "+t"            }
-    });
-
-    wait_for([this] () {
-        return !server_->cqueue().empty();
-    });
-
-    auto cmd = server_->cqueue().back();
-
-    BOOST_TEST(cmd["command"].get<std::string>() == "mode");
-    BOOST_TEST(cmd["channel"].get<std::string>() == "#irccd");
-    BOOST_TEST(cmd["mode"].get<std::string>() == "+t");
-}
-
-BOOST_AUTO_TEST_SUITE(errors)
-
-BOOST_AUTO_TEST_CASE(invalid_identifier_1)
-{
-    boost::system::error_code result;
-    nlohmann::json message;
-
-    ctl_->send({
-        { "command",    "server-mode"   },
-        { "server",     123456          },
-        { "channel",    "#music"        },
-        { "mode",       "+i"            }
-    });
-    ctl_->recv([&] (auto rresult, auto rmessage) {
-        result = rresult;
-        message = rmessage;
-    });
-
-    wait_for([&] {
-        return result;
-    });
-
-    BOOST_TEST(result == server_error::invalid_identifier);
-    BOOST_TEST(message["error"].template get<int>() == server_error::invalid_identifier);
-    BOOST_TEST(message["errorCategory"].template get<std::string>() == "server");
-}
-
-BOOST_AUTO_TEST_CASE(invalid_identifier_2)
-{
-    boost::system::error_code result;
-    nlohmann::json message;
-
-    ctl_->send({
-        { "command",    "server-mode"   },
-        { "server",     ""              },
-        { "channel",    "#music"        },
-        { "mode",       "+i"            }
-    });
-    ctl_->recv([&] (auto rresult, auto rmessage) {
-        result = rresult;
-        message = rmessage;
-    });
-
-    wait_for([&] {
-        return result;
-    });
-
-    BOOST_TEST(result == server_error::invalid_identifier);
-    BOOST_TEST(message["error"].template get<int>() == server_error::invalid_identifier);
-    BOOST_TEST(message["errorCategory"].template get<std::string>() == "server");
-}
-
-BOOST_AUTO_TEST_CASE(invalid_channel_1)
-{
-    boost::system::error_code result;
-    nlohmann::json message;
-
-    ctl_->send({
-        { "command",    "server-mode"   },
-        { "server",     "test"          },
-        { "channel",    ""              },
-        { "mode",       "+i"            }
-    });
-    ctl_->recv([&] (auto rresult, auto rmessage) {
-        result = rresult;
-        message = rmessage;
-    });
-
-    wait_for([&] {
-        return result;
-    });
-
-    BOOST_TEST(result == server_error::invalid_channel);
-    BOOST_TEST(message["error"].template get<int>() == server_error::invalid_channel);
-    BOOST_TEST(message["errorCategory"].template get<std::string>() == "server");
-}
-
-BOOST_AUTO_TEST_CASE(invalid_channel_2)
-{
-    boost::system::error_code result;
-    nlohmann::json message;
-
-    ctl_->send({
-        { "command",    "server-mode"   },
-        { "server",     "test"          },
-        { "channel",    123456          },
-        { "mode",       "+i"            }
-    });
-    ctl_->recv([&] (auto rresult, auto rmessage) {
-        result = rresult;
-        message = rmessage;
-    });
-
-    wait_for([&] {
-        return result;
-    });
-
-    BOOST_TEST(result == server_error::invalid_channel);
-    BOOST_TEST(message["error"].template get<int>() == server_error::invalid_channel);
-    BOOST_TEST(message["errorCategory"].template get<std::string>() == "server");
-}
-
-BOOST_AUTO_TEST_CASE(invalid_mode_1)
-{
-    boost::system::error_code result;
-    nlohmann::json message;
-
-    ctl_->send({
-        { "command",    "server-mode"   },
-        { "server",     "test"          },
-        { "channel",    "#music"        },
-        { "mode",       ""              }
-    });
-    ctl_->recv([&] (auto rresult, auto rmessage) {
-        result = rresult;
-        message = rmessage;
-    });
-
-    wait_for([&] {
-        return result;
-    });
-
-    BOOST_TEST(result == server_error::invalid_mode);
-    BOOST_TEST(message["error"].template get<int>() == server_error::invalid_mode);
-    BOOST_TEST(message["errorCategory"].template get<std::string>() == "server");
-}
-
-BOOST_AUTO_TEST_CASE(invalid_mode_2)
-{
-    boost::system::error_code result;
-    nlohmann::json message;
-
-    ctl_->send({
-        { "command",    "server-mode"   },
-        { "server",     "test"          },
-        { "channel",    "#music"        },
-        { "mode",       123456          }
-    });
-    ctl_->recv([&] (auto rresult, auto rmessage) {
-        result = rresult;
-        message = rmessage;
-    });
-
-    wait_for([&] {
-        return result;
-    });
-
-    BOOST_TEST(result == server_error::invalid_mode);
-    BOOST_TEST(message["error"].template get<int>() == server_error::invalid_mode);
-    BOOST_TEST(message["errorCategory"].template get<std::string>() == "server");
-}
-BOOST_AUTO_TEST_CASE(not_found)
-{
-    boost::system::error_code result;
-    nlohmann::json message;
-
-    ctl_->send({
-        { "command",    "server-mode"   },
-        { "server",     "unknown"       },
-        { "channel",    "#music"        },
-        { "mode",       "+i"            }
-    });
-    ctl_->recv([&] (auto rresult, auto rmessage) {
-        result = rresult;
-        message = rmessage;
-    });
-
-    wait_for([&] {
-        return result;
-    });
-
-    BOOST_TEST(result == server_error::not_found);
-    BOOST_TEST(message["error"].template get<int>() == server_error::not_found);
-    BOOST_TEST(message["errorCategory"].template get<std::string>() == "server");
-}
-
-BOOST_AUTO_TEST_SUITE_END()
-
-BOOST_AUTO_TEST_SUITE_END()
-
-} // !irccd
--- a/tests/src/server-nick-command/CMakeLists.txt	Thu Dec 14 21:51:22 2017 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,23 +0,0 @@
-#
-# CMakeLists.txt -- CMake build system for irccd
-#
-# Copyright (c) 2013-2017 David Demelier <markand@malikania.fr>
-#
-# Permission to use, copy, modify, and/or distribute this software for any
-# purpose with or without fee is hereby granted, provided that the above
-# copyright notice and this permission notice appear in all copies.
-#
-# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
-# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
-# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
-# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
-# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
-# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
-# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
-#
-
-irccd_define_test(
-    NAME server-nick-command
-    SOURCES main.cpp
-    LIBRARIES libirccd libirccdctl
-)
--- a/tests/src/server-nick-command/main.cpp	Thu Dec 14 21:51:22 2017 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,191 +0,0 @@
-/*
- * main.cpp -- test server-nick remote command
- *
- * Copyright (c) 2013-2017 David Demelier <markand@malikania.fr>
- *
- * Permission to use, copy, modify, and/or distribute this software for any
- * purpose with or without fee is hereby granted, provided that the above
- * copyright notice and this permission notice appear in all copies.
- *
- * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
- * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
- * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
- * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
- * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
- * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
- * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
- */
-
-#define BOOST_TEST_MODULE "server-nick"
-#include <boost/test/unit_test.hpp>
-
-#include <irccd/daemon/server_service.hpp>
-
-#include <irccd/test/command_test.hpp>
-#include <irccd/test/journal_server.hpp>
-
-namespace irccd {
-
-namespace {
-
-class server_nick_test : public command_test<server_nick_command> {
-protected:
-    std::shared_ptr<journal_server> server_{new journal_server(service_, "test")};
-
-    server_nick_test()
-    {
-        daemon_->servers().add(server_);
-    }
-};
-
-} // !namespace
-
-BOOST_FIXTURE_TEST_SUITE(server_nick_test_suite, server_nick_test)
-
-BOOST_AUTO_TEST_CASE(basic)
-{
-    nlohmann::json result;
-
-    ctl_->send({
-        { "command",    "server-nick"   },
-        { "server",     "test"          },
-        { "nickname",   "chris"         }
-    });
-    ctl_->recv([&] (auto, auto msg) {
-        result = msg;
-    });
-
-    wait_for([&] () {
-        return result.is_object();
-    });
-
-    BOOST_TEST(result.is_object());
-    BOOST_TEST(server_->nickname() == "chris");
-}
-
-BOOST_AUTO_TEST_SUITE(errors)
-
-BOOST_AUTO_TEST_CASE(invalid_identifier_1)
-{
-    boost::system::error_code result;
-    nlohmann::json message;
-
-    ctl_->send({
-        { "command",    "server-nick"   },
-        { "server",     123456          },
-        { "nickname",   "chris"         }
-    });
-    ctl_->recv([&] (auto rresult, auto rmessage) {
-        result = rresult;
-        message = rmessage;
-    });
-
-    wait_for([&] {
-        return result;
-    });
-
-    BOOST_TEST(result == server_error::invalid_identifier);
-    BOOST_TEST(message["error"].template get<int>() == server_error::invalid_identifier);
-    BOOST_TEST(message["errorCategory"].template get<std::string>() == "server");
-}
-
-BOOST_AUTO_TEST_CASE(invalid_identifier_2)
-{
-    boost::system::error_code result;
-    nlohmann::json message;
-
-    ctl_->send({
-        { "command",    "server-nick"   },
-        { "server",     ""              },
-        { "nickname",   "chris"         }
-    });
-    ctl_->recv([&] (auto rresult, auto rmessage) {
-        result = rresult;
-        message = rmessage;
-    });
-
-    wait_for([&] {
-        return result;
-    });
-
-    BOOST_TEST(result == server_error::invalid_identifier);
-    BOOST_TEST(message["error"].template get<int>() == server_error::invalid_identifier);
-    BOOST_TEST(message["errorCategory"].template get<std::string>() == "server");
-}
-
-BOOST_AUTO_TEST_CASE(invalid_nickname_1)
-{
-    boost::system::error_code result;
-    nlohmann::json message;
-
-    ctl_->send({
-        { "command",    "server-nick"   },
-        { "server",     "test"          },
-        { "nickname",   ""              }
-    });
-    ctl_->recv([&] (auto rresult, auto rmessage) {
-        result = rresult;
-        message = rmessage;
-    });
-
-    wait_for([&] {
-        return result;
-    });
-
-    BOOST_TEST(result == server_error::invalid_nickname);
-    BOOST_TEST(message["error"].template get<int>() == server_error::invalid_nickname);
-    BOOST_TEST(message["errorCategory"].template get<std::string>() == "server");
-}
-
-BOOST_AUTO_TEST_CASE(invalid_nickname_2)
-{
-    boost::system::error_code result;
-    nlohmann::json message;
-
-    ctl_->send({
-        { "command",    "server-nick"   },
-        { "server",     "test"          },
-        { "nickname",   123456          }
-    });
-    ctl_->recv([&] (auto rresult, auto rmessage) {
-        result = rresult;
-        message = rmessage;
-    });
-
-    wait_for([&] {
-        return result;
-    });
-
-    BOOST_TEST(result == server_error::invalid_nickname);
-    BOOST_TEST(message["error"].template get<int>() == server_error::invalid_nickname);
-    BOOST_TEST(message["errorCategory"].template get<std::string>() == "server");
-}
-BOOST_AUTO_TEST_CASE(not_found)
-{
-    boost::system::error_code result;
-    nlohmann::json message;
-
-    ctl_->send({
-        { "command",    "server-nick"   },
-        { "server",     "unknown"       },
-        { "nickname",   "chris"         }
-    });
-    ctl_->recv([&] (auto rresult, auto rmessage) {
-        result = rresult;
-        message = rmessage;
-    });
-
-    wait_for([&] {
-        return result;
-    });
-
-    BOOST_TEST(result == server_error::not_found);
-    BOOST_TEST(message["error"].template get<int>() == server_error::not_found);
-    BOOST_TEST(message["errorCategory"].template get<std::string>() == "server");
-}
-
-BOOST_AUTO_TEST_SUITE_END()
-
-BOOST_AUTO_TEST_SUITE_END()
-
-} // !irccd
--- a/tests/src/server-notice-command/CMakeLists.txt	Thu Dec 14 21:51:22 2017 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,23 +0,0 @@
-#
-# CMakeLists.txt -- CMake build system for irccd
-#
-# Copyright (c) 2013-2017 David Demelier <markand@malikania.fr>
-#
-# Permission to use, copy, modify, and/or distribute this software for any
-# purpose with or without fee is hereby granted, provided that the above
-# copyright notice and this permission notice appear in all copies.
-#
-# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
-# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
-# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
-# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
-# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
-# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
-# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
-#
-
-irccd_define_test(
-    NAME server-notice-command
-    SOURCES main.cpp
-    LIBRARIES libirccd libirccdctl
-)
--- a/tests/src/server-notice-command/main.cpp	Thu Dec 14 21:51:22 2017 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,196 +0,0 @@
-/*
- * main.cpp -- test server-notice remote command
- *
- * Copyright (c) 2013-2017 David Demelier <markand@malikania.fr>
- *
- * Permission to use, copy, modify, and/or distribute this software for any
- * purpose with or without fee is hereby granted, provided that the above
- * copyright notice and this permission notice appear in all copies.
- *
- * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
- * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
- * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
- * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
- * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
- * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
- * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
- */
-
-#define BOOST_TEST_MODULE "server-notice"
-#include <boost/test/unit_test.hpp>
-
-#include <irccd/daemon/server_service.hpp>
-
-#include <irccd/test/command_test.hpp>
-#include <irccd/test/journal_server.hpp>
-
-namespace irccd {
-
-namespace {
-
-class server_notice_test : public command_test<server_notice_command> {
-protected:
-    std::shared_ptr<journal_server> server_{new journal_server(service_, "test")};
-
-    server_notice_test()
-    {
-        daemon_->servers().add(server_);
-    }
-};
-
-} // !namespace
-
-BOOST_FIXTURE_TEST_SUITE(server_notice_test_suite, server_notice_test)
-
-BOOST_AUTO_TEST_CASE(basic)
-{
-    ctl_->send({
-        { "command",    "server-notice" },
-        { "server",     "test"          },
-        { "target",     "#staff"        },
-        { "message",    "quiet!"        }
-    });
-
-    wait_for([this] () {
-        return !server_->cqueue().empty();
-    });
-
-    auto cmd = server_->cqueue().back();
-
-    BOOST_TEST(cmd["command"].get<std::string>() == "notice");
-    BOOST_TEST(cmd["message"].get<std::string>() == "quiet!");
-    BOOST_TEST(cmd["target"].get<std::string>() == "#staff");
-}
-
-BOOST_AUTO_TEST_SUITE(errors)
-
-BOOST_AUTO_TEST_CASE(invalid_identifier_1)
-{
-    boost::system::error_code result;
-    nlohmann::json message;
-
-    ctl_->send({
-        { "command",    "server-notice" },
-        { "server",     123456          },
-        { "target",     "#music"        },
-        { "message",    "quiet!"        }
-    });
-    ctl_->recv([&] (auto rresult, auto rmessage) {
-        result = rresult;
-        message = rmessage;
-    });
-
-    wait_for([&] {
-        return result;
-    });
-
-    BOOST_TEST(result == server_error::invalid_identifier);
-    BOOST_TEST(message["error"].template get<int>() == server_error::invalid_identifier);
-    BOOST_TEST(message["errorCategory"].template get<std::string>() == "server");
-}
-
-BOOST_AUTO_TEST_CASE(invalid_identifier_2)
-{
-    boost::system::error_code result;
-    nlohmann::json message;
-
-    ctl_->send({
-        { "command",    "server-notice" },
-        { "server",     ""              },
-        { "target",     "#music"        },
-        { "message",    "quiet!"        }
-    });
-    ctl_->recv([&] (auto rresult, auto rmessage) {
-        result = rresult;
-        message = rmessage;
-    });
-
-    wait_for([&] {
-        return result;
-    });
-
-    BOOST_TEST(result == server_error::invalid_identifier);
-    BOOST_TEST(message["error"].template get<int>() == server_error::invalid_identifier);
-    BOOST_TEST(message["errorCategory"].template get<std::string>() == "server");
-}
-
-BOOST_AUTO_TEST_CASE(invalid_channel_1)
-{
-    boost::system::error_code result;
-    nlohmann::json message;
-
-    ctl_->send({
-        { "command",    "server-notice" },
-        { "server",     "test"          },
-        { "target",     ""              },
-        { "message",    "quiet!"        }
-    });
-    ctl_->recv([&] (auto rresult, auto rmessage) {
-        result = rresult;
-        message = rmessage;
-    });
-
-    wait_for([&] {
-        return result;
-    });
-
-    BOOST_TEST(result == server_error::invalid_channel);
-    BOOST_TEST(message["error"].template get<int>() == server_error::invalid_channel);
-    BOOST_TEST(message["errorCategory"].template get<std::string>() == "server");
-}
-
-BOOST_AUTO_TEST_CASE(invalid_channel_2)
-{
-    boost::system::error_code result;
-    nlohmann::json message;
-
-    ctl_->send({
-        { "command",    "server-notice" },
-        { "server",     "test"          },
-        { "target",     123456          },
-        { "message",    "quiet!"        }
-    });
-    ctl_->recv([&] (auto rresult, auto rmessage) {
-        result = rresult;
-        message = rmessage;
-    });
-
-    wait_for([&] {
-        return result;
-    });
-
-    BOOST_TEST(result == server_error::invalid_channel);
-    BOOST_TEST(message["error"].template get<int>() == server_error::invalid_channel);
-    BOOST_TEST(message["errorCategory"].template get<std::string>() == "server");
-}
-
-BOOST_AUTO_TEST_CASE(not_found)
-{
-    boost::system::error_code result;
-    nlohmann::json message;
-
-    ctl_->send({
-        { "command",    "server-notice" },
-        { "server",     "unknown"       },
-        { "target",     "#music"        },
-        { "message",    "quiet!"        }
-    });
-    ctl_->recv([&] (auto rresult, auto rmessage) {
-        result = rresult;
-        message = rmessage;
-    });
-
-    wait_for([&] {
-        return result;
-    });
-
-    BOOST_TEST(result == server_error::not_found);
-    BOOST_TEST(message["error"].template get<int>() == server_error::not_found);
-    BOOST_TEST(message["errorCategory"].template get<std::string>() == "server");
-}
-
-BOOST_AUTO_TEST_SUITE_END()
-
-BOOST_AUTO_TEST_SUITE_END()
-
-} // !irccd
--- a/tests/src/server-part-command/CMakeLists.txt	Thu Dec 14 21:51:22 2017 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,23 +0,0 @@
-#
-# CMakeLists.txt -- CMake build system for irccd
-#
-# Copyright (c) 2013-2017 David Demelier <markand@malikania.fr>
-#
-# Permission to use, copy, modify, and/or distribute this software for any
-# purpose with or without fee is hereby granted, provided that the above
-# copyright notice and this permission notice appear in all copies.
-#
-# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
-# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
-# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
-# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
-# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
-# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
-# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
-#
-
-irccd_define_test(
-    NAME server-part-command
-    SOURCES main.cpp
-    LIBRARIES libirccd libirccdctl
-)
--- a/tests/src/server-part-command/main.cpp	Thu Dec 14 21:51:22 2017 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,210 +0,0 @@
-/*
- * main.cpp -- test server-part remote command
- *
- * Copyright (c) 2013-2017 David Demelier <markand@malikania.fr>
- *
- * Permission to use, copy, modify, and/or distribute this software for any
- * purpose with or without fee is hereby granted, provided that the above
- * copyright part and this permission part appear in all copies.
- *
- * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
- * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
- * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
- * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
- * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
- * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
- * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
- */
-
-#define BOOST_TEST_MODULE "server-part"
-#include <boost/test/unit_test.hpp>
-
-#include <irccd/daemon/server_service.hpp>
-
-#include <irccd/test/command_test.hpp>
-#include <irccd/test/journal_server.hpp>
-
-namespace irccd {
-
-namespace {
-
-class server_part_test : public command_test<server_part_command> {
-protected:
-    std::shared_ptr<journal_server> server_{new journal_server(service_, "test")};
-
-    server_part_test()
-    {
-        daemon_->servers().add(server_);
-    }
-};
-
-} // !namespace
-
-BOOST_FIXTURE_TEST_SUITE(server_part_test_suite, server_part_test)
-
-BOOST_AUTO_TEST_CASE(basic)
-{
-    ctl_->send({
-        { "command",    "server-part"   },
-        { "server",     "test"          },
-        { "channel",    "#staff"        },
-        { "reason",     "too noisy"     }
-    });
-
-    wait_for([this] () {
-        return !server_->cqueue().empty();
-    });
-
-    auto cmd = server_->cqueue().back();
-
-    BOOST_TEST(cmd["command"].get<std::string>() == "part");
-    BOOST_TEST(cmd["channel"].get<std::string>() == "#staff");
-    BOOST_TEST(cmd["reason"].get<std::string>() == "too noisy");
-}
-
-BOOST_AUTO_TEST_CASE(noreason)
-{
-    ctl_->send({
-        { "command",    "server-part"   },
-        { "server",     "test"          },
-        { "channel",    "#staff"        }
-    });
-
-    wait_for([this] () {
-        return !server_->cqueue().empty();
-    });
-
-    auto cmd = server_->cqueue().back();
-
-    BOOST_TEST(cmd["command"].get<std::string>() == "part");
-    BOOST_TEST(cmd["channel"].get<std::string>() == "#staff");
-    BOOST_TEST(cmd["reason"].get<std::string>() == "");
-}
-
-BOOST_AUTO_TEST_SUITE(errors)
-
-BOOST_AUTO_TEST_CASE(invalid_identifier_1)
-{
-    boost::system::error_code result;
-    nlohmann::json message;
-
-    ctl_->send({
-        { "command",    "server-part"   },
-        { "server",     123456          },
-        { "channel",    "#music"        }
-    });
-    ctl_->recv([&] (auto rresult, auto rmessage) {
-        result = rresult;
-        message = rmessage;
-    });
-
-    wait_for([&] {
-        return result;
-    });
-
-    BOOST_TEST(result == server_error::invalid_identifier);
-    BOOST_TEST(message["error"].template get<int>() == server_error::invalid_identifier);
-    BOOST_TEST(message["errorCategory"].template get<std::string>() == "server");
-}
-
-BOOST_AUTO_TEST_CASE(invalid_identifier_2)
-{
-    boost::system::error_code result;
-    nlohmann::json message;
-
-    ctl_->send({
-        { "command",    "server-part"   },
-        { "server",     ""              },
-        { "channel",    "#music"        }
-    });
-    ctl_->recv([&] (auto rresult, auto rmessage) {
-        result = rresult;
-        message = rmessage;
-    });
-
-    wait_for([&] {
-        return result;
-    });
-
-    BOOST_TEST(result == server_error::invalid_identifier);
-    BOOST_TEST(message["error"].template get<int>() == server_error::invalid_identifier);
-    BOOST_TEST(message["errorCategory"].template get<std::string>() == "server");
-}
-
-BOOST_AUTO_TEST_CASE(invalid_channel_1)
-{
-    boost::system::error_code result;
-    nlohmann::json message;
-
-    ctl_->send({
-        { "command",    "server-part"   },
-        { "server",     "test"          },
-        { "channel",    ""              }
-    });
-    ctl_->recv([&] (auto rresult, auto rmessage) {
-        result = rresult;
-        message = rmessage;
-    });
-
-    wait_for([&] {
-        return result;
-    });
-
-    BOOST_TEST(result == server_error::invalid_channel);
-    BOOST_TEST(message["error"].template get<int>() == server_error::invalid_channel);
-    BOOST_TEST(message["errorCategory"].template get<std::string>() == "server");
-}
-
-BOOST_AUTO_TEST_CASE(invalid_channel_2)
-{
-    boost::system::error_code result;
-    nlohmann::json message;
-
-    ctl_->send({
-        { "command",    "server-part"   },
-        { "server",     "test"          },
-        { "channel",    123456          }
-    });
-    ctl_->recv([&] (auto rresult, auto rmessage) {
-        result = rresult;
-        message = rmessage;
-    });
-
-    wait_for([&] {
-        return result;
-    });
-
-    BOOST_TEST(result == server_error::invalid_channel);
-    BOOST_TEST(message["error"].template get<int>() == server_error::invalid_channel);
-    BOOST_TEST(message["errorCategory"].template get<std::string>() == "server");
-}
-
-BOOST_AUTO_TEST_CASE(not_found)
-{
-    boost::system::error_code result;
-    nlohmann::json message;
-
-    ctl_->send({
-        { "command",    "server-part"   },
-        { "server",     "unknown"       },
-        { "channel",    "#music"        }
-    });
-    ctl_->recv([&] (auto rresult, auto rmessage) {
-        result = rresult;
-        message = rmessage;
-    });
-
-    wait_for([&] {
-        return result;
-    });
-
-    BOOST_TEST(result == server_error::not_found);
-    BOOST_TEST(message["error"].template get<int>() == server_error::not_found);
-    BOOST_TEST(message["errorCategory"].template get<std::string>() == "server");
-}
-
-BOOST_AUTO_TEST_SUITE_END()
-
-BOOST_AUTO_TEST_SUITE_END()
-
-} // !irccd
--- a/tests/src/server-reconnect-command/CMakeLists.txt	Thu Dec 14 21:51:22 2017 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,23 +0,0 @@
-#
-# CMakeLists.txt -- CMake build system for irccd
-#
-# Copyright (c) 2013-2017 David Demelier <markand@malikania.fr>
-#
-# Permission to use, copy, modify, and/or distribute this software for any
-# purpose with or without fee is hereby granted, provided that the above
-# copyright notice and this permission notice appear in all copies.
-#
-# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
-# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
-# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
-# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
-# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
-# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
-# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
-#
-
-irccd_define_test(
-    NAME server-reconnect-command
-    SOURCES main.cpp
-    LIBRARIES libirccd libirccdctl
-)
--- a/tests/src/server-reconnect-command/main.cpp	Thu Dec 14 21:51:22 2017 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,154 +0,0 @@
-/*
- * main.cpp -- test server-reconnect remote command
- *
- * Copyright (c) 2013-2017 David Demelier <markand@malikania.fr>
- *
- * Permission to use, copy, modify, and/or distribute this software for any
- * purpose with or without fee is hereby granted, provided that the above
- * copyright notice and this permission notice appear in all copies.
- *
- * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
- * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
- * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
- * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
- * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
- * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
- * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
- */
-
-#define BOOST_TEST_MODULE "server-reconnect"
-#include <boost/test/unit_test.hpp>
-
-#include <irccd/daemon/server_service.hpp>
-
-#include <irccd/test/command_test.hpp>
-#include <irccd/test/journal_server.hpp>
-
-namespace irccd {
-
-namespace {
-
-class server_reconnect_test : public command_test<server_reconnect_command> {
-protected:
-    std::shared_ptr<journal_server> server1_{new journal_server(service_, "s1")};
-    std::shared_ptr<journal_server> server2_{new journal_server(service_, "s2")};
-
-    server_reconnect_test()
-    {
-        daemon_->servers().add(server1_);
-        daemon_->servers().add(server2_);
-    }
-};
-
-} // !namespace
-
-BOOST_FIXTURE_TEST_SUITE(server_reconnect_test_suite, server_reconnect_test)
-
-BOOST_AUTO_TEST_CASE(basic)
-{
-    ctl_->send({
-        { "command",    "server-reconnect"  },
-        { "server",     "s1"                }
-    });
-
-    wait_for([this] () {
-        return !server1_->cqueue().empty();
-    });
-
-    auto cmd1 = server1_->cqueue().back();
-
-    BOOST_TEST(cmd1["command"].get<std::string>() == "reconnect");
-    BOOST_TEST(server2_->cqueue().empty());
-}
-
-BOOST_AUTO_TEST_CASE(all)
-{
-    ctl_->send({{"command", "server-reconnect"}});
-
-    wait_for([this] () {
-        return !server1_->cqueue().empty() && !server2_->cqueue().empty();
-    });
-
-    auto cmd1 = server1_->cqueue().back();
-    auto cmd2 = server2_->cqueue().back();
-
-    BOOST_TEST(cmd1["command"].get<std::string>() == "reconnect");
-    BOOST_TEST(cmd2["command"].get<std::string>() == "reconnect");
-}
-
-BOOST_AUTO_TEST_SUITE(errors)
-
-BOOST_AUTO_TEST_CASE(invalid_identifier_1)
-{
-    boost::system::error_code result;
-    nlohmann::json message;
-
-    ctl_->send({
-        { "command",    "server-reconnect"  },
-        { "server",     123456              }
-    });
-    ctl_->recv([&] (auto rresult, auto rmessage) {
-        result = rresult;
-        message = rmessage;
-    });
-
-    wait_for([&] {
-        return result;
-    });
-
-    BOOST_TEST(result == server_error::invalid_identifier);
-    BOOST_TEST(message["error"].template get<int>() == server_error::invalid_identifier);
-    BOOST_TEST(message["errorCategory"].template get<std::string>() == "server");
-}
-
-BOOST_AUTO_TEST_CASE(invalid_identifier_2)
-{
-    boost::system::error_code result;
-    nlohmann::json message;
-
-    ctl_->send({
-        { "command",    "server-reconnect"  },
-        { "server",     ""                  }
-    });
-    ctl_->recv([&] (auto rresult, auto rmessage) {
-        result = rresult;
-        message = rmessage;
-    });
-
-    wait_for([&] {
-        return result;
-    });
-
-    BOOST_TEST(result == server_error::invalid_identifier);
-    BOOST_TEST(message["error"].template get<int>() == server_error::invalid_identifier);
-    BOOST_TEST(message["errorCategory"].template get<std::string>() == "server");
-}
-
-BOOST_AUTO_TEST_CASE(not_found)
-{
-    boost::system::error_code result;
-    nlohmann::json message;
-
-    ctl_->send({
-        { "command",    "server-reconnect"  },
-        { "server",     "unknown"           }
-    });
-    ctl_->recv([&] (auto rresult, auto rmessage) {
-        result = rresult;
-        message = rmessage;
-    });
-
-    wait_for([&] {
-        return result;
-    });
-
-    BOOST_TEST(result == server_error::not_found);
-    BOOST_TEST(message["error"].template get<int>() == server_error::not_found);
-    BOOST_TEST(message["errorCategory"].template get<std::string>() == "server");
-}
-
-BOOST_AUTO_TEST_SUITE_END()
-
-BOOST_AUTO_TEST_SUITE_END()
-
-} // !irccd
--- a/tests/src/server-topic-command/CMakeLists.txt	Thu Dec 14 21:51:22 2017 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,23 +0,0 @@
-#
-# CMakeLists.txt -- CMake build system for irccd
-#
-# Copyright (c) 2013-2017 David Demelier <markand@malikania.fr>
-#
-# Permission to use, copy, modify, and/or distribute this software for any
-# purpose with or without fee is hereby granted, provided that the above
-# copyright notice and this permission notice appear in all copies.
-#
-# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
-# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
-# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
-# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
-# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
-# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
-# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
-#
-
-irccd_define_test(
-    NAME server-topic-command
-    SOURCES main.cpp
-    LIBRARIES libirccd libirccdctl
-)
--- a/tests/src/server-topic-command/main.cpp	Thu Dec 14 21:51:22 2017 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,196 +0,0 @@
-/*
- * main.cpp -- test server-topic remote command
- *
- * Copyright (c) 2013-2017 David Demelier <markand@malikania.fr>
- *
- * Permission to use, copy, modify, and/or distribute this software for any
- * purpose with or without fee is hereby granted, provided that the above
- * copyright notice and this permission notice appear in all copies.
- *
- * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
- * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
- * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
- * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
- * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
- * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
- * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
- */
-
-#define BOOST_TEST_MODULE "server-topic"
-#include <boost/test/unit_test.hpp>
-
-#include <irccd/daemon/server_service.hpp>
-
-#include <irccd/test/command_test.hpp>
-#include <irccd/test/journal_server.hpp>
-
-namespace irccd {
-
-namespace {
-
-class server_topic_test : public command_test<server_topic_command> {
-protected:
-    std::shared_ptr<journal_server> server_{new journal_server(service_, "test")};
-
-    server_topic_test()
-    {
-        daemon_->servers().add(server_);
-    }
-};
-
-} // !namespace
-
-BOOST_FIXTURE_TEST_SUITE(server_topic_test_suite, server_topic_test)
-
-BOOST_AUTO_TEST_CASE(basic)
-{
-    ctl_->send({
-        { "command",    "server-topic"  },
-        { "server",     "test"          },
-        { "channel",    "#staff"        },
-        { "topic",      "new version"   }
-    });
-
-    wait_for([this] () {
-        return !server_->cqueue().empty();
-    });
-
-    auto cmd = server_->cqueue().back();
-
-    BOOST_TEST(cmd["command"].get<std::string>() == "topic");
-    BOOST_TEST(cmd["channel"].get<std::string>() == "#staff");
-    BOOST_TEST(cmd["topic"].get<std::string>() == "new version");
-}
-
-BOOST_AUTO_TEST_SUITE(errors)
-
-BOOST_AUTO_TEST_CASE(invalid_identifier_1)
-{
-    boost::system::error_code result;
-    nlohmann::json message;
-
-    ctl_->send({
-        { "command",    "server-topic"  },
-        { "server",     123456          },
-        { "channel",    "#music"        },
-        { "topic",      "plop"          }
-    });
-    ctl_->recv([&] (auto rresult, auto rmessage) {
-        result = rresult;
-        message = rmessage;
-    });
-
-    wait_for([&] {
-        return result;
-    });
-
-    BOOST_TEST(result == server_error::invalid_identifier);
-    BOOST_TEST(message["error"].template get<int>() == server_error::invalid_identifier);
-    BOOST_TEST(message["errorCategory"].template get<std::string>() == "server");
-}
-
-BOOST_AUTO_TEST_CASE(invalid_identifier_2)
-{
-    boost::system::error_code result;
-    nlohmann::json message;
-
-    ctl_->send({
-        { "command",    "server-topic"  },
-        { "server",     ""              },
-        { "channel",    "#music"        },
-        { "topic",      "plop"          }
-    });
-    ctl_->recv([&] (auto rresult, auto rmessage) {
-        result = rresult;
-        message = rmessage;
-    });
-
-    wait_for([&] {
-        return result;
-    });
-
-    BOOST_TEST(result == server_error::invalid_identifier);
-    BOOST_TEST(message["error"].template get<int>() == server_error::invalid_identifier);
-    BOOST_TEST(message["errorCategory"].template get<std::string>() == "server");
-}
-
-BOOST_AUTO_TEST_CASE(invalid_channel_1)
-{
-    boost::system::error_code result;
-    nlohmann::json message;
-
-    ctl_->send({
-        { "command",    "server-topic"  },
-        { "server",     "test"          },
-        { "channel",    ""              },
-        { "topic",      "plop"          }
-    });
-    ctl_->recv([&] (auto rresult, auto rmessage) {
-        result = rresult;
-        message = rmessage;
-    });
-
-    wait_for([&] {
-        return result;
-    });
-
-    BOOST_TEST(result == server_error::invalid_channel);
-    BOOST_TEST(message["error"].template get<int>() == server_error::invalid_channel);
-    BOOST_TEST(message["errorCategory"].template get<std::string>() == "server");
-}
-
-BOOST_AUTO_TEST_CASE(invalid_channel_2)
-{
-    boost::system::error_code result;
-    nlohmann::json message;
-
-    ctl_->send({
-        { "command",    "server-topic"  },
-        { "server",     "test"          },
-        { "channel",    123456          },
-        { "topic",      "plop"          }
-    });
-    ctl_->recv([&] (auto rresult, auto rmessage) {
-        result = rresult;
-        message = rmessage;
-    });
-
-    wait_for([&] {
-        return result;
-    });
-
-    BOOST_TEST(result == server_error::invalid_channel);
-    BOOST_TEST(message["error"].template get<int>() == server_error::invalid_channel);
-    BOOST_TEST(message["errorCategory"].template get<std::string>() == "server");
-}
-
-BOOST_AUTO_TEST_CASE(not_found)
-{
-    boost::system::error_code result;
-    nlohmann::json message;
-
-    ctl_->send({
-        { "command",    "server-topic"  },
-        { "server",     "unknown"       },
-        { "channel",    "#music"        },
-        { "topic",      "plop"          }
-    });
-    ctl_->recv([&] (auto rresult, auto rmessage) {
-        result = rresult;
-        message = rmessage;
-    });
-
-    wait_for([&] {
-        return result;
-    });
-
-    BOOST_TEST(result == server_error::not_found);
-    BOOST_TEST(message["error"].template get<int>() == server_error::not_found);
-    BOOST_TEST(message["errorCategory"].template get<std::string>() == "server");
-}
-
-BOOST_AUTO_TEST_SUITE_END()
-
-BOOST_AUTO_TEST_SUITE_END()
-
-} // !irccd
--- a/tests/src/system-jsapi/CMakeLists.txt	Thu Dec 14 21:51:22 2017 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,24 +0,0 @@
-#
-# CMakeLists.txt -- CMake build system for irccd
-#
-# Copyright (c) 2013-2017 David Demelier <markand@malikania.fr>
-#
-# Permission to use, copy, modify, and/or distribute this software for any
-# purpose with or without fee is hereby granted, provided that the above
-# copyright notice and this permission notice appear in all copies.
-#
-# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
-# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
-# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
-# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
-# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
-# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
-# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
-#
-
-irccd_define_test(
-    NAME system-jsapi
-    SOURCES main.cpp
-    LIBRARIES libirccd-js
-    FLAGS IRCCD_EXECUTABLE=\"$<TARGET_FILE:irccd>\"
-)
--- a/tests/src/system-jsapi/main.cpp	Thu Dec 14 21:51:22 2017 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,65 +0,0 @@
-/*
- * main.cpp -- test Irccd.System API
- *
- * Copyright (c) 2013-2017 David Demelier <markand@malikania.fr>
- *
- * Permission to use, copy, modify, and/or distribute this software for any
- * purpose with or without fee is hereby granted, provided that the above
- * copyright notice and this permission notice appear in all copies.
- *
- * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
- * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
- * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
- * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
- * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
- * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
- * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
- */
-
-#define BOOST_TEST_MODULE "System Javascript API"
-#include <boost/test/unit_test.hpp>
-
-#include <irccd/system.hpp>
-
-#include <irccd/daemon/irccd.hpp>
-
-#include <irccd/js/file_jsapi.hpp>
-#include <irccd/js/system_jsapi.hpp>
-
-#include <irccd/test/js_test.hpp>
-
-namespace irccd {
-
-using fixture = js_test<file_jsapi, system_jsapi>;
-
-BOOST_FIXTURE_TEST_SUITE(system_jsapi_suite, fixture)
-
-BOOST_AUTO_TEST_CASE(home)
-{
-    duk_peval_string_noresult(plugin_->context(), "result = Irccd.System.home();");
-
-    BOOST_TEST(duk_get_global_string(plugin_->context(),"result"));
-    BOOST_TEST(duk_get_string(plugin_->context(), -1) == sys::home());
-}
-
-#if defined(HAVE_POPEN)
-
-BOOST_AUTO_TEST_CASE(popen)
-{
-    auto ret = duk_peval_string(plugin_->context(),
-        "f = Irccd.System.popen(\"" IRCCD_EXECUTABLE " --version\", \"r\");"
-        "r = f.readline();"
-    );
-
-    if (ret != 0)
-        throw dukx_stack(plugin_->context(), -1);
-
-    BOOST_TEST(duk_get_global_string(plugin_->context(), "r"));
-    BOOST_TEST(duk_get_string(plugin_->context(), -1) == IRCCD_VERSION);
-}
-
-#endif
-
-BOOST_AUTO_TEST_SUITE_END()
-
-} // !irccd
--- a/tests/src/timer-jsapi/CMakeLists.txt	Thu Dec 14 21:51:22 2017 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,23 +0,0 @@
-#
-# CMakeLists.txt -- CMake build system for irccd
-#
-# Copyright (c) 2013-2017 David Demelier <markand@malikania.fr>
-#
-# Permission to use, copy, modify, and/or distribute this software for any
-# purpose with or without fee is hereby granted, provided that the above
-# copyright notice and this permission notice appear in all copies.
-#
-# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
-# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
-# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
-# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
-# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
-# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
-# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
-#
-
-irccd_define_test(
-    NAME timer-jsapi
-    SOURCES main.cpp
-    LIBRARIES libirccd-js
-)
--- a/tests/src/timer-jsapi/main.cpp	Thu Dec 14 21:51:22 2017 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,90 +0,0 @@
-/*
- * main.cpp -- test Irccd.Timer API
- *
- * Copyright (c) 2013-2017 David Demelier <markand@malikania.fr>
- *
- * Permission to use, copy, modify, and/or distribute this software for any
- * purpose with or without fee is hereby granted, provided that the above
- * copyright notice and this permission notice appear in all copies.
- *
- * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
- * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
- * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
- * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
- * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
- * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
- * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
- */
-
-#define BOOST_TEST_MODULE "Timer Javascript API"
-#include <boost/test/unit_test.hpp>
-#include <boost/timer/timer.hpp>
-
-#include <irccd/js/plugin_jsapi.hpp>
-#include <irccd/js/timer_jsapi.hpp>
-
-#include <irccd/test/js_test.hpp>
-
-#include <irccd/js/logger_jsapi.hpp>
-
-namespace irccd {
-
-namespace {
-
-class js_timer_test : public js_test<plugin_jsapi, timer_jsapi> {
-public:
-    js_timer_test()
-        : js_test(CMAKE_CURRENT_SOURCE_DIR "/timer.js")
-    {
-    }
-
-    void set_type(const std::string& name)
-    {
-        duk_get_global_string(plugin_->context(), "Irccd");
-        duk_get_prop_string(plugin_->context(), -1, "Timer");
-        duk_get_prop_string(plugin_->context(), -1, name.c_str());
-        duk_put_global_string(plugin_->context(), "type");
-        duk_pop_n(plugin_->context(), 2);
-
-        plugin_->open();
-        plugin_->on_load(irccd_);
-    }
-};
-
-} // !namespace
-
-BOOST_FIXTURE_TEST_SUITE(js_timer_test_suite, js_timer_test)
-
-BOOST_AUTO_TEST_CASE(single)
-{
-    boost::timer::cpu_timer timer;
-
-    set_type("Single");
-
-    while (timer.elapsed().wall / 1000000LL < 3000) {
-        service_.reset();
-        service_.poll();
-    }
-
-    BOOST_TEST(duk_get_global_string(plugin_->context(), "count"));
-    BOOST_TEST(duk_get_int(plugin_->context(), -1) == 1);
-}
-
-BOOST_AUTO_TEST_CASE(repeat)
-{
-    boost::timer::cpu_timer timer;
-
-    set_type("Repeat");
-
-    while (timer.elapsed().wall / 1000000LL < 3000) {
-        service_.reset();
-        service_.poll();
-    }
-
-    BOOST_TEST(duk_get_global_string(plugin_->context(), "count"));
-    BOOST_TEST(duk_get_int(plugin_->context(), -1) >= 5);
-}
-
-BOOST_AUTO_TEST_SUITE_END()
-
-} // !irccd
--- a/tests/src/timer-jsapi/timer.js	Thu Dec 14 21:51:22 2017 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,13 +0,0 @@
-count = 0;
-
-function onLoad()
-{
-    if (typeof (type) === "undefined")
-        throw Error("global timer type not defined");
-
-    t = new Irccd.Timer(type, 50, function () {
-        count += 1;
-    });
-
-    t.start();
-}
--- a/tests/src/unicode-jsapi/CMakeLists.txt	Thu Dec 14 21:51:22 2017 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,23 +0,0 @@
-#
-# CMakeLists.txt -- CMake build system for irccd
-#
-# Copyright (c) 2013-2017 David Demelier <markand@malikania.fr>
-#
-# Permission to use, copy, modify, and/or distribute this software for any
-# purpose with or without fee is hereby granted, provided that the above
-# copyright notice and this permission notice appear in all copies.
-#
-# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
-# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
-# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
-# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
-# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
-# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
-# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
-#
-
-irccd_define_test(
-    NAME unicode-jsapi
-    SOURCES main.cpp
-    LIBRARIES libirccd-js
-)
--- a/tests/src/unicode-jsapi/main.cpp	Thu Dec 14 21:51:22 2017 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,69 +0,0 @@
-/*
- * main.cpp -- test Irccd.Unicode API
- *
- * Copyright (c) 2013-2017 David Demelier <markand@malikania.fr>
- *
- * Permission to use, copy, modify, and/or distribute this software for any
- * purpose with or without fee is hereby granted, provided that the above
- * copyright notice and this permission notice appear in all copies.
- *
- * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
- * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
- * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
- * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
- * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
- * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
- * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
- */
-
-/*
- * /!\ Be sure that this file is kept saved in UTF-8 /!\
- */
-
-#define BOOST_TEST_MODULE "Unicode Javascript API"
-#include <boost/test/unit_test.hpp>
-
-#include <irccd/js/unicode_jsapi.hpp>
-
-#include <irccd/test/js_test.hpp>
-
-namespace irccd {
-
-BOOST_FIXTURE_TEST_SUITE(unicode_jsapi_suite, js_test<unicode_jsapi>)
-
-BOOST_AUTO_TEST_CASE(is_letter)
-{
-    duk_peval_string_noresult(plugin_->context(), "result = Irccd.Unicode.isLetter(String('é').charCodeAt(0));");
-    BOOST_TEST(duk_get_global_string(plugin_->context(), "result"));
-    BOOST_TEST(duk_get_boolean(plugin_->context(), -1));
-
-    duk_peval_string_noresult(plugin_->context(), "result = Irccd.Unicode.isLetter(String('€').charCodeAt(0));");
-    BOOST_TEST(duk_get_global_string(plugin_->context(), "result"));
-    BOOST_TEST(!duk_get_boolean(plugin_->context(), -1));
-}
-
-BOOST_AUTO_TEST_CASE(is_lower)
-{
-    duk_peval_string_noresult(plugin_->context(), "result = Irccd.Unicode.isLower(String('é').charCodeAt(0));");
-    BOOST_TEST(duk_get_global_string(plugin_->context(), "result"));
-    BOOST_TEST(duk_get_boolean(plugin_->context(), -1));
-
-    duk_peval_string_noresult(plugin_->context(), "result = Irccd.Unicode.isLower(String('É').charCodeAt(0));");
-    BOOST_TEST(duk_get_global_string(plugin_->context(), "result"));
-    BOOST_TEST(!duk_get_boolean(plugin_->context(), -1));
-}
-
-BOOST_AUTO_TEST_CASE(is_upper)
-{
-    duk_peval_string_noresult(plugin_->context(), "result = Irccd.Unicode.isUpper(String('É').charCodeAt(0));");
-    BOOST_TEST(duk_get_global_string(plugin_->context(), "result"));
-    BOOST_TEST(duk_get_boolean(plugin_->context(), -1));
-
-    duk_peval_string_noresult(plugin_->context(), "result = Irccd.Unicode.isUpper(String('é').charCodeAt(0));");
-    BOOST_TEST(duk_get_global_string(plugin_->context(), "result"));
-    BOOST_TEST(!duk_get_boolean(plugin_->context(), -1));
-}
-
-BOOST_AUTO_TEST_SUITE_END()
-
-} // !irccd
--- a/tests/src/util-jsapi/CMakeLists.txt	Thu Dec 14 21:51:22 2017 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,23 +0,0 @@
-#
-# CMakeLists.txt -- CMake build system for irccd
-#
-# Copyright (c) 2013-2017 David Demelier <markand@malikania.fr>
-#
-# Permission to use, copy, modify, and/or distribute this software for any
-# purpose with or without fee is hereby granted, provided that the above
-# copyright notice and this permission notice appear in all copies.
-#
-# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
-# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
-# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
-# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
-# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
-# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
-# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
-#
-
-irccd_define_test(
-    NAME util-jsapi
-    SOURCES main.cpp
-    LIBRARIES libirccd-js
-)
--- a/tests/src/util-jsapi/main.cpp	Thu Dec 14 21:51:22 2017 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,260 +0,0 @@
-/*
- * main.cpp -- test Irccd.Util API
- *
- * Copyright (c) 2013-2017 David Demelier <markand@malikania.fr>
- *
- * Permission to use, copy, modify, and/or distribute this software for any
- * purpose with or without fee is hereby granted, provided that the above
- * copyright notice and this permission notice appear in all copies.
- *
- * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
- * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
- * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
- * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
- * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
- * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
- * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
- */
-
-#define BOOST_TEST_MODULE "Unicode Javascript API"
-#include <boost/test/unit_test.hpp>
-
-#include <irccd/js/util_jsapi.hpp>
-
-#include <irccd/test/js_test.hpp>
-
-namespace irccd {
-
-BOOST_FIXTURE_TEST_SUITE(util_jsapi_suite, js_test<util_jsapi>)
-
-/*
- * Irccd.Util misc.
- * ------------------------------------------------------------------
- */
-
-BOOST_AUTO_TEST_CASE(format_simple)
-{
-    auto ret = duk_peval_string(plugin_->context(),
-        "result = Irccd.Util.format(\"#{target}\", { target: \"markand\" })"
-    );
-
-    if (ret != 0)
-        throw dukx_stack(plugin_->context(), -1);
-
-    BOOST_TEST(duk_get_global_string(plugin_->context(), "result"));
-    BOOST_TEST(duk_get_string(plugin_->context(), -1) == "markand");
-}
-
-BOOST_AUTO_TEST_CASE(splituser)
-{
-    if (duk_peval_string(plugin_->context(), "result = Irccd.Util.splituser(\"user!~user@hyper/super/host\");") != 0)
-        throw dukx_stack(plugin_->context(), -1);
-
-    BOOST_TEST(duk_get_global_string(plugin_->context(), "result"));
-    BOOST_TEST(duk_get_string(plugin_->context(), -1) == "user");
-}
-
-BOOST_AUTO_TEST_CASE(splithost)
-{
-    if (duk_peval_string(plugin_->context(), "result = Irccd.Util.splithost(\"user!~user@hyper/super/host\");") != 0)
-        throw dukx_stack(plugin_->context(), -1);
-
-    BOOST_TEST(duk_get_global_string(plugin_->context(), "result"));
-    BOOST_TEST(duk_get_string(plugin_->context(), -1) == "~user@hyper/super/host");
-}
-
-/*
- * Irccd.Util.cut.
- * ------------------------------------------------------------------
- */
-
-BOOST_AUTO_TEST_CASE(cut_string_simple)
-{
-    auto ret = duk_peval_string(plugin_->context(),
-        "lines = Irccd.Util.cut('hello world');\n"
-        "line0 = lines[0];\n"
-    );
-
-    if (ret != 0)
-        throw dukx_stack(plugin_->context(), -1);
-
-    BOOST_TEST(duk_get_global_string(plugin_->context(), "line0"));
-    BOOST_TEST(duk_get_string(plugin_->context(), -1) == "hello world");
-}
-
-BOOST_AUTO_TEST_CASE(cut_string_double)
-{
-    auto ret = duk_peval_string(plugin_->context(),
-        "lines = Irccd.Util.cut('hello world', 5);\n"
-        "line0 = lines[0];\n"
-        "line1 = lines[1];\n"
-    );
-
-    if (ret != 0)
-        throw dukx_stack(plugin_->context(), -1);
-
-    BOOST_TEST(duk_get_global_string(plugin_->context(), "line0"));
-    BOOST_TEST(duk_get_string(plugin_->context(), -1) == "hello");
-    BOOST_TEST(duk_get_global_string(plugin_->context(), "line1"));
-    BOOST_TEST(duk_get_string(plugin_->context(), -1) == "world");
-}
-
-BOOST_AUTO_TEST_CASE(cut_string_dirty)
-{
-    auto ret = duk_peval_string(plugin_->context(),
-        "lines = Irccd.Util.cut('     hello    world     ', 5);\n"
-        "line0 = lines[0];\n"
-        "line1 = lines[1];\n"
-    );
-
-    if (ret != 0)
-        throw dukx_stack(plugin_->context(), -1);
-
-    BOOST_TEST(duk_get_global_string(plugin_->context(), "line0"));
-    BOOST_TEST(duk_get_string(plugin_->context(), -1) == "hello");
-    BOOST_TEST(duk_get_global_string(plugin_->context(), "line1"));
-    BOOST_TEST(duk_get_string(plugin_->context(), -1) == "world");
-}
-
-BOOST_AUTO_TEST_CASE(cut_string_too_much_lines)
-{
-    auto ret = duk_peval_string(plugin_->context(),
-        "lines = Irccd.Util.cut('abc def ghi jkl', 3, 3);\n"
-    );
-
-    if (ret != 0)
-        throw dukx_stack(plugin_->context(), -1);
-
-    BOOST_TEST(duk_get_global_string(plugin_->context(), "lines"));
-    BOOST_TEST(duk_is_undefined(plugin_->context(), -1));
-}
-
-BOOST_AUTO_TEST_CASE(cut_string_token_too_big)
-{
-    auto ret = duk_peval_string(plugin_->context(),
-        "try {\n"
-        "  lines = Irccd.Util.cut('hello world', 3);\n"
-        "} catch (e) {\n"
-        "  name = e.name;\n"
-        "  message = e.message;\n"
-        "}\n"
-    );
-
-    if (ret != 0)
-        throw dukx_stack(plugin_->context(), -1);
-
-    BOOST_TEST(duk_get_global_string(plugin_->context(), "name"));
-    BOOST_TEST(duk_get_string(plugin_->context(), -1) == "RangeError");
-    BOOST_TEST(duk_get_global_string(plugin_->context(), "message"));
-    BOOST_TEST(duk_get_string(plugin_->context(), -1) == "word 'hello' could not fit in maxc limit (3)");
-}
-
-BOOST_AUTO_TEST_CASE(cut_string_negative_maxc)
-{
-    auto ret = duk_peval_string(plugin_->context(),
-        "try {\n"
-        "  lines = Irccd.Util.cut('hello world', -3);\n"
-        "} catch (e) {\n"
-        "  name = e.name;\n"
-        "  message = e.message;\n"
-        "}\n"
-    );
-
-    if (ret != 0)
-        throw dukx_stack(plugin_->context(), -1);
-
-    BOOST_TEST(duk_get_global_string(plugin_->context(), "name"));
-    BOOST_TEST(duk_get_string(plugin_->context(), -1) == "RangeError");
-    BOOST_TEST(duk_get_global_string(plugin_->context(), "message"));
-    BOOST_TEST(duk_get_string(plugin_->context(), -1) == "argument 1 (maxc) must be positive");
-}
-
-BOOST_AUTO_TEST_CASE(cut_string_negative_maxl)
-{
-    auto ret = duk_peval_string(plugin_->context(),
-        "try {\n"
-        "  lines = Irccd.Util.cut('hello world', undefined, -1);\n"
-        "} catch (e) {\n"
-        "  name = e.name;\n"
-        "  message = e.message;\n"
-        "}\n"
-    );
-
-    if (ret != 0)
-        throw dukx_stack(plugin_->context(), -1);
-
-    BOOST_TEST(duk_get_global_string(plugin_->context(), "name"));
-    BOOST_TEST(duk_get_string(plugin_->context(), -1) == "RangeError");
-    BOOST_TEST(duk_get_global_string(plugin_->context(), "message"));
-    BOOST_TEST(duk_get_string(plugin_->context(), -1) == "argument 2 (maxl) must be positive");
-}
-
-BOOST_AUTO_TEST_CASE(cut_array_simple)
-{
-    auto ret = duk_peval_string(plugin_->context(),
-        "lines = Irccd.Util.cut([ 'hello', 'world' ]);\n"
-        "line0 = lines[0];\n"
-    );
-
-    if (ret != 0)
-        throw dukx_stack(plugin_->context(), -1);
-
-    BOOST_TEST(duk_get_global_string(plugin_->context(), "line0"));
-    BOOST_TEST(duk_get_string(plugin_->context(), -1) == "hello world");
-}
-
-BOOST_AUTO_TEST_CASE(cut_array_double)
-{
-    auto ret = duk_peval_string(plugin_->context(),
-        "lines = Irccd.Util.cut([ 'hello', 'world' ], 5);\n"
-        "line0 = lines[0];\n"
-        "line1 = lines[1];\n"
-    );
-
-    if (ret != 0)
-        throw dukx_stack(plugin_->context(), -1);
-
-    BOOST_TEST(duk_get_global_string(plugin_->context(), "line0"));
-    BOOST_TEST(duk_get_string(plugin_->context(), -1) == "hello");
-    BOOST_TEST(duk_get_global_string(plugin_->context(), "line1"));
-    BOOST_TEST(duk_get_string(plugin_->context(), -1) == "world");
-}
-
-BOOST_AUTO_TEST_CASE(cut_array_dirty)
-{
-    auto ret = duk_peval_string(plugin_->context(),
-        "lines = Irccd.Util.cut([ '   ', ' hello  ', '  world ', '    '], 5);\n"
-        "line0 = lines[0];\n"
-        "line1 = lines[1];\n"
-    );
-
-    if (ret != 0)
-        throw dukx_stack(plugin_->context(), -1);
-
-    BOOST_TEST(duk_get_global_string(plugin_->context(), "line0"));
-    BOOST_TEST(duk_get_string(plugin_->context(), -1) == "hello");
-    BOOST_TEST(duk_get_global_string(plugin_->context(), "line1"));
-    BOOST_TEST(duk_get_string(plugin_->context(), -1) == "world");
-}
-
-BOOST_AUTO_TEST_CASE(cut_invalid_data)
-{
-    auto ret = duk_peval_string(plugin_->context(),
-        "try {\n"
-        "  lines = Irccd.Util.cut(123);\n"
-        "} catch (e) {\n"
-        "  name = e.name;\n"
-        "  message = e.message;\n"
-        "}\n"
-    );
-
-    if (ret != 0)
-        throw dukx_stack(plugin_->context(), -1);
-
-    BOOST_TEST(duk_get_global_string(plugin_->context(), "name"));
-    BOOST_TEST(duk_get_string(plugin_->context(), -1) == "TypeError");
-}
-
-BOOST_AUTO_TEST_SUITE_END()
-
-} // !irccd
--- a/tests/src/util/CMakeLists.txt	Thu Dec 14 21:51:22 2017 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,23 +0,0 @@
-#
-# CMakeLists.txt -- CMake build system for irccd
-#
-# Copyright (c) 2013-2017 David Demelier <markand@malikania.fr>
-#
-# Permission to use, copy, modify, and/or distribute this software for any
-# purpose with or without fee is hereby granted, provided that the above
-# copyright notice and this permission notice appear in all copies.
-#
-# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
-# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
-# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
-# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
-# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
-# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
-# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
-#
-
-irccd_define_test(
-    NAME util
-    SOURCES main.cpp
-    LIBRARIES libirccd
-)
--- a/tests/src/util/main.cpp	Thu Dec 14 21:51:22 2017 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,530 +0,0 @@
-/*
- * main.cpp -- test util functions
- *
- * Copyright (c) 2013-2017 David Demelier <markand@malikania.fr>
- *
- * Permission to use, copy, modify, and/or distribute this software for any
- * purpose with or without fee is hereby granted, provided that the above
- * copyright notice and this permission notice appear in all copies.
- *
- * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
- * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
- * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
- * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
- * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
- * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
- * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
- */
-
-#define BOOST_TEST_MODULE "util"
-#include <boost/test/unit_test.hpp>
-
-#include <cstdint>
-
-#include <irccd/util.hpp>
-#include <irccd/fs_util.hpp>
-#include <irccd/string_util.hpp>
-#include <irccd/system.hpp>
-
-namespace std {
-
-std::ostream& operator<<(std::ostream& out, const std::vector<std::string>& list)
-{
-    for (const auto& s : list)
-        out << s << " ";
-
-    return out;
-}
-
-} // !std
-
-namespace irccd {
-
-BOOST_AUTO_TEST_SUITE(format)
-
-/*
- * string_util::format function
- * --------------------------------------------------------
- */
-
-BOOST_AUTO_TEST_CASE(nothing)
-{
-    std::string expected = "hello world!";
-    std::string result = string_util::format("hello world!");
-
-    BOOST_REQUIRE_EQUAL(expected, result);
-}
-
-BOOST_AUTO_TEST_CASE(escape)
-{
-    string_util::subst params;
-
-    params.keywords.emplace("target", "hello");
-
-    BOOST_REQUIRE_EQUAL("$@#", string_util::format("$@#"));
-    BOOST_REQUIRE_EQUAL(" $ @ # ", string_util::format(" $ @ # "));
-    BOOST_REQUIRE_EQUAL("#", string_util::format("#"));
-    BOOST_REQUIRE_EQUAL(" # ", string_util::format(" # "));
-    BOOST_REQUIRE_EQUAL("#@", string_util::format("#@"));
-    BOOST_REQUIRE_EQUAL("##", string_util::format("##"));
-    BOOST_REQUIRE_EQUAL("#!", string_util::format("#!"));
-    BOOST_REQUIRE_EQUAL("#{target}", string_util::format("##{target}"));
-    BOOST_REQUIRE_EQUAL("@hello", string_util::format("@#{target}", params));
-    BOOST_REQUIRE_EQUAL("hello#", string_util::format("#{target}#", params));
-    BOOST_REQUIRE_THROW(string_util::format("#{failure"), std::exception);
-}
-
-BOOST_AUTO_TEST_CASE(disable_date)
-{
-    string_util::subst params;
-
-    params.flags &= ~(string_util::subst_flags::date);
-
-    BOOST_REQUIRE_EQUAL("%H:%M", string_util::format("%H:%M", params));
-}
-
-BOOST_AUTO_TEST_CASE(disable_keywords)
-{
-    string_util::subst params;
-
-    params.keywords.emplace("target", "hello");
-    params.flags &= ~(string_util::subst_flags::keywords);
-
-    BOOST_REQUIRE_EQUAL("#{target}", string_util::format("#{target}", params));
-}
-
-BOOST_AUTO_TEST_CASE(disable_env)
-{
-    string_util::subst params;
-
-    params.flags &= ~(string_util::subst_flags::env);
-
-    BOOST_REQUIRE_EQUAL("${HOME}", string_util::format("${HOME}", params));
-}
-
-BOOST_AUTO_TEST_CASE(keyword_simple)
-{
-    string_util::subst params;
-
-    params.keywords.insert({"target", "irccd"});
-
-    std::string expected = "hello irccd!";
-    std::string result = string_util::format("hello #{target}!", params);
-
-    BOOST_REQUIRE_EQUAL(expected, result);
-}
-
-BOOST_AUTO_TEST_CASE(keyword_multiple)
-{
-    string_util::subst params;
-
-    params.keywords.insert({"target", "irccd"});
-    params.keywords.insert({"source", "nightmare"});
-
-    std::string expected = "hello irccd from nightmare!";
-    std::string result = string_util::format("hello #{target} from #{source}!", params);
-
-    BOOST_REQUIRE_EQUAL(expected, result);
-}
-
-BOOST_AUTO_TEST_CASE(keyword_adj_twice)
-{
-    string_util::subst params;
-
-    params.keywords.insert({"target", "irccd"});
-
-    std::string expected = "hello irccdirccd!";
-    std::string result = string_util::format("hello #{target}#{target}!", params);
-
-    BOOST_REQUIRE_EQUAL(expected, result);
-}
-
-BOOST_AUTO_TEST_CASE(keyword_missing)
-{
-    std::string expected = "hello !";
-    std::string result = string_util::format("hello #{target}!");
-
-    BOOST_REQUIRE_EQUAL(expected, result);
-}
-
-BOOST_AUTO_TEST_CASE(env_simple)
-{
-    std::string home = sys::env("HOME");
-
-    if (!home.empty()) {
-        std::string expected = "my home is " + home;
-        std::string result = string_util::format("my home is ${HOME}");
-
-        BOOST_REQUIRE_EQUAL(expected, result);
-    }
-}
-
-BOOST_AUTO_TEST_CASE(env_missing)
-{
-    std::string expected = "value is ";
-    std::string result = string_util::format("value is ${HOPE_THIS_VAR_NOT_EXIST}");
-
-    BOOST_REQUIRE_EQUAL(expected, result);
-}
-
-BOOST_AUTO_TEST_SUITE_END()
-
-/*
- * string_util::split function
- * --------------------------------------------------------
- */
-
-BOOST_AUTO_TEST_SUITE(split)
-
-using list = std::vector<std::string>;
-
-BOOST_AUTO_TEST_CASE(simple)
-{
-    list expected { "a", "b" };
-    list result = string_util::split("a;b", ";");
-
-    BOOST_REQUIRE_EQUAL(expected, result);
-}
-
-BOOST_AUTO_TEST_CASE(cut)
-{
-    list expected { "msg", "#staff", "foo bar baz" };
-    list result = string_util::split("msg;#staff;foo bar baz", ";", 3);
-
-    BOOST_REQUIRE_EQUAL(expected, result);
-}
-
-BOOST_AUTO_TEST_SUITE_END()
-
-/*
- * string_util::strip function
- * --------------------------------------------------------
- */
-
-BOOST_AUTO_TEST_SUITE(strip)
-
-BOOST_AUTO_TEST_CASE(left)
-{
-    std::string value = "   123";
-    std::string result = string_util::strip(value);
-
-    BOOST_REQUIRE_EQUAL("123", result);
-}
-
-BOOST_AUTO_TEST_CASE(right)
-{
-    std::string value = "123   ";
-    std::string result = string_util::strip(value);
-
-    BOOST_REQUIRE_EQUAL("123", result);
-}
-
-BOOST_AUTO_TEST_CASE(both)
-{
-    std::string value = "   123   ";
-    std::string result = string_util::strip(value);
-
-    BOOST_REQUIRE_EQUAL("123", result);
-}
-
-BOOST_AUTO_TEST_CASE(none)
-{
-    std::string value = "without";
-    std::string result = string_util::strip(value);
-
-    BOOST_REQUIRE_EQUAL("without", result);
-}
-
-BOOST_AUTO_TEST_CASE(betweenEmpty)
-{
-    std::string value = "one list";
-    std::string result = string_util::strip(value);
-
-    BOOST_REQUIRE_EQUAL("one list", result);
-}
-
-BOOST_AUTO_TEST_CASE(betweenLeft)
-{
-    std::string value = "  space at left";
-    std::string result = string_util::strip(value);
-
-    BOOST_REQUIRE_EQUAL("space at left", result);
-}
-
-BOOST_AUTO_TEST_CASE(betweenRight)
-{
-    std::string value = "space at right  ";
-    std::string result = string_util::strip(value);
-
-    BOOST_REQUIRE_EQUAL("space at right", result);
-}
-
-BOOST_AUTO_TEST_CASE(betweenBoth)
-{
-    std::string value = "  space at both  ";
-    std::string result = string_util::strip(value);
-
-    BOOST_REQUIRE_EQUAL("space at both", result);
-}
-
-BOOST_AUTO_TEST_CASE(empty)
-{
-    std::string value = "    ";
-    std::string result = string_util::strip(value);
-
-    BOOST_REQUIRE_EQUAL("", result);
-}
-
-BOOST_AUTO_TEST_SUITE_END()
-
-/*
- * string_util::join function
- * --------------------------------------------------------
- */
-
-BOOST_AUTO_TEST_SUITE(join)
-
-BOOST_AUTO_TEST_CASE(empty)
-{
-    std::string expected = "";
-    std::string result = string_util::join<int>({});
-
-    BOOST_REQUIRE_EQUAL(expected, result);
-}
-
-BOOST_AUTO_TEST_CASE(one)
-{
-    std::string expected = "1";
-    std::string result = string_util::join({1});
-
-    BOOST_REQUIRE_EQUAL(expected, result);
-}
-
-BOOST_AUTO_TEST_CASE(two)
-{
-    std::string expected = "1:2";
-    std::string result = string_util::join({1, 2});
-
-    BOOST_REQUIRE_EQUAL(expected, result);
-}
-
-BOOST_AUTO_TEST_CASE(delimiterString)
-{
-    std::string expected = "1;;2;;3";
-    std::string result = string_util::join({1, 2, 3}, ";;");
-
-    BOOST_REQUIRE_EQUAL(expected, result);
-}
-
-BOOST_AUTO_TEST_CASE(delimiterChar)
-{
-    std::string expected = "1@2@3@4";
-    std::string result = string_util::join({1, 2, 3, 4}, '@');
-
-    BOOST_REQUIRE_EQUAL(expected, result);
-}
-
-BOOST_AUTO_TEST_SUITE_END()
-
-/*
- * string_util::is_identifier function
- * --------------------------------------------------------
- */
-
-BOOST_AUTO_TEST_SUITE(is_identifier_valid)
-
-BOOST_AUTO_TEST_CASE(correct)
-{
-    BOOST_REQUIRE(string_util::is_identifier("localhost"));
-    BOOST_REQUIRE(string_util::is_identifier("localhost2"));
-    BOOST_REQUIRE(string_util::is_identifier("localhost2-4_"));
-}
-
-BOOST_AUTO_TEST_CASE(incorrect)
-{
-    BOOST_REQUIRE(!string_util::is_identifier(""));
-    BOOST_REQUIRE(!string_util::is_identifier("localhost with spaces"));
-    BOOST_REQUIRE(!string_util::is_identifier("localhost*"));
-    BOOST_REQUIRE(!string_util::is_identifier("&&"));
-    BOOST_REQUIRE(!string_util::is_identifier("@'"));
-    BOOST_REQUIRE(!string_util::is_identifier("##"));
-    BOOST_REQUIRE(!string_util::is_identifier("===++"));
-}
-
-BOOST_AUTO_TEST_SUITE_END()
-
-/*
- * string_util::is_boolean function
- * --------------------------------------------------------
- */
-
-BOOST_AUTO_TEST_SUITE(is_boolean)
-
-BOOST_AUTO_TEST_CASE(correct)
-{
-    // true
-    BOOST_REQUIRE(string_util::is_boolean("true"));
-    BOOST_REQUIRE(string_util::is_boolean("True"));
-    BOOST_REQUIRE(string_util::is_boolean("TRUE"));
-    BOOST_REQUIRE(string_util::is_boolean("TruE"));
-
-    // yes
-    BOOST_REQUIRE(string_util::is_boolean("yes"));
-    BOOST_REQUIRE(string_util::is_boolean("Yes"));
-    BOOST_REQUIRE(string_util::is_boolean("YES"));
-    BOOST_REQUIRE(string_util::is_boolean("YeS"));
-
-    // on
-    BOOST_REQUIRE(string_util::is_boolean("on"));
-    BOOST_REQUIRE(string_util::is_boolean("On"));
-    BOOST_REQUIRE(string_util::is_boolean("oN"));
-    BOOST_REQUIRE(string_util::is_boolean("ON"));
-
-    // 1
-    BOOST_REQUIRE(string_util::is_boolean("1"));
-}
-
-BOOST_AUTO_TEST_CASE(incorrect)
-{
-    BOOST_REQUIRE(!string_util::is_boolean("false"));
-    BOOST_REQUIRE(!string_util::is_boolean("lol"));
-    BOOST_REQUIRE(!string_util::is_boolean(""));
-    BOOST_REQUIRE(!string_util::is_boolean("0"));
-}
-
-BOOST_AUTO_TEST_SUITE_END()
-
-/*
- * string_util::is_number function
- * --------------------------------------------------------
- */
-
-BOOST_AUTO_TEST_SUITE(is_number)
-
-BOOST_AUTO_TEST_CASE(correct)
-{
-    BOOST_REQUIRE(string_util::is_number("123"));
-    BOOST_REQUIRE(string_util::is_number("-123"));
-    BOOST_REQUIRE(string_util::is_number("123.67"));
-}
-
-BOOST_AUTO_TEST_CASE(incorrect)
-{
-    BOOST_REQUIRE(!string_util::is_number("lol"));
-    BOOST_REQUIRE(!string_util::is_number("this is not a number"));
-}
-
-BOOST_AUTO_TEST_SUITE_END()
-
-/*
- * string_util::to_int function
- * ------------------------------------------------------------------
- */
-
-BOOST_AUTO_TEST_SUITE(to_int)
-
-BOOST_AUTO_TEST_CASE(signed_to_int)
-{
-    BOOST_TEST(string_util::to_int("10")                     == 10);
-    BOOST_TEST(string_util::to_int<std::int8_t>("-10")       == -10);
-    BOOST_TEST(string_util::to_int<std::int8_t>("10")        == 10);
-    BOOST_TEST(string_util::to_int<std::int16_t>("-1000")    == -1000);
-    BOOST_TEST(string_util::to_int<std::int16_t>("1000")     == 1000);
-    BOOST_TEST(string_util::to_int<std::int32_t>("-1000")    == -1000);
-    BOOST_TEST(string_util::to_int<std::int32_t>("1000")     == 1000);
-}
-
-BOOST_AUTO_TEST_CASE(signed_to_int64)
-{
-    BOOST_TEST(string_util::to_int<std::int64_t>("-9223372036854775807") == -9223372036854775807LL);
-    BOOST_TEST(string_util::to_int<std::int64_t>("9223372036854775807") == 9223372036854775807LL);
-}
-
-BOOST_AUTO_TEST_CASE(unsigned_to_uint)
-{
-    BOOST_TEST(string_util::to_uint("10")                    == 10U);
-    BOOST_TEST(string_util::to_uint<std::uint8_t>("10")       == 10U);
-    BOOST_TEST(string_util::to_uint<std::uint16_t>("1000")    == 1000U);
-    BOOST_TEST(string_util::to_uint<std::uint32_t>("1000")    == 1000U);
-}
-
-BOOST_AUTO_TEST_CASE(unsigned_to_uint64)
-{
-    BOOST_TEST(string_util::to_uint<std::uint64_t>("18446744073709551615") == 18446744073709551615ULL);
-}
-
-BOOST_AUTO_TEST_SUITE_END()
-
-BOOST_AUTO_TEST_SUITE(errors)
-
-BOOST_AUTO_TEST_CASE(invalid_argument)
-{
-    BOOST_REQUIRE_THROW(string_util::to_int("plopation"), std::invalid_argument);
-    BOOST_REQUIRE_THROW(string_util::to_uint("plopation"), std::invalid_argument);
-}
-
-BOOST_AUTO_TEST_CASE(out_of_range)
-{
-    BOOST_REQUIRE_THROW(string_util::to_int<std::int8_t>("1000"), std::out_of_range);
-    BOOST_REQUIRE_THROW(string_util::to_int<std::int8_t>("-1000"), std::out_of_range);
-    BOOST_REQUIRE_THROW(string_util::to_uint<std::uint8_t>("1000"), std::out_of_range);
-    BOOST_REQUIRE_THROW(string_util::to_uint<std::uint8_t>("-1000"), std::out_of_range);
-}
-
-BOOST_AUTO_TEST_SUITE_END()
-
-/*
- * fs_util::find function (name)
- * ------------------------------------------------------------------
- */
-
-BOOST_AUTO_TEST_SUITE(fs_find_name)
-
-BOOST_AUTO_TEST_CASE(not_recursive)
-{
-    auto file1 = fs_util::find(CMAKE_SOURCE_DIR "/tests/root", "file-1.txt", false);
-    auto file2 = fs_util::find(CMAKE_SOURCE_DIR "/tests/root", "file-2.txt", false);
-
-    BOOST_TEST(file1.find("file-1.txt") != std::string::npos);
-    BOOST_TEST(file2.empty());
-}
-
-BOOST_AUTO_TEST_CASE(recursive)
-{
-    auto file1 = fs_util::find(CMAKE_SOURCE_DIR "/tests/root", "file-1.txt", true);
-    auto file2 = fs_util::find(CMAKE_SOURCE_DIR "/tests/root", "file-2.txt", true);
-
-    BOOST_TEST(file1.find("file-1.txt") != std::string::npos);
-    BOOST_TEST(file2.find("file-2.txt") != std::string::npos);
-}
-
-BOOST_AUTO_TEST_SUITE_END()
-
-/*
- * fs_util::find function (regex)
- * ------------------------------------------------------------------
- */
-
-BOOST_AUTO_TEST_SUITE(fs_find_regex)
-
-BOOST_AUTO_TEST_CASE(not_recursive)
-{
-    const std::regex regex("file-[12]\\.txt");
-
-    auto file = fs_util::find(CMAKE_SOURCE_DIR "/tests/root", regex, false);
-
-    BOOST_TEST(file.find("file-1.txt") != std::string::npos);
-}
-
-BOOST_AUTO_TEST_CASE(recursive)
-{
-    const std::regex regex("file-[12]\\.txt");
-
-    auto file = fs_util::find(CMAKE_SOURCE_DIR "/tests/root/level-1", regex, true);
-
-    BOOST_TEST(file.find("file-2.txt") != std::string::npos);
-}
-
-BOOST_AUTO_TEST_SUITE_END()
-
-} // !irccd