From 040eee1aa49b49df4698d83a05af57c220127fd1 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sat, 4 May 2024 13:36:04 +0200 Subject: Adding upstream version 2.2.0. Signed-off-by: Daniel Baumann --- src/lib/config/tests/command_mgr_unittests.cc | 570 ++++++++++++++++++++++++++ 1 file changed, 570 insertions(+) create mode 100644 src/lib/config/tests/command_mgr_unittests.cc (limited to 'src/lib/config/tests/command_mgr_unittests.cc') diff --git a/src/lib/config/tests/command_mgr_unittests.cc b/src/lib/config/tests/command_mgr_unittests.cc new file mode 100644 index 0000000..b607d3e --- /dev/null +++ b/src/lib/config/tests/command_mgr_unittests.cc @@ -0,0 +1,570 @@ +// Copyright (C) 2015-2020 Internet Systems Consortium, Inc. ("ISC") +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +#include + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace isc::asiolink; +using namespace isc::config; +using namespace isc::data; +using namespace isc::hooks; +using namespace std; + +// Test class for Command Manager +class CommandMgrTest : public ::testing::Test { +public: + isc::test::Sandbox sandbox; + + /// Default constructor + CommandMgrTest() + : io_service_(new IOService()) { + + CommandMgr::instance().setIOService(io_service_); + + handler_name_ = ""; + handler_params_ = ElementPtr(); + handler_called_ = false; + callout_name_ = ""; + callout_argument_names_.clear(); + std::string processed_log_ = ""; + + CommandMgr::instance().deregisterAll(); + CommandMgr::instance().closeCommandSocket(); + + resetCalloutIndicators(); + } + + /// Default destructor + virtual ~CommandMgrTest() { + CommandMgr::instance().deregisterAll(); + CommandMgr::instance().closeCommandSocket(); + resetCalloutIndicators(); + } + + /// @brief Returns socket path (using either hardcoded path or env variable) + /// @return path to the unix socket + std::string getSocketPath() { + std::string socket_path; + const char* env = getenv("KEA_SOCKET_TEST_DIR"); + if (env) { + socket_path = std::string(env) + "/test-socket"; + } else { + socket_path = sandbox.join("test-socket"); + } + return (socket_path); + } + + /// @brief Resets indicators related to callout invocation. + /// + /// It also removes any registered callouts. + static void resetCalloutIndicators() { + callout_name_ = ""; + callout_argument_names_.clear(); + + // Iterate over existing hook points and for each of them remove + // callouts registered. + std::vector hooks = ServerHooks::getServerHooksPtr()->getHookNames(); + for (auto h = hooks.cbegin(); h != hooks.cend(); ++h) { + HooksManager::preCalloutsLibraryHandle().deregisterAllCallouts(*h); + } + } + + /// @brief A simple command handler that always returns an eror + static ConstElementPtr my_handler(const std::string& name, + const ConstElementPtr& params) { + + handler_name_ = name; + handler_params_ = params; + handler_called_ = true; + + return (createAnswer(123, "test error message")); + } + + /// @brief Test callback which stores callout name and passed arguments and + /// which handles the command. + /// + /// @param callout_handle Handle passed by the hooks framework. + /// @return Always 0. + static int + hook_lib_callout(CalloutHandle& callout_handle) { + callout_name_ = "hook_lib_callout"; + + ConstElementPtr command; + callout_handle.getArgument("command", command); + + ConstElementPtr arg; + std::string command_name = parseCommand(arg, command); + + callout_handle.setArgument("response", + createAnswer(234, "text generated by hook handler")); + + callout_argument_names_ = callout_handle.getArgumentNames(); + // Sort arguments alphabetically, so as we can access them on + // expected positions and verify. + std::sort(callout_argument_names_.begin(), callout_argument_names_.end()); + return (0); + } + + /// @brief Test callback which stores callout name and passed arguments and + /// which handles the command. + /// + /// @param callout_handle Handle passed by the hooks framework. + /// @return Always 0. + static int + command_processed_callout(CalloutHandle& callout_handle) { + callout_name_ = "command_processed_handler"; + + std::string name; + callout_handle.getArgument("name", name); + + ConstElementPtr arguments; + callout_handle.getArgument("arguments", arguments); + + ConstElementPtr response; + callout_handle.getArgument("response", response); + std::ostringstream os; + os << name << ":" << arguments->str() << ":" << response->str(); + processed_log_ = os.str(); + + if (name == "change-response") { + callout_handle.setArgument("response", + createAnswer(777, "replaced response text")); + } + + return (0); + } + + /// @brief IO service used by these tests. + IOServicePtr io_service_; + + /// @brief Name of the command (used in my_handler) + static std::string handler_name_; + + /// @brief Parameters passed to the handler (used in my_handler) + static ConstElementPtr handler_params_; + + /// @brief Indicates whether my_handler was called + static bool handler_called_; + + /// @brief Holds invoked callout name. + static std::string callout_name_; + + /// @brief Holds a list of arguments passed to the callout. + static std::vector callout_argument_names_; + + /// @brief Holds the generated command process log + static std::string processed_log_; +}; + +/// Name passed to the handler (used in my_handler) +std::string CommandMgrTest::handler_name_(""); + +/// Parameters passed to the handler (used in my_handler) +ConstElementPtr CommandMgrTest::handler_params_; + +/// Indicates whether my_handler was called +bool CommandMgrTest::handler_called_(false); + +/// Holds invoked callout name. +std::string CommandMgrTest::callout_name_(""); + +/// @brief Holds the generated command process log +std::string CommandMgrTest::processed_log_; + +/// Holds a list of arguments passed to the callout. +std::vector CommandMgrTest::callout_argument_names_; + +// Test checks whether the internal command 'list-commands' +// is working properly. +TEST_F(CommandMgrTest, listCommandsEmpty) { + + ConstElementPtr command = createCommand("list-commands"); + + ConstElementPtr answer; + + EXPECT_NO_THROW(answer = CommandMgr::instance().processCommand(command)); + + ASSERT_TRUE(answer); + + EXPECT_EQ("{ \"arguments\": [ \"list-commands\" ], \"result\": 0 }", + answer->str()); +} + +// Test checks whether calling a bogus command is handled properly. +TEST_F(CommandMgrTest, bogusCommand) { + + ConstElementPtr command = createCommand("no-such-command"); + + ConstElementPtr answer; + + EXPECT_NO_THROW(answer = CommandMgr::instance().processCommand(command)); + + // Make sure the status code is non-zero + ASSERT_TRUE(answer); + int status_code; + parseAnswer(status_code, answer); + EXPECT_EQ(CONTROL_RESULT_COMMAND_UNSUPPORTED, status_code); +} + +// Test checks whether handlers installation is sanitized. In particular, +// whether NULL handler and attempt to install handlers for the same +// command twice are rejected. +TEST_F(CommandMgrTest, handlerInstall) { + + // Check that it's not allowed to install NULL pointer instead of a real + // command. + EXPECT_THROW(CommandMgr::instance().registerCommand("my-command", 0), + InvalidCommandHandler); + + // This registration should succeed. + EXPECT_NO_THROW(CommandMgr::instance().registerCommand("my-command", + my_handler)); + + // Check that it's not possible to install handlers for the same + // command twice. + EXPECT_THROW(CommandMgr::instance().registerCommand("my-command", + my_handler), InvalidCommandName); +} + +// Test checks whether the internal list-commands command is working +// correctly. Also, checks installation and deinstallation of other +// command handlers. +TEST_F(CommandMgrTest, listCommands) { + + // Let's install two custom commands. + EXPECT_NO_THROW(CommandMgr::instance().registerCommand("make-a-coffee", + my_handler)); + EXPECT_NO_THROW(CommandMgr::instance().registerCommand("do-the-dishes", + my_handler)); + + // And then run 'list-commands' + ConstElementPtr list_all = createCommand("list-commands"); + ConstElementPtr answer; + + // Now check that the command is returned by list-commands + EXPECT_NO_THROW(answer = CommandMgr::instance().processCommand(list_all)); + ASSERT_TRUE(answer); + EXPECT_EQ("{ \"arguments\": [ \"do-the-dishes\", \"list-commands\", " + "\"make-a-coffee\" ], \"result\": 0 }", answer->str()); + + // Now unregister one command + EXPECT_NO_THROW(CommandMgr::instance().deregisterCommand("do-the-dishes")); + + // Now check that the command is returned by list-commands + EXPECT_NO_THROW(answer = CommandMgr::instance().processCommand(list_all)); + ASSERT_TRUE(answer); + EXPECT_EQ("{ \"arguments\": [ \"list-commands\", " + "\"make-a-coffee\" ], \"result\": 0 }", answer->str()); + + // Now test deregistration. It should work the first time. + EXPECT_NO_THROW(CommandMgr::instance().deregisterCommand("make-a-coffee")); + + // Second time should throw an exception as the handler is no longer there. + EXPECT_THROW(CommandMgr::instance().deregisterCommand("make-a-coffee"), + InvalidCommandName); + + // You can't uninstall list-commands as it's the internal handler. + // It always must be there. + EXPECT_THROW(CommandMgr::instance().deregisterCommand("list-commands"), + InvalidCommandName); + + // Attempt to register a handler for existing command should fail. + EXPECT_THROW(CommandMgr::instance().registerCommand("list-commands", + my_handler), InvalidCommandName); +} + +// Test checks whether deregisterAll method uninstalls all handlers, +// except list-commands. +TEST_F(CommandMgrTest, deregisterAll) { + + // Install a couple handlers. + EXPECT_NO_THROW(CommandMgr::instance().registerCommand("my-command1", + my_handler)); + EXPECT_NO_THROW(CommandMgr::instance().registerCommand("my-command2", + my_handler)); + + EXPECT_NO_THROW(CommandMgr::instance().deregisterAll()); + + ConstElementPtr answer; + EXPECT_NO_THROW(answer = CommandMgr::instance() + .processCommand(createCommand("list-commands"))); + ASSERT_TRUE(answer); + EXPECT_EQ("{ \"arguments\": [ \"list-commands\" ], \"result\": 0 }", + answer->str()); +} + +// Test checks whether a command handler can be installed and then +// runs through processCommand to check that it's indeed called. +TEST_F(CommandMgrTest, processCommand) { + // Install my handler + EXPECT_NO_THROW(CommandMgr::instance().registerCommand("my-command", + my_handler)); + + // Now tell CommandMgr to process a command 'my-command' with the + // specified parameter. + ElementPtr my_params = Element::fromJSON("[ \"just\", \"some\", \"data\" ]"); + ConstElementPtr command = createCommand("my-command", my_params); + ConstElementPtr answer; + EXPECT_NO_THROW(answer = CommandMgr::instance().processCommand(command)); + + // There should be an answer. + ASSERT_TRUE(answer); + + // my_handler remembers all passed parameters and returns status code of 123. + ConstElementPtr answer_arg; + int status_code; + // Check that the returned status code is correct. + EXPECT_NO_THROW(answer_arg = parseAnswer(status_code, answer)); + EXPECT_EQ(123, status_code); + + // Check that the parameters passed are correct. + EXPECT_EQ(true, handler_called_); + EXPECT_EQ("my-command", handler_name_); + ASSERT_TRUE(handler_params_); + EXPECT_EQ("[ \"just\", \"some\", \"data\" ]", handler_params_->str()); + + // Command handlers not installed so expecting that callouts weren't + // called. + EXPECT_TRUE(callout_name_.empty()); +} + +// Verify that processing a command can be delegated to a hook library. +TEST_F(CommandMgrTest, delegateProcessCommand) { + // Register callout so as we can check that it is called before + // processing the command by the manager. + HooksManager::preCalloutsLibraryHandle().registerCommandCallout( + "my-command", hook_lib_callout); + + // Install local handler + EXPECT_NO_THROW(CommandMgr::instance().registerCommand("my-command", + my_handler)); + + // Now tell CommandMgr to process a command 'my-command' with the + // specified parameter. + ElementPtr my_params = Element::fromJSON("[ \"just\", \"some\", \"data\" ]"); + ConstElementPtr command = createCommand("my-command", my_params); + ConstElementPtr answer; + ASSERT_NO_THROW(answer = CommandMgr::instance().processCommand(command)); + + // There should be an answer. + ASSERT_TRUE(answer); + + // Local handler shouldn't be called because the command is handled by the + // hook library. + ASSERT_FALSE(handler_called_); + + // Returned status should be unique for the hook library. + ConstElementPtr answer_arg; + int status_code; + ASSERT_NO_THROW(answer_arg = parseAnswer(status_code, answer)); + EXPECT_EQ(234, status_code); + + EXPECT_EQ("hook_lib_callout", callout_name_); + + // Check that the appropriate arguments have been set. Include the + // 'response' which should have been set by the callout. + ASSERT_EQ(2, callout_argument_names_.size()); + EXPECT_EQ("command", callout_argument_names_[0]); + EXPECT_EQ("response", callout_argument_names_[1]); +} + +// Verify that 'list-command' command returns combined list of supported +// commands from hook library and from the Kea Command Manager. +TEST_F(CommandMgrTest, delegateListCommands) { + // Register callout so as we can check that it is called before + // processing the command by the manager. + HooksManager::preCalloutsLibraryHandle().registerCommandCallout( + "my-command", hook_lib_callout); + + // Create my-command-bis which is unique for the local Command Manager, + // i.e. not supported by the hook library. This command should also + // be returned as a result of processing 'list-commands'. + EXPECT_NO_THROW(CommandMgr::instance().registerCommand("my-command-bis", + my_handler)); + + // Process command. The command should be routed to the hook library + // and the hook library should return the commands it supports. + ConstElementPtr command = createCommand("list-commands"); + ConstElementPtr answer; + ASSERT_NO_THROW(answer = CommandMgr::instance().processCommand(command)); + + // There should be an answer. + ASSERT_TRUE(answer); + + ConstElementPtr answer_arg; + int status_code; + ASSERT_NO_THROW(answer_arg = parseAnswer(status_code, answer)); + EXPECT_EQ(0, status_code); + + // The hook library supports: my-command and list-commands commands. The + // local Command Manager supports list-commands and my-command-bis. The + // combined list should include 3 unique commands. + const std::vector& commands_list = answer_arg->listValue(); + ASSERT_EQ(3, commands_list.size()); + std::vector command_names_list; + for (auto cmd = commands_list.cbegin(); cmd != commands_list.cend(); + ++cmd) { + command_names_list.push_back((*cmd)->stringValue()); + } + std::sort(command_names_list.begin(), command_names_list.end()); + EXPECT_EQ("list-commands", command_names_list[0]); + EXPECT_EQ("my-command", command_names_list[1]); + EXPECT_EQ("my-command-bis", command_names_list[2]); +} + +// This test verifies that a Unix socket can be opened properly and that input +// parameters (socket-type and socket-name) are verified. +TEST_F(CommandMgrTest, unixCreate) { + // Null pointer is obviously a bad idea. + EXPECT_THROW(CommandMgr::instance().openCommandSocket(ConstElementPtr()), + isc::config::BadSocketInfo); + + // So is passing no parameters. + ElementPtr socket_info = Element::createMap(); + EXPECT_THROW(CommandMgr::instance().openCommandSocket(socket_info), + isc::config::BadSocketInfo); + + // We don't support ipx sockets + socket_info->set("socket-type", Element::create("ipx")); + EXPECT_THROW(CommandMgr::instance().openCommandSocket(socket_info), + isc::config::BadSocketInfo); + + socket_info->set("socket-type", Element::create("unix")); + EXPECT_THROW(CommandMgr::instance().openCommandSocket(socket_info), + isc::config::BadSocketInfo); + + socket_info->set("socket-name", Element::create(getSocketPath())); + EXPECT_NO_THROW(CommandMgr::instance().openCommandSocket(socket_info)); + EXPECT_GE(CommandMgr::instance().getControlSocketFD(), 0); + + // It should be possible to close the socket. + EXPECT_NO_THROW(CommandMgr::instance().closeCommandSocket()); +} + +// This test checks that when unix path is too long, the socket cannot be opened. +TEST_F(CommandMgrTest, unixCreateTooLong) { + ElementPtr socket_info = Element::fromJSON("{ \"socket-type\": \"unix\"," + "\"socket-name\": \"/tmp/toolongtoolongtoolongtoolongtoolongtoolong" + "toolongtoolongtoolongtoolongtoolongtoolongtoolongtoolongtoolong" + "\" }"); + + EXPECT_THROW(CommandMgr::instance().openCommandSocket(socket_info), + SocketError); +} + +// This test verifies that a registered callout for the command_processed +// hookpoint is invoked and passed the correct information. +TEST_F(CommandMgrTest, commandProcessedHook) { + // Register callout so as we can check that it is called before + // processing the command by the manager. + HooksManager::preCalloutsLibraryHandle().registerCallout( + "command_processed", command_processed_callout); + + // Install local handler + EXPECT_NO_THROW(CommandMgr::instance().registerCommand("my-command", + my_handler)); + + // Now tell CommandMgr to process a command 'my-command' with the + // specified parameter. + ElementPtr my_params = Element::fromJSON("[ \"just\", \"some\", \"data\" ]"); + ConstElementPtr command = createCommand("my-command", my_params); + ConstElementPtr answer; + ASSERT_NO_THROW(answer = CommandMgr::instance().processCommand(command)); + + // There should be an answer. + ASSERT_TRUE(answer); + + // Local handler should be called + ASSERT_TRUE(handler_called_); + + // Verify that the response came through intact + EXPECT_EQ("{ \"result\": 123, \"text\": \"test error message\" }", + answer->str()); + + // Make sure invoked the command_processed callout + EXPECT_EQ("command_processed_handler", callout_name_); + + // Verify the callout could extract all the context arguments + EXPECT_EQ("my-command:[ \"just\", \"some\", \"data\" ]:" + "{ \"result\": 123, \"text\": \"test error message\" }", + processed_log_); +} + +// This test verifies that a registered callout for the command_processed +// hookpoint is invoked and can replace the command response content. +TEST_F(CommandMgrTest, commandProcessedHookReplaceResponse) { + // Register callout so as we can check that it is called before + // processing the command by the manager. + HooksManager::preCalloutsLibraryHandle().registerCallout( + "command_processed", command_processed_callout); + + // Install local handler + EXPECT_NO_THROW(CommandMgr::instance().registerCommand("my-command", + my_handler)); + + // Now tell CommandMgr to process a command 'my-command' with the + // specified parameter. + ElementPtr my_params = Element::fromJSON("[ \"just\", \"some\", \"data\" ]"); + ConstElementPtr command = createCommand("change-response", my_params); + ConstElementPtr answer; + ASSERT_NO_THROW(answer = CommandMgr::instance().processCommand(command)); + + // There should be an answer. + ASSERT_TRUE(answer); + + // Local handler should not have been called, command isn't recognized + ASSERT_FALSE(handler_called_); + + // Verify that we overrode response came + EXPECT_EQ("{ \"result\": 777, \"text\": \"replaced response text\" }", + answer->str()); + + // Make sure invoked the command_processed callout + EXPECT_EQ("command_processed_handler", callout_name_); + + // Verify the callout could extract all the context arguments + EXPECT_EQ("change-response:[ \"just\", \"some\", \"data\" ]:" + "{ \"result\": 2, \"text\": \"'change-response' command not supported.\" }", + processed_log_); +} + +// Verifies that a socket cannot be concurrently opened more than once. +TEST_F(CommandMgrTest, exclusiveOpen) { + // Pass in valid parameters. + ElementPtr socket_info = Element::createMap(); + socket_info->set("socket-type", Element::create("unix")); + socket_info->set("socket-name", Element::create(getSocketPath())); + + EXPECT_NO_THROW(CommandMgr::instance().openCommandSocket(socket_info)); + EXPECT_GE(CommandMgr::instance().getControlSocketFD(), 0); + + // Should not be able to open it twice. + EXPECT_THROW(CommandMgr::instance().openCommandSocket(socket_info), + isc::config::SocketError); + + // Now let's close it. + EXPECT_NO_THROW(CommandMgr::instance().closeCommandSocket()); + + // Should be able to re-open it now. + EXPECT_NO_THROW(CommandMgr::instance().openCommandSocket(socket_info)); + EXPECT_GE(CommandMgr::instance().getControlSocketFD(), 0); + + // Now let's close it. + EXPECT_NO_THROW(CommandMgr::instance().closeCommandSocket()); +} -- cgit v1.2.3