]> git.ipfire.org Git - thirdparty/kea.git/commitdiff
[#3477] Checkpoint: added HttpCommandMgr UTs
authorFrancis Dupont <fdupont@isc.org>
Mon, 8 Jul 2024 11:44:53 +0000 (13:44 +0200)
committerFrancis Dupont <fdupont@isc.org>
Thu, 1 Aug 2024 07:23:53 +0000 (09:23 +0200)
src/lib/config/tests/Makefile.am
src/lib/config/tests/http_command_mgr_unittests.cc [new file with mode: 0644]

index b50eebe7f2ebcb8bd1a41df9e39405d9a4fd4a4b..41077ce9533f3916e8632659d3170a2401cef5f6 100644 (file)
@@ -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 (file)
index 0000000..24d5d6a
--- /dev/null
@@ -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 <config.h>
+
+#include <asiolink/asio_wrapper.h>
+#include <asiolink/interval_timer.h>
+#include <asiolink/testutils/test_tls.h>
+#include <cc/command_interpreter.h>
+#include <config/http_command_mgr.h>
+#include <config/command_mgr.h>
+#include <http/response.h>
+#include <http/response_parser.h>
+#include <http/tests/test_http_client.h>
+#include <testutils/gtest_utils.h>
+
+#include <gtest/gtest.h>
+
+#include <list>
+#include <sstream>
+
+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