changeset 453:acb2d4990249

Irccdctl: implement rule-edit
author David Demelier <markand@malikania.fr>
date Thu, 20 Jul 2017 22:55:02 +0200
parents 2170aa0e38aa
children 9bb6bf1cb50b
files doc/html/CMakeLists.txt doc/html/irccdctl/command/index.md doc/html/irccdctl/command/rule-edit.md irccd/main.cpp irccdctl/cli.cpp irccdctl/cli.hpp irccdctl/main.cpp libirccd/irccd/command.cpp libirccd/irccd/command.hpp libirccd/irccd/rule.cpp libirccd/irccd/rule.hpp libirccd/irccd/service.cpp libirccd/irccd/service.hpp tests/CMakeLists.txt tests/cmd-rule-edit/CMakeLists.txt tests/cmd-rule-edit/main.cpp
diffstat 16 files changed, 853 insertions(+), 0 deletions(-) [+]
line wrap: on
line diff
--- a/doc/html/CMakeLists.txt	Thu Jul 20 22:56:34 2017 +0200
+++ b/doc/html/CMakeLists.txt	Thu Jul 20 22:55:02 2017 +0200
@@ -153,6 +153,7 @@
     ${html_SOURCE_DIR}/irccdctl/command/plugin-reload.md
     ${html_SOURCE_DIR}/irccdctl/command/plugin-unload.md
     ${html_SOURCE_DIR}/irccdctl/command/rule-add.md
+    ${html_SOURCE_DIR}/irccdctl/command/rule-edit.md
     ${html_SOURCE_DIR}/irccdctl/command/rule-info.md
     ${html_SOURCE_DIR}/irccdctl/command/rule-list.md
     ${html_SOURCE_DIR}/irccdctl/command/rule-move.md
--- a/doc/html/irccdctl/command/index.md	Thu Jul 20 22:56:34 2017 +0200
+++ b/doc/html/irccdctl/command/index.md	Thu Jul 20 22:55:02 2017 +0200
@@ -13,6 +13,7 @@
   - [plugin-reload](plugin-reload.html)
   - [plugin-unload](plugin-unload.html)
   - [rule-add](rule-add.html)
+  - [rule-edit](rule-edit.html)
   - [rule-info](rule-info.html)
   - [rule-list](rule-list.html)
   - [rule-move](rule-move.html)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/html/irccdctl/command/rule-edit.md	Thu Jul 20 22:55:02 2017 +0200
@@ -0,0 +1,35 @@
+---
+title: rule-edit
+guide: yes
+---
+
+# rule-edit
+
+Edit an existing rule in irccd.
+
+All options can be specified multiple times.
+
+Available options:
+
+  - **a, --action**: set action
+  - **c, --add-channel**: match a channel
+  - **C, --remove-channel**: remove a channel
+  - **e, --add-event**: match an event
+  - **E, --remove-event**: remove an event
+  - **p, --add-plugin**: match a plugin
+  - **P, --add-plugin**: remove a plugin
+  - **s, --add-server**: match a server
+  - **S, --remove-server**: remove a server
+
+## Usage
+
+````nohighlight
+usage: irccdctl rule-edit [options] index
+````
+
+## Example
+
+````nohighlight
+$ irccdctl rule-edit -p hangman 0
+$ irccdctl rule-edit -S localhost -c #games -p hangman 1
+````
--- a/irccd/main.cpp	Thu Jul 20 22:56:34 2017 +0200
+++ b/irccd/main.cpp	Thu Jul 20 22:55:02 2017 +0200
@@ -321,6 +321,7 @@
     instance->commands().add(std::make_unique<command::ServerReconnectCommand>());
     instance->commands().add(std::make_unique<command::ServerTopicCommand>());
     instance->commands().add(std::make_unique<command::RuleAddCommand>());
+    instance->commands().add(std::make_unique<command::RuleEditCommand>());
     instance->commands().add(std::make_unique<command::RuleInfoCommand>());
     instance->commands().add(std::make_unique<command::RuleListCommand>());
     instance->commands().add(std::make_unique<command::RuleMoveCommand>());
--- a/irccdctl/cli.cpp	Thu Jul 20 22:56:34 2017 +0200
+++ b/irccdctl/cli.cpp	Thu Jul 20 22:55:02 2017 +0200
@@ -923,6 +923,102 @@
 }
 
 /*
+ * RuleEditCli.
+ * ------------------------------------------------------------------
+ */
+
+RuleEditCli::RuleEditCli()
+    : Cli("rule-edit",
+          "edit an existing rule",
+          "rule-edit [options] index",
+          "Edit an existing rule in irccd.\n\n"
+          "All options can be specified multiple times.\n\n"
+          "Available options:\n"
+          "  -a, --action\t\t\tset action\n"
+          "  -c, --add-channel\t\tmatch a channel\n"
+          "  -C, --remove-channel\t\tremove a channel\n"
+          "  -e, --add-event\t\tmatch an event\n"
+          "  -E, --remove-event\t\tremove an event\n"
+          "  -p, --add-plugin\t\tmatch a plugin\n"
+          "  -P, --add-plugin\t\tremove a plugin\n"
+          "  -s, --add-server\t\tmatch a server\n"
+          "  -S, --remove-server\t\tremove a server\n\n"
+          "Example:\n"
+          "\tirccdctl rule-edit -p hangman 0\n"
+          "\tirccdctl rule-edit -S localhost -c #games -p hangman 1")
+{
+}
+
+void RuleEditCli::exec(Irccdctl &irccdctl, const std::vector<std::string> &args)
+{
+    static const option::Options options{
+        { "-a",                 true },
+        { "--action",           true },
+        { "-c",                 true },
+        { "--add-channel",      true },
+        { "-C",                 true },
+        { "--remove-channel",   true },
+        { "-e",                 true },
+        { "--add-event",        true },
+        { "-E",                 true },
+        { "--remove-event",     true },
+        { "-p",                 true },
+        { "--add-plugin",       true },
+        { "-P",                 true },
+        { "--remove-plugin",    true },
+        { "-s",                 true },
+        { "--add-server",       true },
+        { "-S",                 true },
+        { "--remove-server",    true },
+    };
+
+    auto copy = args;
+    auto result = option::read(copy, options);
+
+    if (copy.size() < 1)
+        throw std::invalid_argument("rule-edit requires at least 1 argument");
+
+    auto json = nlohmann::json::object({
+        { "command",    "rule-edit"             },
+        { "channels",   nlohmann::json::array() },
+        { "events",     nlohmann::json::array() },
+        { "plugins",    nlohmann::json::array() },
+        { "servers",    nlohmann::json::array() }
+    });
+
+    for (const auto& pair : result) {
+        // Action.
+        if (pair.first == "-a" || pair.first == "--action")
+            json["action"] = pair.second;
+
+        // Additions.
+        if (pair.first == "-c" || pair.first == "--add-channel")
+            json["add-channels"].push_back(pair.second);
+        if (pair.first == "-e" || pair.first == "--add-event")
+            json["add-events"].push_back(pair.second);
+        if (pair.first == "-p" || pair.first == "--add-plugin")
+            json["add-plugins"].push_back(pair.second);
+        if (pair.first == "-s" || pair.first == "--add-server")
+            json["add-servers"].push_back(pair.second);
+
+        // Removals.
+        if (pair.first == "-C" || pair.first == "--remove-channel")
+            json["remove-channels"].push_back(pair.second);
+        if (pair.first == "-E" || pair.first == "--remove-event")
+            json["remove-events"].push_back(pair.second);
+        if (pair.first == "-P" || pair.first == "--remove-plugin")
+            json["remove-plugins"].push_back(pair.second);
+        if (pair.first == "-S" || pair.first == "--remove-server")
+            json["remove-servers"].push_back(pair.second);
+    }
+
+    // Index.
+    json["index"] = util::toNumber<unsigned>(copy[0]);
+
+    check(request(irccdctl, json));
+}
+
+/*
  * RuleListCli.
  * ------------------------------------------------------------------
  */
--- a/irccdctl/cli.hpp	Thu Jul 20 22:56:34 2017 +0200
+++ b/irccdctl/cli.hpp	Thu Jul 20 22:55:02 2017 +0200
@@ -607,6 +607,27 @@
 };
 
 /*
+ * RuleEditCli.
+ * ------------------------------------------------------------------
+ */
+
+/**
+ * \brief Implementation of irccdctl rule-edit.
+ */
+class RuleEditCli : public Cli {
+public:
+    /**
+     * Default constructor.
+     */
+    RuleEditCli();
+
+    /**
+     * \copydoc Cli::exec
+     */
+    void exec(Irccdctl &client, const std::vector<std::string> &args) override;
+};
+
+/*
  * RuleListCli.
  * ------------------------------------------------------------------
  */
--- a/irccdctl/main.cpp	Thu Jul 20 22:56:34 2017 +0200
+++ b/irccdctl/main.cpp	Thu Jul 20 22:55:02 2017 +0200
@@ -523,6 +523,7 @@
     commands.push_back(std::make_unique<cli::ServerReconnectCli>());
     commands.push_back(std::make_unique<cli::ServerTopicCli>());
     commands.push_back(std::make_unique<cli::RuleAddCli>());
+    commands.push_back(std::make_unique<cli::RuleEditCli>());
     commands.push_back(std::make_unique<cli::RuleListCli>());
     commands.push_back(std::make_unique<cli::RuleInfoCli>());
     commands.push_back(std::make_unique<cli::RuleMoveCli>());
--- a/libirccd/irccd/command.cpp	Thu Jul 20 22:56:34 2017 +0200
+++ b/libirccd/irccd/command.cpp	Thu Jul 20 22:55:02 2017 +0200
@@ -487,6 +487,56 @@
     client.success("server-topic");
 }
 
+RuleEditCommand::RuleEditCommand()
+    : Command("rule-edit")
+{
+}
+
+void RuleEditCommand::exec(Irccd &irccd, TransportClient &client, const nlohmann::json &args)
+{
+    static const auto updateset = [] (auto &set, auto args, const auto &key) {
+        for (const auto &v : args["remove-"s + key]) {
+            if (v.is_string())
+                set.erase(v.template get<std::string>());
+        }
+        for (const auto &v : args["add-"s + key]) {
+            if (v.is_string())
+                set.insert(v.template get<std::string>());
+        }
+    };
+
+    // Create a copy to avoid incomplete edition in case of errors.
+    auto index = util::json::requireUint(args, "index");
+    auto rule = irccd.rules().require(index);
+
+    updateset(rule.channels(), args, "channels");
+    updateset(rule.events(), args, "events");
+    updateset(rule.plugins(), args, "plugins");
+    updateset(rule.servers(), args, "servers");
+
+    auto action = args.find("action");
+
+    if (action != args.end()) {
+        if (!action->is_string()) {
+            client.error("rule-edit", "action must be \"accept\" or \"drop\"");
+            return;
+        }
+
+        if (action->get<std::string>() == "accept")
+            rule.setAction(RuleAction::Accept);
+        else if (action->get<std::string>() == "drop")
+            rule.setAction(RuleAction::Drop);
+        else {
+            client.error("rule-edit", "invalid action '"s + action->get<std::string>() + "'");
+            return;
+        }
+    }
+
+    // All done, sync the rule.
+    irccd.rules().require(index) = rule;
+    client.success("rule-edit");
+}
+
 RuleListCommand::RuleListCommand()
     : Command("rule-list")
 {
--- a/libirccd/irccd/command.hpp	Thu Jul 20 22:56:34 2017 +0200
+++ b/libirccd/irccd/command.hpp	Thu Jul 20 22:55:02 2017 +0200
@@ -458,6 +458,19 @@
     void exec(Irccd &irccd, TransportClient &client, const nlohmann::json &args) override;
 };
 
+class IRCCD_EXPORT RuleEditCommand : public Command {
+public:
+    /**
+     * Constructor.
+     */
+    RuleEditCommand();
+
+    /**
+     * \copydoc Command::exec
+     */
+    void exec(Irccd &irccd, TransportClient &client, const nlohmann::json &args) override;
+};
+
 /**
  * \brief Implementation of rule-list transport command.
  */
--- a/libirccd/irccd/rule.cpp	Thu Jul 20 22:56:34 2017 +0200
+++ b/libirccd/irccd/rule.cpp	Thu Jul 20 22:55:02 2017 +0200
@@ -16,6 +16,7 @@
  * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
  */
 
+#include <cassert>
 #include <stdexcept>
 
 #include "logger.hpp"
@@ -86,6 +87,13 @@
     return m_action;
 }
 
+void Rule::setAction(RuleAction action) noexcept
+{
+    assert(action == RuleAction::Accept || action == RuleAction::Drop);
+
+    m_action = action;
+}
+
 const RuleSet &Rule::servers() const noexcept
 {
     return m_servers;
--- a/libirccd/irccd/rule.hpp	Thu Jul 20 22:56:34 2017 +0200
+++ b/libirccd/irccd/rule.hpp	Thu Jul 20 22:55:02 2017 +0200
@@ -109,6 +109,13 @@
     IRCCD_EXPORT RuleAction action() const noexcept;
 
     /**
+     * Set the action.
+     *
+     * \pre action must be valid
+     */
+    IRCCD_EXPORT void setAction(RuleAction action) noexcept;
+
+    /**
      * Get the servers.
      *
      * \return the servers
@@ -116,12 +123,33 @@
     IRCCD_EXPORT const RuleSet &servers() const noexcept;
 
     /**
+     * Overloaded function.
+     *
+     * \return the servers
+     */
+    inline RuleSet &servers() noexcept
+    {
+        return m_servers;
+    }
+
+    /**
      * Get the channels.
      *
      * \return the channels
      */
     IRCCD_EXPORT const RuleSet &channels() const noexcept;
 
+
+    /**
+     * Overloaded function.
+     *
+     * \return the channels
+     */
+    inline RuleSet &channels() noexcept
+    {
+        return m_channels;
+    }
+
     /**
      * Get the origins.
      *
@@ -136,12 +164,34 @@
      */
     IRCCD_EXPORT const RuleSet &plugins() const noexcept;
 
+
+    /**
+     * Overloaded function.
+     *
+     * \return the plugins
+     */
+    inline RuleSet &plugins() noexcept
+    {
+        return m_plugins;
+    }
+
     /**
      * Get the events.
      *
      * \return the events
      */
     IRCCD_EXPORT const RuleSet &events() const noexcept;
+
+
+    /**
+     * Overloaded function.
+     *
+     * \return the events
+     */
+    inline RuleSet& events() noexcept
+    {
+        return m_events;
+    }
 };
 
 } // !irccd
--- a/libirccd/irccd/service.cpp	Thu Jul 20 22:56:34 2017 +0200
+++ b/libirccd/irccd/service.cpp	Thu Jul 20 22:55:02 2017 +0200
@@ -304,6 +304,14 @@
     return m_rules[position];
 }
 
+Rule &RuleService::require(unsigned position)
+{
+    if (position >= m_rules.size())
+        throw std::out_of_range("rule " + std::to_string(position) + " does not exist");
+
+    return m_rules[position];
+}
+
 bool RuleService::solve(const std::string &server,
                         const std::string &channel,
                         const std::string &origin,
--- a/libirccd/irccd/service.hpp	Thu Jul 20 22:56:34 2017 +0200
+++ b/libirccd/irccd/service.hpp	Thu Jul 20 22:55:02 2017 +0200
@@ -356,6 +356,13 @@
     IRCCD_EXPORT const Rule &require(unsigned position) const;
 
     /**
+     * Overloaded function.
+     *
+     * \copydoc require
+     */
+    IRCCD_EXPORT Rule& require(unsigned position);
+
+    /**
      * Resolve the action to execute with the specified list of rules.
      *
      * \param server the server name
--- a/tests/CMakeLists.txt	Thu Jul 20 22:56:34 2017 +0200
+++ b/tests/CMakeLists.txt	Thu Jul 20 22:55:02 2017 +0200
@@ -27,6 +27,7 @@
     add_subdirectory(cmd-plugin-reload)
     add_subdirectory(cmd-plugin-unload)
     add_subdirectory(cmd-rule-add)
+    add_subdirectory(cmd-rule-edit)
     add_subdirectory(cmd-rule-info)
     add_subdirectory(cmd-rule-list)
     add_subdirectory(cmd-rule-move)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/cmd-rule-edit/CMakeLists.txt	Thu Jul 20 22:55:02 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-edit
+    SOURCES main.cpp
+    LIBRARIES libirccd libirccdctl
+)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/cmd-rule-edit/main.cpp	Thu Jul 20 22:55:02 2017 +0200
@@ -0,0 +1,537 @@
+/*
+ * 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.
+ */
+
+#include <command.hpp>
+#include <command-tester.hpp>
+#include <service.hpp>
+
+using namespace irccd;
+using namespace irccd::command;
+
+class RuleEditCommandTest : 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:
+    RuleEditCommandTest()
+        : CommandTester(std::make_unique<RuleEditCommand>())
+    {
+        m_irccd.commands().add(std::make_unique<RuleInfoCommand>());
+        m_irccd.rules().add(Rule(
+            { "s1", "s2" },
+            { "c1", "c2" },
+            { "o1", "o2" },
+            { "p1", "p2" },
+            { "onMessage", "onCommand" },
+            RuleAction::Drop
+        ));
+        m_irccdctl.client().onMessage.connect([&] (auto result) {
+            m_result = result;
+        });
+    }
+};
+
+TEST_F(RuleEditCommandTest, addServer)
+{
+    try {
+        m_irccdctl.client().request({
+            { "command",        "rule-edit"     },
+            { "add-servers",    { "new-s3" }    },
+            { "index",          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-info" },
+            { "index", 0 }
+        });
+
+        poll([&] () {
+            return m_result.is_object();
+        });
+
+        ASSERT_TRUE(m_result.is_object());
+        ASSERT_TRUE(m_result["status"].get<bool>());
+        ASSERT_TRUE(contains(m_result["servers"], "s1"));
+        ASSERT_TRUE(contains(m_result["servers"], "s2"));
+        ASSERT_TRUE(contains(m_result["servers"], "new-s3"));
+        ASSERT_TRUE(contains(m_result["channels"], "c1"));
+        ASSERT_TRUE(contains(m_result["channels"], "c2"));
+        ASSERT_TRUE(contains(m_result["plugins"], "p1"));
+        ASSERT_TRUE(contains(m_result["plugins"], "p2"));
+        ASSERT_TRUE(contains(m_result["events"], "onMessage"));
+        ASSERT_TRUE(contains(m_result["events"], "onCommand"));
+        ASSERT_EQ(m_result["action"].get<std::string>(), "drop");
+    } catch (const std::exception &ex) {
+        FAIL() << ex.what();
+    }
+}
+
+TEST_F(RuleEditCommandTest, addChannel)
+{
+    try {
+        m_irccdctl.client().request({
+            { "command",        "rule-edit"     },
+            { "add-channels",   { "new-c3" }    },
+            { "index",          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-info" },
+            { "index", 0 }
+        });
+
+        poll([&] () {
+            return m_result.is_object();
+        });
+
+        ASSERT_TRUE(m_result.is_object());
+        ASSERT_TRUE(m_result["status"].get<bool>());
+        ASSERT_TRUE(contains(m_result["servers"], "s1"));
+        ASSERT_TRUE(contains(m_result["servers"], "s2"));
+        ASSERT_TRUE(contains(m_result["channels"], "c1"));
+        ASSERT_TRUE(contains(m_result["channels"], "c2"));
+        ASSERT_TRUE(contains(m_result["channels"], "new-c3"));
+        ASSERT_TRUE(contains(m_result["plugins"], "p1"));
+        ASSERT_TRUE(contains(m_result["plugins"], "p2"));
+        ASSERT_TRUE(contains(m_result["events"], "onMessage"));
+        ASSERT_TRUE(contains(m_result["events"], "onCommand"));
+        ASSERT_EQ(m_result["action"].get<std::string>(), "drop");
+    } catch (const std::exception &ex) {
+        FAIL() << ex.what();
+    }
+}
+
+TEST_F(RuleEditCommandTest, addPlugin)
+{
+    try {
+        m_irccdctl.client().request({
+            { "command",        "rule-edit"     },
+            { "add-plugins",    { "new-p3" }    },
+            { "index",          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-info" },
+            { "index", 0 }
+        });
+
+        poll([&] () {
+            return m_result.is_object();
+        });
+
+        ASSERT_TRUE(m_result.is_object());
+        ASSERT_TRUE(m_result["status"].get<bool>());
+        ASSERT_TRUE(contains(m_result["servers"], "s1"));
+        ASSERT_TRUE(contains(m_result["servers"], "s2"));
+        ASSERT_TRUE(contains(m_result["channels"], "c1"));
+        ASSERT_TRUE(contains(m_result["channels"], "c2"));
+        ASSERT_TRUE(contains(m_result["plugins"], "p1"));
+        ASSERT_TRUE(contains(m_result["plugins"], "p2"));
+        ASSERT_TRUE(contains(m_result["plugins"], "new-p3"));
+        ASSERT_TRUE(contains(m_result["events"], "onMessage"));
+        ASSERT_TRUE(contains(m_result["events"], "onCommand"));
+        ASSERT_EQ(m_result["action"].get<std::string>(), "drop");
+    } catch (const std::exception &ex) {
+        FAIL() << ex.what();
+    }
+}
+
+TEST_F(RuleEditCommandTest, addEvent)
+{
+    try {
+        m_irccdctl.client().request({
+            { "command",        "rule-edit"     },
+            { "add-events",     { "onQuery" }   },
+            { "index",          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-info" },
+            { "index", 0 }
+        });
+
+        poll([&] () {
+            return m_result.is_object();
+        });
+
+        ASSERT_TRUE(m_result.is_object());
+        ASSERT_TRUE(m_result["status"].get<bool>());
+        ASSERT_TRUE(contains(m_result["servers"], "s1"));
+        ASSERT_TRUE(contains(m_result["servers"], "s2"));
+        ASSERT_TRUE(contains(m_result["channels"], "c1"));
+        ASSERT_TRUE(contains(m_result["channels"], "c2"));
+        ASSERT_TRUE(contains(m_result["plugins"], "p1"));
+        ASSERT_TRUE(contains(m_result["plugins"], "p2"));
+        ASSERT_TRUE(contains(m_result["events"], "onMessage"));
+        ASSERT_TRUE(contains(m_result["events"], "onCommand"));
+        ASSERT_TRUE(contains(m_result["events"], "onQuery"));
+        ASSERT_EQ(m_result["action"].get<std::string>(), "drop");
+    } catch (const std::exception &ex) {
+        FAIL() << ex.what();
+    }
+}
+
+TEST_F(RuleEditCommandTest, addEventAndServer)
+{
+    try {
+        m_irccdctl.client().request({
+            { "command",        "rule-edit"     },
+            { "add-servers",    { "new-s3" }    },
+            { "add-events",     { "onQuery" }   },
+            { "index",          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-info" },
+            { "index", 0 }
+        });
+
+        poll([&] () {
+            return m_result.is_object();
+        });
+
+        ASSERT_TRUE(m_result.is_object());
+        ASSERT_TRUE(m_result["status"].get<bool>());
+        ASSERT_TRUE(contains(m_result["servers"], "s1"));
+        ASSERT_TRUE(contains(m_result["servers"], "s2"));
+        ASSERT_TRUE(contains(m_result["servers"], "new-s3"));
+        ASSERT_TRUE(contains(m_result["channels"], "c1"));
+        ASSERT_TRUE(contains(m_result["channels"], "c2"));
+        ASSERT_TRUE(contains(m_result["plugins"], "p1"));
+        ASSERT_TRUE(contains(m_result["plugins"], "p2"));
+        ASSERT_TRUE(contains(m_result["events"], "onMessage"));
+        ASSERT_TRUE(contains(m_result["events"], "onCommand"));
+        ASSERT_TRUE(contains(m_result["events"], "onQuery"));
+        ASSERT_EQ(m_result["action"].get<std::string>(), "drop");
+    } catch (const std::exception &ex) {
+        FAIL() << ex.what();
+    }
+}
+
+TEST_F(RuleEditCommandTest, changeAction)
+{
+    try {
+        m_irccdctl.client().request({
+            { "command",        "rule-edit"     },
+            { "action",         "accept"        },
+            { "index",          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-info" },
+            { "index", 0 }
+        });
+
+        poll([&] () {
+            return m_result.is_object();
+        });
+
+        ASSERT_TRUE(m_result.is_object());
+        ASSERT_TRUE(m_result["status"].get<bool>());
+        ASSERT_TRUE(contains(m_result["servers"], "s1"));
+        ASSERT_TRUE(contains(m_result["servers"], "s2"));
+        ASSERT_TRUE(contains(m_result["channels"], "c1"));
+        ASSERT_TRUE(contains(m_result["channels"], "c2"));
+        ASSERT_TRUE(contains(m_result["plugins"], "p1"));
+        ASSERT_TRUE(contains(m_result["plugins"], "p2"));
+        ASSERT_TRUE(contains(m_result["events"], "onMessage"));
+        ASSERT_TRUE(contains(m_result["events"], "onCommand"));
+        ASSERT_EQ(m_result["action"].get<std::string>(), "accept");
+    } catch (const std::exception &ex) {
+        FAIL() << ex.what();
+    }
+}
+
+TEST_F(RuleEditCommandTest, removeServer)
+{
+    try {
+        m_irccdctl.client().request({
+            { "command",        "rule-edit"     },
+            { "remove-servers", { "s2" }        },
+            { "index",          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-info" },
+            { "index", 0 }
+        });
+
+        poll([&] () {
+            return m_result.is_object();
+        });
+
+        ASSERT_TRUE(m_result.is_object());
+        ASSERT_TRUE(m_result["status"].get<bool>());
+        ASSERT_TRUE(contains(m_result["servers"], "s1"));
+        ASSERT_FALSE(contains(m_result["servers"], "s2"));
+        ASSERT_TRUE(contains(m_result["channels"], "c1"));
+        ASSERT_TRUE(contains(m_result["channels"], "c2"));
+        ASSERT_TRUE(contains(m_result["plugins"], "p1"));
+        ASSERT_TRUE(contains(m_result["plugins"], "p2"));
+        ASSERT_TRUE(contains(m_result["events"], "onMessage"));
+        ASSERT_TRUE(contains(m_result["events"], "onCommand"));
+        ASSERT_EQ(m_result["action"].get<std::string>(), "drop");
+    } catch (const std::exception &ex) {
+        FAIL() << ex.what();
+    }
+}
+
+TEST_F(RuleEditCommandTest, removeChannel)
+{
+    try {
+        m_irccdctl.client().request({
+            { "command",        "rule-edit"     },
+            { "remove-channels", { "c2" }       },
+            { "index",          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-info" },
+            { "index", 0 }
+        });
+
+        poll([&] () {
+            return m_result.is_object();
+        });
+
+        ASSERT_TRUE(m_result.is_object());
+        ASSERT_TRUE(m_result["status"].get<bool>());
+        ASSERT_TRUE(contains(m_result["servers"], "s1"));
+        ASSERT_TRUE(contains(m_result["servers"], "s2"));
+        ASSERT_TRUE(contains(m_result["channels"], "c1"));
+        ASSERT_FALSE(contains(m_result["channels"], "c2"));
+        ASSERT_TRUE(contains(m_result["plugins"], "p1"));
+        ASSERT_TRUE(contains(m_result["plugins"], "p2"));
+        ASSERT_TRUE(contains(m_result["events"], "onMessage"));
+        ASSERT_TRUE(contains(m_result["events"], "onCommand"));
+        ASSERT_EQ(m_result["action"].get<std::string>(), "drop");
+    } catch (const std::exception &ex) {
+        FAIL() << ex.what();
+    }
+}
+
+TEST_F(RuleEditCommandTest, removePlugin)
+{
+    try {
+        m_irccdctl.client().request({
+            { "command",        "rule-edit"     },
+            { "remove-plugins", { "p2" }        },
+            { "index",          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-info" },
+            { "index", 0 }
+        });
+
+        poll([&] () {
+            return m_result.is_object();
+        });
+
+        ASSERT_TRUE(m_result.is_object());
+        ASSERT_TRUE(m_result["status"].get<bool>());
+        ASSERT_TRUE(contains(m_result["servers"], "s1"));
+        ASSERT_TRUE(contains(m_result["servers"], "s2"));
+        ASSERT_TRUE(contains(m_result["channels"], "c1"));
+        ASSERT_TRUE(contains(m_result["channels"], "c2"));
+        ASSERT_TRUE(contains(m_result["plugins"], "p1"));
+        ASSERT_FALSE(contains(m_result["plugins"], "p2"));
+        ASSERT_TRUE(contains(m_result["events"], "onMessage"));
+        ASSERT_TRUE(contains(m_result["events"], "onCommand"));
+        ASSERT_EQ(m_result["action"].get<std::string>(), "drop");
+    } catch (const std::exception &ex) {
+        FAIL() << ex.what();
+    }
+}
+
+TEST_F(RuleEditCommandTest, removeEvent)
+{
+    try {
+        m_irccdctl.client().request({
+            { "command",        "rule-edit"     },
+            { "remove-events",  { "onCommand" } },
+            { "index",          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-info" },
+            { "index", 0 }
+        });
+
+        poll([&] () {
+            return m_result.is_object();
+        });
+
+        ASSERT_TRUE(m_result.is_object());
+        ASSERT_TRUE(m_result["status"].get<bool>());
+        ASSERT_TRUE(contains(m_result["servers"], "s1"));
+        ASSERT_TRUE(contains(m_result["servers"], "s2"));
+        ASSERT_TRUE(contains(m_result["channels"], "c1"));
+        ASSERT_TRUE(contains(m_result["channels"], "c2"));
+        ASSERT_TRUE(contains(m_result["plugins"], "p1"));
+        ASSERT_TRUE(contains(m_result["plugins"], "p2"));
+        ASSERT_TRUE(contains(m_result["events"], "onMessage"));
+        ASSERT_FALSE(contains(m_result["events"], "onCommand"));
+        ASSERT_EQ(m_result["action"].get<std::string>(), "drop");
+    } catch (const std::exception &ex) {
+        FAIL() << ex.what();
+    }
+}
+
+TEST_F(RuleEditCommandTest, removeEventAndServer)
+{
+    try {
+        m_irccdctl.client().request({
+            { "command",        "rule-edit"     },
+            { "remove-servers", { "s2" }        },
+            { "remove-events",  { "onCommand" } },
+            { "index",          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-info" },
+            { "index", 0 }
+        });
+
+        poll([&] () {
+            return m_result.is_object();
+        });
+
+        ASSERT_TRUE(m_result.is_object());
+        ASSERT_TRUE(m_result["status"].get<bool>());
+        ASSERT_TRUE(contains(m_result["servers"], "s1"));
+        ASSERT_FALSE(contains(m_result["servers"], "s2"));
+        ASSERT_TRUE(contains(m_result["channels"], "c1"));
+        ASSERT_TRUE(contains(m_result["channels"], "c2"));
+        ASSERT_TRUE(contains(m_result["plugins"], "p1"));
+        ASSERT_TRUE(contains(m_result["plugins"], "p2"));
+        ASSERT_TRUE(contains(m_result["events"], "onMessage"));
+        ASSERT_FALSE(contains(m_result["events"], "onCommand"));
+        ASSERT_EQ(m_result["action"].get<std::string>(), "drop");
+    } catch (const std::exception &ex) {
+        FAIL() << ex.what();
+    }
+}
+
+int main(int argc, char **argv)
+{
+    testing::InitGoogleTest(&argc, argv);
+
+    return RUN_ALL_TESTS();
+}