changeset 451:1fdedd2977d2

Irccdctl: implement rule-move
author David Demelier <markand@malikania.fr>
date Fri, 07 Jul 2017 18:03:18 +0200
parents c8c68d4bf555
children 2170aa0e38aa
files doc/html/CMakeLists.txt doc/html/irccdctl/command/index.md doc/html/irccdctl/command/rule-move.md irccd/main.cpp irccdctl/cli.cpp irccdctl/cli.hpp irccdctl/main.cpp libirccd/irccd/command.cpp libirccd/irccd/command.hpp tests/CMakeLists.txt tests/cmd-rule-move/CMakeLists.txt tests/cmd-rule-move/main.cpp
diffstat 12 files changed, 578 insertions(+), 3 deletions(-) [+]
line wrap: on
line diff
--- a/doc/html/CMakeLists.txt	Fri Jul 07 12:22:17 2017 +0200
+++ b/doc/html/CMakeLists.txt	Fri Jul 07 18:03:18 2017 +0200
@@ -152,8 +152,9 @@
     ${html_SOURCE_DIR}/irccdctl/command/plugin-load.md
     ${html_SOURCE_DIR}/irccdctl/command/plugin-reload.md
     ${html_SOURCE_DIR}/irccdctl/command/plugin-unload.md
+    ${html_SOURCE_DIR}/irccdctl/command/rule-info.md
     ${html_SOURCE_DIR}/irccdctl/command/rule-list.md
-    ${html_SOURCE_DIR}/irccdctl/command/rule-info.md
+    ${html_SOURCE_DIR}/irccdctl/command/rule-move.md
     ${html_SOURCE_DIR}/irccdctl/command/rule-remove.md
     ${html_SOURCE_DIR}/irccdctl/command/server-cmode.md
     ${html_SOURCE_DIR}/irccdctl/command/server-cnotice.md
--- a/doc/html/irccdctl/command/index.md	Fri Jul 07 12:22:17 2017 +0200
+++ b/doc/html/irccdctl/command/index.md	Fri Jul 07 18:03:18 2017 +0200
@@ -12,8 +12,9 @@
   - [plugin-load](plugin-load.html)
   - [plugin-reload](plugin-reload.html)
   - [plugin-unload](plugin-unload.html)
+  - [rule-info](rule-info.html)
   - [rule-list](rule-list.html)
-  - [rule-info](rule-info.html)
+  - [rule-move](rule-move.html)
   - [rule-remove](rule-remove.html)
   - [server-cmode](server-cmode.html)
   - [server-cnotice](server-cnotice.html)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/html/irccdctl/command/rule-move.md	Fri Jul 07 18:03:18 2017 +0200
@@ -0,0 +1,25 @@
+---
+title: rule-move
+guide: yes
+---
+
+# rule-move
+
+Move a rule from the given source at the specified destination index.
+
+The rule will replace the existing one at the given destination moving
+down every other rules. If destination is greater or equal the number of rules,
+the rule is moved to the end.
+
+## Usage
+
+````nohighlight
+irccdctl rule-move source destination
+````
+
+## Example
+
+````nohighlight
+irccdctl rule-move 0 5
+irccdctl rule-move 4 3
+````
--- a/irccd/main.cpp	Fri Jul 07 12:22:17 2017 +0200
+++ b/irccd/main.cpp	Fri Jul 07 18:03:18 2017 +0200
@@ -322,6 +322,7 @@
     instance->commands().add(std::make_unique<command::ServerTopicCommand>());
     instance->commands().add(std::make_unique<command::RuleInfoCommand>());
     instance->commands().add(std::make_unique<command::RuleListCommand>());
+    instance->commands().add(std::make_unique<command::RuleMoveCommand>());
     instance->commands().add(std::make_unique<command::RuleRemoveCommand>());
 
     // Load Javascript API and plugin loader.
--- a/irccdctl/cli.cpp	Fri Jul 07 12:22:17 2017 +0200
+++ b/irccdctl/cli.cpp	Fri Jul 07 18:03:18 2017 +0200
@@ -983,6 +983,47 @@
 }
 
 /*
+ * RuleMoveCli.
+ * ------------------------------------------------------------------
+ */
+
+RuleMoveCli::RuleMoveCli()
+    : Cli("rule-move",
+          "move a rule to a new position",
+          "rule-move source destination",
+          "Move a rule from the given source at the specified destination index.\n\n"
+          "The rule will replace the existing one at the given destination moving\ndown every "
+          "other rules. If destination is greater or equal the number of rules,\nthe rule "
+          "is moved to the end.\n\n"
+          "Example:\n"
+          "\tirccdctl rule-move 0 5\n"
+          "\tirccdctl rule-move 4 3")
+{
+}
+
+void RuleMoveCli::exec(Irccdctl &irccdctl, const std::vector<std::string> &args)
+{
+    if (args.size() < 2)
+        throw std::invalid_argument("rule-move requires 2 arguments");
+
+    int from = 0;
+    int to = 0;
+
+    try {
+        from = std::stoi(args[0]);
+        to = std::stoi(args[1]);
+    } catch (...) {
+        throw std::invalid_argument("invalid number");
+    }
+
+    check(request(irccdctl, {
+        { "command",    "rule-move" },
+        { "from",       from        },
+        { "to",         to          }
+    }));
+}
+
+/*
  * WatchCli.
  * ------------------------------------------------------------------
  */
--- a/irccdctl/cli.hpp	Fri Jul 07 12:22:17 2017 +0200
+++ b/irccdctl/cli.hpp	Fri Jul 07 18:03:18 2017 +0200
@@ -648,6 +648,26 @@
     void exec(Irccdctl &client, const std::vector<std::string> &args) override;
 };
 
+/*
+ * RuleMoveCli
+ * ------------------------------------------------------------------
+ */
+
+/**
+ * \brief Implementation of irccdctl rule-move.
+ */
+class RuleMoveCli : public Cli {
+public:
+    /**
+     * Default constructor.
+     */
+    RuleMoveCli();
+
+    /**
+     * \copydoc Cli::exec
+     */
+    void exec(Irccdctl &client, const std::vector<std::string> &args) override;
+};
 
 /*
  * WatchCli.
--- a/irccdctl/main.cpp	Fri Jul 07 12:22:17 2017 +0200
+++ b/irccdctl/main.cpp	Fri Jul 07 18:03:18 2017 +0200
@@ -524,6 +524,7 @@
     commands.push_back(std::make_unique<cli::ServerTopicCli>());
     commands.push_back(std::make_unique<cli::RuleListCli>());
     commands.push_back(std::make_unique<cli::RuleInfoCli>());
+    commands.push_back(std::make_unique<cli::RuleMoveCli>());
     commands.push_back(std::make_unique<cli::RuleRemoveCli>());
     commands.push_back(std::make_unique<cli::WatchCli>());
 }
--- a/libirccd/irccd/command.cpp	Fri Jul 07 12:22:17 2017 +0200
+++ b/libirccd/irccd/command.cpp	Fri Jul 07 18:03:18 2017 +0200
@@ -493,6 +493,60 @@
     }
 }
 
+RuleMoveCommand::RuleMoveCommand()
+    : Command("rule-move")
+{
+}
+
+void RuleMoveCommand::exec(Irccd &irccd, TransportClient &client, const nlohmann::json &args)
+{
+    auto from = util::json::requireUint(args, "from");
+    auto to = util::json::requireUint(args, "to");
+
+    /*
+     * Examples of moves
+     * --------------------------------------------------------------
+     *
+     * Before: [0] [1] [2]
+     *
+     * from = 0
+     * to   = 2
+     *
+     * After:  [1] [2] [0]
+     *
+     * --------------------------------------------------------------
+     *
+     * Before: [0] [1] [2]
+     *
+     * from = 2
+     * to   = 0
+     *
+     * After:  [2] [0] [1]
+     *
+     * --------------------------------------------------------------
+     *
+     * Before: [0] [1] [2]
+     *
+     * from = 0
+     * to   = 123
+     *
+     * After:  [1] [2] [0]
+     */
+
+    // Ignore dump input.
+    if (from == to)
+        client.success("rule-move");
+    else if (from >= irccd.rules().length())
+        client.error("rule-move", "rule source index is out of range");
+    else {
+        auto save = irccd.rules().list()[from];
+
+        irccd.rules().remove(from);
+        irccd.rules().insert(save, to > irccd.rules().length() ? irccd.rules().length() : to);
+        client.success("rule-move");
+    }
+}
+
 } // !command
 
 } // !irccd
--- a/libirccd/irccd/command.hpp	Fri Jul 07 12:22:17 2017 +0200
+++ b/libirccd/irccd/command.hpp	Fri Jul 07 18:03:18 2017 +0200
@@ -506,6 +506,22 @@
     void exec(Irccd &irccd, TransportClient &client, const nlohmann::json &args) override;
 };
 
+/**
+ * \brief Implementation of rule-move transport command.
+ */
+class IRCCD_EXPORT RuleMoveCommand : public Command {
+public:
+    /**
+     * Constructor.
+     */
+    RuleMoveCommand();
+
+    /**
+     * \copydoc Command::exec
+     */
+    void exec(Irccd &irccd, TransportClient &client, const nlohmann::json &args) override;
+};
+
 } // !command
 
 } // !irccd
--- a/tests/CMakeLists.txt	Fri Jul 07 12:22:17 2017 +0200
+++ b/tests/CMakeLists.txt	Fri Jul 07 18:03:18 2017 +0200
@@ -26,8 +26,9 @@
     add_subdirectory(cmd-plugin-load)
     add_subdirectory(cmd-plugin-reload)
     add_subdirectory(cmd-plugin-unload)
+    add_subdirectory(cmd-rule-info)
     add_subdirectory(cmd-rule-list)
-    add_subdirectory(cmd-rule-info)
+    add_subdirectory(cmd-rule-move)
     add_subdirectory(cmd-rule-remove)
     add_subdirectory(cmd-server-cmode)
     add_subdirectory(cmd-server-cnotice)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/cmd-rule-move/CMakeLists.txt	Fri Jul 07 18:03:18 2017 +0200
@@ -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 cmd-rule-move
+    SOURCES main.cpp
+    LIBRARIES libirccd libirccdctl
+)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/cmd-rule-move/main.cpp	Fri Jul 07 18:03:18 2017 +0200
@@ -0,0 +1,391 @@
+/*
+ * 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.
+ */
+
+#include <command.hpp>
+#include <command-tester.hpp>
+#include <service.hpp>
+
+using namespace irccd;
+using namespace irccd::command;
+
+class RuleMoveCommandTest : public CommandTester {
+protected:
+    nlohmann::json m_result;
+
+    /*
+     * Rule sets are unordered so use this function to search a string in
+     * the JSON array.
+     */
+    inline bool contains(const nlohmann::json &array, const std::string &str)
+    {
+        for (const auto &v : array)
+            if (v.is_string() && v == str)
+                return true;
+
+        return false;
+    }
+
+public:
+    RuleMoveCommandTest()
+        : CommandTester(std::make_unique<RuleMoveCommand>())
+    {
+        m_irccd.commands().add(std::make_unique<RuleListCommand>());
+        m_irccd.rules().add(Rule(
+            { "s0" },
+            { "c0" },
+            { "o0" },
+            { "p0" },
+            { "onMessage" },
+            RuleAction::Drop
+        ));
+        m_irccd.rules().add(Rule(
+            { "s1", },
+            { "c1", },
+            { "o1", },
+            { "p1", },
+            { "onMessage", },
+            RuleAction::Accept
+        ));
+        m_irccd.rules().add(Rule(
+            { "s2", },
+            { "c2", },
+            { "o2", },
+            { "p2", },
+            { "onMessage", },
+            RuleAction::Accept
+        ));
+        m_irccdctl.client().onMessage.connect([&] (auto result) {
+            m_result = result;
+        });
+    }
+};
+
+TEST_F(RuleMoveCommandTest, backward)
+{
+    try {
+        m_irccdctl.client().request({
+            { "command",    "rule-move" },
+            { "from",       2           },
+            { "to",         0           }
+        });
+
+        poll([&] () {
+            return m_result.is_object();
+        });
+
+        ASSERT_TRUE(m_result.is_object());
+        ASSERT_TRUE(m_result["status"].get<bool>());
+
+        m_result = nullptr;
+        m_irccdctl.client().request({{ "command", "rule-list" }});
+
+        poll([&] () {
+            return m_result.is_object();
+        });
+
+        ASSERT_TRUE(m_result.is_object());
+        ASSERT_TRUE(m_result["status"].get<bool>());
+
+        // Rule 2.
+        {
+            auto servers = m_result["list"][0]["servers"];
+            auto channels = m_result["list"][0]["channels"];
+            auto plugins = m_result["list"][0]["plugins"];
+            auto events = m_result["list"][0]["events"];
+
+            ASSERT_TRUE(contains(servers, "s2"));
+            ASSERT_TRUE(contains(channels, "c2"));
+            ASSERT_TRUE(contains(plugins, "p2"));
+            ASSERT_TRUE(contains(events, "onMessage"));
+            ASSERT_EQ("accept", m_result["list"][0]["action"].get<std::string>());
+        }
+
+        // Rule 0.
+        {
+            auto servers = m_result["list"][1]["servers"];
+            auto channels = m_result["list"][1]["channels"];
+            auto plugins = m_result["list"][1]["plugins"];
+            auto events = m_result["list"][1]["events"];
+
+            ASSERT_TRUE(contains(servers, "s0"));
+            ASSERT_TRUE(contains(channels, "c0"));
+            ASSERT_TRUE(contains(plugins, "p0"));
+            ASSERT_TRUE(contains(events, "onMessage"));
+            ASSERT_EQ("drop", m_result["list"][1]["action"].get<std::string>());
+        }
+
+        // Rule 1.
+        {
+            auto servers = m_result["list"][2]["servers"];
+            auto channels = m_result["list"][2]["channels"];
+            auto plugins = m_result["list"][2]["plugins"];
+            auto events = m_result["list"][2]["events"];
+
+            ASSERT_TRUE(contains(servers, "s1"));
+            ASSERT_TRUE(contains(channels, "c1"));
+            ASSERT_TRUE(contains(plugins, "p1"));
+            ASSERT_TRUE(contains(events, "onMessage"));
+            ASSERT_EQ("accept", m_result["list"][2]["action"].get<std::string>());
+        }
+    } catch (const std::exception& ex) {
+        FAIL() << ex.what();
+    }
+}
+
+TEST_F(RuleMoveCommandTest, upward)
+{
+    try {
+        m_irccdctl.client().request({
+            { "command",    "rule-move" },
+            { "from",       0           },
+            { "to",         2           }
+        });
+
+        poll([&] () {
+            return m_result.is_object();
+        });
+
+        ASSERT_TRUE(m_result.is_object());
+        ASSERT_TRUE(m_result["status"].get<bool>());
+
+        m_result = nullptr;
+        m_irccdctl.client().request({{ "command", "rule-list" }});
+
+        poll([&] () {
+            return m_result.is_object();
+        });
+
+        ASSERT_TRUE(m_result.is_object());
+        ASSERT_TRUE(m_result["status"].get<bool>());
+
+        // Rule 1.
+        {
+            auto servers = m_result["list"][0]["servers"];
+            auto channels = m_result["list"][0]["channels"];
+            auto plugins = m_result["list"][0]["plugins"];
+            auto events = m_result["list"][0]["events"];
+
+            ASSERT_TRUE(contains(servers, "s1"));
+            ASSERT_TRUE(contains(channels, "c1"));
+            ASSERT_TRUE(contains(plugins, "p1"));
+            ASSERT_TRUE(contains(events, "onMessage"));
+            ASSERT_EQ("accept", m_result["list"][0]["action"].get<std::string>());
+        }
+
+        // Rule 2.
+        {
+            auto servers = m_result["list"][1]["servers"];
+            auto channels = m_result["list"][1]["channels"];
+            auto plugins = m_result["list"][1]["plugins"];
+            auto events = m_result["list"][1]["events"];
+
+            ASSERT_TRUE(contains(servers, "s2"));
+            ASSERT_TRUE(contains(channels, "c2"));
+            ASSERT_TRUE(contains(plugins, "p2"));
+            ASSERT_TRUE(contains(events, "onMessage"));
+            ASSERT_EQ("accept", m_result["list"][1]["action"].get<std::string>());
+        }
+
+        // Rule 0.
+        {
+            auto servers = m_result["list"][2]["servers"];
+            auto channels = m_result["list"][2]["channels"];
+            auto plugins = m_result["list"][2]["plugins"];
+            auto events = m_result["list"][2]["events"];
+
+            ASSERT_TRUE(contains(servers, "s0"));
+            ASSERT_TRUE(contains(channels, "c0"));
+            ASSERT_TRUE(contains(plugins, "p0"));
+            ASSERT_TRUE(contains(events, "onMessage"));
+            ASSERT_EQ("drop", m_result["list"][2]["action"].get<std::string>());
+        }
+    } catch (const std::exception& ex) {
+        FAIL() << ex.what();
+    }
+}
+
+TEST_F(RuleMoveCommandTest, same)
+{
+    try {
+        m_irccdctl.client().request({
+            { "command",    "rule-move" },
+            { "from",       1           },
+            { "to",         1           }
+        });
+
+        poll([&] () {
+            return m_result.is_object();
+        });
+
+        ASSERT_TRUE(m_result.is_object());
+        ASSERT_TRUE(m_result["status"].get<bool>());
+
+        m_result = nullptr;
+        m_irccdctl.client().request({{ "command", "rule-list" }});
+
+        poll([&] () {
+            return m_result.is_object();
+        });
+
+        ASSERT_TRUE(m_result.is_object());
+        ASSERT_TRUE(m_result["status"].get<bool>());
+
+        // Rule 0.
+        {
+            auto servers = m_result["list"][0]["servers"];
+            auto channels = m_result["list"][0]["channels"];
+            auto plugins = m_result["list"][0]["plugins"];
+            auto events = m_result["list"][0]["events"];
+
+            ASSERT_TRUE(contains(servers, "s0"));
+            ASSERT_TRUE(contains(channels, "c0"));
+            ASSERT_TRUE(contains(plugins, "p0"));
+            ASSERT_TRUE(contains(events, "onMessage"));
+            ASSERT_EQ("drop", m_result["list"][0]["action"].get<std::string>());
+        }
+
+        // Rule 1.
+        {
+            auto servers = m_result["list"][1]["servers"];
+            auto channels = m_result["list"][1]["channels"];
+            auto plugins = m_result["list"][1]["plugins"];
+            auto events = m_result["list"][1]["events"];
+
+            ASSERT_TRUE(contains(servers, "s1"));
+            ASSERT_TRUE(contains(channels, "c1"));
+            ASSERT_TRUE(contains(plugins, "p1"));
+            ASSERT_TRUE(contains(events, "onMessage"));
+            ASSERT_EQ("accept", m_result["list"][1]["action"].get<std::string>());
+        }
+
+        // Rule 2.
+        {
+            auto servers = m_result["list"][2]["servers"];
+            auto channels = m_result["list"][2]["channels"];
+            auto plugins = m_result["list"][2]["plugins"];
+            auto events = m_result["list"][2]["events"];
+
+            ASSERT_TRUE(contains(servers, "s2"));
+            ASSERT_TRUE(contains(channels, "c2"));
+            ASSERT_TRUE(contains(plugins, "p2"));
+            ASSERT_TRUE(contains(events, "onMessage"));
+            ASSERT_EQ("accept", m_result["list"][2]["action"].get<std::string>());
+        }
+    } catch (const std::exception& ex) {
+        FAIL() << ex.what();
+    }
+}
+
+TEST_F(RuleMoveCommandTest, beyond)
+{
+    try {
+        m_irccdctl.client().request({
+            { "command",    "rule-move" },
+            { "from",       0           },
+            { "to",         123         }
+        });
+
+        poll([&] () {
+            return m_result.is_object();
+        });
+
+        ASSERT_TRUE(m_result.is_object());
+        ASSERT_TRUE(m_result["status"].get<bool>());
+
+        m_result = nullptr;
+        m_irccdctl.client().request({{ "command", "rule-list" }});
+
+        poll([&] () {
+            return m_result.is_object();
+        });
+
+        ASSERT_TRUE(m_result.is_object());
+        ASSERT_TRUE(m_result["status"].get<bool>());
+
+        // Rule 1.
+        {
+            auto servers = m_result["list"][0]["servers"];
+            auto channels = m_result["list"][0]["channels"];
+            auto plugins = m_result["list"][0]["plugins"];
+            auto events = m_result["list"][0]["events"];
+
+            ASSERT_TRUE(contains(servers, "s1"));
+            ASSERT_TRUE(contains(channels, "c1"));
+            ASSERT_TRUE(contains(plugins, "p1"));
+            ASSERT_TRUE(contains(events, "onMessage"));
+            ASSERT_EQ("accept", m_result["list"][0]["action"].get<std::string>());
+        }
+
+        // Rule 2.
+        {
+            auto servers = m_result["list"][1]["servers"];
+            auto channels = m_result["list"][1]["channels"];
+            auto plugins = m_result["list"][1]["plugins"];
+            auto events = m_result["list"][1]["events"];
+
+            ASSERT_TRUE(contains(servers, "s2"));
+            ASSERT_TRUE(contains(channels, "c2"));
+            ASSERT_TRUE(contains(plugins, "p2"));
+            ASSERT_TRUE(contains(events, "onMessage"));
+            ASSERT_EQ("accept", m_result["list"][1]["action"].get<std::string>());
+        }
+
+        // Rule 0.
+        {
+            auto servers = m_result["list"][2]["servers"];
+            auto channels = m_result["list"][2]["channels"];
+            auto plugins = m_result["list"][2]["plugins"];
+            auto events = m_result["list"][2]["events"];
+
+            ASSERT_TRUE(contains(servers, "s0"));
+            ASSERT_TRUE(contains(channels, "c0"));
+            ASSERT_TRUE(contains(plugins, "p0"));
+            ASSERT_TRUE(contains(events, "onMessage"));
+            ASSERT_EQ("drop", m_result["list"][2]["action"].get<std::string>());
+        }
+    } catch (const std::exception& ex) {
+        FAIL() << ex.what();
+    }
+}
+
+TEST_F(RuleMoveCommandTest, outOfBounds)
+{
+    try {
+        m_irccdctl.client().request({
+            { "command",    "rule-move" },
+            { "from",       1024        },
+            { "to",         0           }
+        });
+
+        poll([&] () {
+            return m_result.is_object();
+        });
+
+        ASSERT_TRUE(m_result.is_object());
+        ASSERT_FALSE(m_result["status"].get<bool>());
+    } catch (const std::exception& ex) {
+        FAIL() << ex.what();
+    }
+}
+
+int main(int argc, char **argv)
+{
+    testing::InitGoogleTest(&argc, argv);
+
+    return RUN_ALL_TESTS();
+}