diff --git a/src/lib/config/tests/Makefile.am b/src/lib/config/tests/Makefile.am index b50eebe7f2..41077ce953 100644 --- a/src/lib/config/tests/Makefile.am +++ b/src/lib/config/tests/Makefile.am @@ -25,6 +25,7 @@ run_unittests_SOURCES += cmd_http_listener_unittests.cc run_unittests_SOURCES += cmd_response_creator_unittests.cc run_unittests_SOURCES += cmd_response_creator_factory_unittests.cc run_unittests_SOURCES += http_command_config_unittests.cc +run_unittests_SOURCES += http_command_mgr_unittests.cc run_unittests_SOURCES += http_command_response_creator_factory_unittests.cc run_unittests_SOURCES += http_command_response_creator_unittests.cc diff --git a/src/lib/config/tests/http_command_mgr_unittests.cc b/src/lib/config/tests/http_command_mgr_unittests.cc new file mode 100644 index 0000000000..24d5d6a531 --- /dev/null +++ b/src/lib/config/tests/http_command_mgr_unittests.cc @@ -0,0 +1,283 @@ +// Copyright (C) 2021-2024 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 +#include + +using namespace isc; +using namespace isc::asiolink; +using namespace isc::asiolink::test; +using namespace isc::config; +using namespace isc::data; +using namespace isc::dhcp; +using namespace isc::http; +using namespace isc::util; +using namespace std; +using namespace boost::asio::ip; +namespace ph = std::placeholders; + +namespace { + +/// @brief IP address to which HTTP service is bound. +const std::string SERVER_ADDRESS = "127.0.0.1"; + +/// @brief Port number to which HTTP service is bound. +const unsigned short SERVER_PORT = 18123; + +/// @brief Test timeout (ms). +const long TEST_TIMEOUT = 10000; + +/// @brief Test fixture class for @ref CmdHttpListener. +class HttpCommandMgrTest : public ::testing::Test { +public: + + /// @brief Constructor. + /// + /// Resets state, starts test timer which detects timeouts, + /// initializes HTTP control socket config. + HttpCommandMgrTest() + : io_service_(new IOService()), test_timer_(io_service_), client_(), + http_config_() { + resetState(io_service_); + test_timer_.setup(std::bind(&HttpCommandMgrTest::timeoutHandler, this, true), + TEST_TIMEOUT, IntervalTimer::ONE_SHOT); + HttpCommandMgr::instance().setIOService(io_service_); + + // Initializes the HTTP control socket config. + ElementPtr config = Element::createMap(); + config->set("socket-address", Element::create(SERVER_ADDRESS)); + config->set("socket-port", Element::create(SERVER_PORT)); + http_config_.reset(new HttpCommandConfig(config)); + } + + /// @brief Destructor. + /// + /// Closes HTTP client, cancels timer, resets state. + virtual ~HttpCommandMgrTest() { + if (client_) { + client_->close(); + } + test_timer_.cancel(); + resetState(); + } + + /// @brief Resets state. + /// + /// @param io_service The IO service of the @c HttpCommandMgr. + void resetState(IOServicePtr io_service = IOServicePtr()) { + // Deregisters commands. + config::CommandMgr::instance().deregisterAll(); + + if (HttpCommandMgr::instance().getHttpListener()) { + HttpCommandMgr::instance().close(); + } + if (io_service) { + HttpCommandMgr::instance().setIOService(io_service); + } else { + io_service_->stopAndPoll(); + HttpCommandMgr::instance().setIOService(IOServicePtr()); + } + } + + /// @brief Constructs a complete HTTP POST given a request body. + /// + /// @param request_body string containing the desired request body. + /// + /// @return string containing the constructed POST. + std::string buildPostStr(const std::string& request_body) { + // Create the command string. + std::stringstream ss; + ss << "POST /foo/bar HTTP/1.1\r\n" + "Content-Type: application/json\r\n" + "Content-Length: " + << request_body.size() << "\r\n\r\n" + << request_body; + return (ss.str()); + } + + /// @brief Initiates a command via a new HTTP client. + /// + /// This method creates a TestHttpClient instance, and starts a + /// request based on the given command. + /// + /// @param request_body JSON String containing the API command + /// to be sent. + void startRequest(const std::string& request_body = "{ }") { + std::string request_str = buildPostStr(request_body); + + // Instantiate the client. + client_.reset(new TestHttpClient(io_service_, SERVER_ADDRESS, + SERVER_PORT)); + + // Start the request. Note, nothing happens until the IOService runs. + client_->startRequest(request_str); + } + + /// @brief Callback function invoke upon test timeout. + /// + /// It stops the IO service and reports test timeout. + /// + /// @param fail_on_timeout Specifies if test failure should be reported. + void timeoutHandler(const bool fail_on_timeout) { + if (fail_on_timeout) { + ADD_FAILURE() << "Timeout occurred while running the test!"; + } + io_service_->stop(); + } + + /// @brief Create an HttpResponse from a response string. + /// + /// @param response_str a string containing the whole HTTP + /// response received. + /// + /// @return An HttpResponse constructed from by parsing the + /// response string. + HttpResponsePtr parseResponse(const std::string response_str) { + HttpResponsePtr hr(new HttpResponse()); + HttpResponseParser parser(*hr); + parser.initModel(); + parser.postBuffer(&response_str[0], response_str.size()); + parser.poll(); + if (!parser.httpParseOk()) { + isc_throw(Unexpected, "response_str: '" << response_str + << "' failed to parse: " << parser.getErrorMessage()); + } + + return (hr); + } + + /// @brief IO service used in drive the test and test clients. + IOServicePtr io_service_; + + /// @brief Asynchronous timer service to detect timeouts. + IntervalTimer test_timer_; + + /// @brief Client connection. + TestHttpClientPtr client_; + + /// @brief HTTP control socket config. + HttpCommandConfigPtr http_config_; +}; + +/// Verifies the configure and close of HttpCommandMgr. +TEST_F(HttpCommandMgrTest, basics) { + // Make sure we can create one. + ASSERT_NO_THROW_LOG(HttpCommandMgr::instance().configure(http_config_)); + auto listener = HttpCommandMgr::instance().getHttpListener(); + ASSERT_TRUE(listener); + + // Verify the getters do what we expect. + EXPECT_EQ(SERVER_ADDRESS, listener->getLocalAddress().toText()); + EXPECT_EQ(SERVER_PORT, listener->getLocalPort()); + + // Stop it and verify we're no longer listening. + ASSERT_NO_THROW_LOG(HttpCommandMgr::instance().close()); + EXPECT_FALSE(HttpCommandMgr::instance().getHttpListener()); + + // Make sure we can call stop again without problems. + ASSERT_NO_THROW_LOG(HttpCommandMgr::instance().close()); + + // We should be able to restart it. + ASSERT_NO_THROW_LOG(HttpCommandMgr::instance().configure(http_config_)); + EXPECT_TRUE(HttpCommandMgr::instance().getHttpListener()); + + // Close it with postponed garbage collection. + ASSERT_NO_THROW_LOG(HttpCommandMgr::instance().close(false)); + EXPECT_TRUE(HttpCommandMgr::instance().getHttpListener()); + ASSERT_NO_THROW_LOG(HttpCommandMgr::instance().garbageCollectListeners()); + EXPECT_FALSE(HttpCommandMgr::instance().getHttpListener()); +} + +#if 0 +// This test verifies that an HTTP connection can be established and used to +// transmit an HTTP request and receive the response. +TEST_F(HttpCommandMgrTest, basicListenAndRespond) { + + // Create a listener. + ASSERT_NO_THROW_LOG(listener_.reset(new HttpCommandMgr(IOAddress(SERVER_ADDRESS), + SERVER_PORT))); + ASSERT_TRUE(listener_); + + // Start the listener and verify it's listening. + ASSERT_NO_THROW_LOG(listener_->start()); + ASSERT_TRUE(listener_->isRunning()); + + // Now let's send a "foo" command. This should create a client, connect + // to our listener, post our request and retrieve our reply. + ASSERT_NO_THROW(startRequest("{\"command\": \"foo\"}")); + ASSERT_TRUE(client_); + ASSERT_NO_THROW(runIOService()); + ASSERT_TRUE(client_); + + // Parse the response into an HttpResponse. + HttpResponsePtr hr; + ASSERT_NO_THROW_LOG(hr = parseResponse(client_->getResponse())); + + // Without a command handler loaded, we should get an unsupported command response. + EXPECT_EQ(hr->getBody(), "[ { \"result\": 2, \"text\": \"'foo' command not supported.\" } ]"); + + // Now let's register the foo command handler. + CommandMgr::instance().registerCommand("foo", + std::bind(&HttpCommandMgrTest::fooCommandHandler, + this, ph::_1, ph::_2)); + // Try posting the foo command again. + ASSERT_NO_THROW(startRequest("{\"command\": \"foo\"}")); + ASSERT_TRUE(client_); + + // Parse the response. + ASSERT_NO_THROW_LOG(hr = parseResponse(client_->getResponse())); + + // We should have a response from our command handler. + EXPECT_EQ(hr->getBody(), "[ { \"arguments\": [ \"bar\" ], \"result\": 0 } ]"); + + // Make sure the listener is still listening. + ASSERT_TRUE(listener_->isRunning()); + + // Stop the listener then verify it has stopped. + ASSERT_NO_THROW_LOG(listener_->stop()); + ASSERT_TRUE(listener_->isStopped()); +} + +// Check if a TLS listener can be created. +TEST_F(HttpCommandMgrTest, tls) { + IOAddress address(SERVER_ADDRESS); + uint16_t port = SERVER_PORT; + TlsContextPtr context; + configServer(context); + + // Make sure we can create the listener. + ASSERT_NO_THROW_LOG(listener_.reset(new HttpCommandMgr(address, port, 1, context))); + EXPECT_EQ(listener_->getAddress(), address); + EXPECT_EQ(listener_->getPort(), port); + EXPECT_EQ(listener_->getTlsContext(), context); + EXPECT_TRUE(listener_->isStopped()); + + // Make sure we can start it and it's listening. + ASSERT_NO_THROW_LOG(listener_->start()); + ASSERT_TRUE(listener_->isRunning()); + + // Stop it. + ASSERT_NO_THROW_LOG(listener_->stop()); + ASSERT_TRUE(listener_->isStopped()); +} +#endif + +} // end of anonymous namespace