]> git.ipfire.org Git - thirdparty/kea.git/commitdiff
[#1730] Added CmdHttpListener listen-and-respond unit tests
authorThomas Markwalder <tmark@isc.org>
Wed, 10 Mar 2021 14:18:21 +0000 (09:18 -0500)
committerThomas Markwalder <tmark@isc.org>
Mon, 22 Mar 2021 17:51:50 +0000 (13:51 -0400)
src/lib/config/cmd_http_listener.cc
    CmdHttpListener::start() - added check for multi-threading enabled

src/lib/config/tests/cmd_http_listener_unittests.cc
    Added CmdHttpListenerTest fixture and new listener/client
    interatcion testing

src/lib/http/tests/server_client_unittests.cc
    Extracted TestHttpClient to its own header file

src/lib/http/tests/test_http_client.h - new file

src/lib/config/cmd_http_listener.cc
src/lib/config/tests/cmd_http_listener_unittests.cc
src/lib/http/tests/server_client_unittests.cc
src/lib/http/tests/test_http_client.h [new file with mode: 0644]

index 31d1a2238258482dc97a88675fae02241db22278..c40adc2202080fc76889c9c5e9becf67ed756357 100644 (file)
@@ -13,6 +13,7 @@
 #include <cmd_response_creator_factory.h>
 #include <config_log.h>
 #include <config/timeouts.h>
+#include <util/multi_threading_mgr.h>
 
 #include <boost/pointer_cast.hpp>
 
@@ -20,6 +21,7 @@ using namespace isc::asiolink;
 using namespace isc::config;
 using namespace isc::data;
 using namespace isc::http;
+using namespace isc::util;
 
 namespace isc {
 namespace config {
@@ -35,6 +37,13 @@ CmdHttpListener::~CmdHttpListener() {
 
 void
 CmdHttpListener::start() {
+    // We must be in multi-threading mode.
+    if (!MultiThreadingMgr::instance().getMode()) {
+        isc_throw(InvalidOperation, "CmdHttpListener cannot be started"
+                  " when multi-threading is disabled");
+    }
+
+    // Punt if we're already listening.
     if (isListening()) {
         isc_throw(InvalidOperation, "CmdHttpListener is already listening!");
     }
index b2d6dc21f09fd1df240ad226a6d49e22bff8501b..c0f9184ca07544277effba15bb7c3a042cf770eb 100644 (file)
@@ -6,23 +6,49 @@
 
 #include <config.h>
 
+#include <asiolink/asio_wrapper.h>
+#include <asiolink/interval_timer.h>
+#include <cc/command_interpreter.h>
 #include <config/cmd_http_listener.h>
+#include <config/command_mgr.h>
+#include <http/response.h>
+#include <http/response_parser.h>
+#include <http/tests/test_http_client.h>
+#include <util/multi_threading_mgr.h>
 #include <testutils/gtest_utils.h>
 
 #include <gtest/gtest.h>
 
+#include <thread>
+#include <list>
+#include <sstream>
+
 using namespace isc;
 using namespace isc::config;
 using namespace isc::data;
+using namespace boost::asio::ip;
+using namespace isc::asiolink;
+using namespace isc::http;
+using namespace isc::util;
+namespace ph = std::placeholders;
 
 namespace {
 
-/// Verifies the construction, starting, stopping, destruction
-/// the CmdHttpListener class.
+/// @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;
+
+/// Verifies the construction, starting, stopping, and destruction
+/// of CmdHttpListener.
 TEST(CmdHttpListener, basics) {
     CmdHttpListenerPtr listener;
-    asiolink::IOAddress address("127.0.0.1");
-    uint16_t port = 8080;
+    asiolink::IOAddress address(SERVER_ADDRESS);
+    uint16_t port = SERVER_PORT;
 
     // Make sure we can create one.
     ASSERT_NO_THROW_LOG(listener.reset(new CmdHttpListener(address, port)));
@@ -37,6 +63,19 @@ TEST(CmdHttpListener, basics) {
     EXPECT_FALSE(listener->isListening());
     EXPECT_EQ(listener->getThreadCount(), 0);
 
+    // Verify that we cannot start it when multi-threading is disabled.
+    ASSERT_FALSE(MultiThreadingMgr::instance().getMode());
+    ASSERT_THROW_MSG(listener->start(), InvalidOperation,
+                     "CmdHttpListener cannot be started"
+                     " when multi-threading is disabled");
+
+    // It should still not be listening and have no threads.
+    EXPECT_FALSE(listener->isListening());
+    EXPECT_EQ(listener->getThreadCount(), 0);
+
+    // Enable multi-threading.
+    MultiThreadingMgr::instance().setMode(true);
+
     // Make sure we can start it and it's listening with 1 thread.
     ASSERT_NO_THROW_LOG(listener->start());
     ASSERT_TRUE(listener->isListening());
@@ -78,4 +117,516 @@ TEST(CmdHttpListener, basics) {
     EXPECT_EQ(listener->getThreadCount(), 0);
 }
 
+/// @brief Test fixture class for @ref CmdHttpListener.
+class CmdHttpListenerTest : public ::testing::Test {
+public:
+
+    /// @brief Constructor.
+    ///
+    /// Starts test timer which detects timeouts, deregisters all commands
+    /// from CommandMgr, and enables multi-threading mode.
+    CmdHttpListenerTest()
+        : io_service_(), test_timer_(io_service_), run_io_service_timer_(io_service_),
+        clients_(), num_threads_(), num_clients_() {
+        test_timer_.setup(std::bind(&CmdHttpListenerTest::timeoutHandler, this, true),
+                          TEST_TIMEOUT, IntervalTimer::ONE_SHOT);
+
+        // Deregisters commands.
+        CommandMgr::instance().deregisterAll();
+
+        // Ensure we're in MT mode.
+        MultiThreadingMgr::instance().setMode(true);
+    }
+
+    /// @brief Destructor.
+    ///
+    /// Removes HTTP clients, unregisters commands, disables MT.
+    virtual ~CmdHttpListenerTest() {
+        // Destroy all remaining clients.
+        for (auto client = clients_.begin(); client != clients_.end();
+             ++client) {
+            (*client)->close();
+        }
+
+        // Deregisters commands.
+        config::CommandMgr::instance().deregisterAll();
+
+        // Shut of MT.
+        MultiThreadingMgr::instance().setMode(false);
+    }
+
+    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, adds the
+    /// client to the list of clients, and starts a request based
+    /// on the given command. The client will run on the main
+    /// thread and be driven by the test's IOService instance.
+    ///
+    /// @param request_body JSON String containing the API command
+    /// to be be sent.
+    void startRequest(const std::string& request_body = "{ }") {
+        std::string request_str = buildPostStr(request_body);
+
+        // Instantiate the client.
+        TestHttpClientPtr client(new TestHttpClient(io_service_, SERVER_ADDRESS,
+                                                    SERVER_PORT));
+        // Add it to the list of clients.
+        clients_.push_back(client);
+
+        // Start the request.  Note, nothing happens until the IOService runs.
+        client->startRequest(request_str);
+    }
+
+    /// @brief Initiates a "thread" command via a new HTTP client.
+    ///
+    /// This method creates a TestHttpClient instance, adds the
+    /// client to the list of clients, and starts a request based
+    /// on the given command. The client will run on the main
+    /// thread and be driven by the test's IOService instance.
+    ///
+    /// The command has a single argument, "client-ptr". The function creates
+    /// the value for this argument from the pointer address of client instance
+    /// it creates.  This argument should be echoed back in the response, along
+    /// with the thread-id of the CmdHttpListener thread which handled the
+    /// command. The response body should look this:
+    ///
+    /// ```
+    /// [ { "arguments": { "client-ptr": "xxxxx", "thread-id": "zzzzz" }, "result": 0} ]
+    /// ```
+    void startThreadCommand() {
+        // Create a new client.
+        TestHttpClientPtr client(new TestHttpClient(io_service_, SERVER_ADDRESS,
+                                                    SERVER_PORT));
+
+        // Construct the "thread" command post including the argument,
+        // "client-ptr", whose value is the stringified pointer to the
+        // newly created client.
+        std::stringstream request_body;
+        request_body << "{\"command\": \"thread\", \"arguments\": { \"client-ptr\": \""
+                     << client << "\" } }";
+
+        std::string command = buildPostStr(request_body.str());
+
+        // Add it to the list of clients.
+        clients_.push_back(client);
+
+        // Start the request.  Note, nothing happens until the IOService runs.
+        ASSERT_NO_THROW_LOG(client->startRequest(command));
+    }
+
+    /// @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 Runs IO service with optional timeout.
+    ///
+    /// We iterate over calls to asio::io_service.run(), until
+    /// all the clients have completed their requests.  We do it this
+    /// because the test clients stop the io_service when they're
+    /// through with a request.
+    ///
+    /// @param timeout Optional value specifying for how long the io service
+    /// should be ran.
+    void runIOService() {
+        // Loop until the clients are done, an error occurs, or the time runs out.
+        bool keep_going = true;
+        while (keep_going) {
+            // Always call restart() before we call run();
+            io_service_.get_io_service().restart();
+
+            // Run until a client stops the service.
+            io_service_.run();
+
+            // If all the clients are done receiving, the test is done.
+            keep_going = false;
+            for ( auto client : clients_ ) {
+                if (!client->receiveDone()) {
+                    keep_going = true;
+                    break;
+                }
+            }
+        }
+    }
+
+    /// @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 Handler for the 'foo' command.
+    ///
+    /// The command needs no arguments and returns a response
+    /// with a body containing:
+    ///
+    /// "[ { \"arguments\": [ \"bar\" ], \"result\": 0 } ]")
+    ///
+    /// @param command_name Command name, i.e. 'foo'.
+    /// @param command_arguments Command arguments (empty).
+    ///
+    /// @return Returns response with a single string "bar".
+    ConstElementPtr fooCommandHandler(const std::string& /*command_name*/,
+                                      const ConstElementPtr& /*command_arguments*/) {
+        ElementPtr arguments = Element::createList();
+        arguments->add(Element::create("bar"));
+        return (createAnswer(CONTROL_RESULT_SUCCESS, arguments));
+    }
+
+    /// @brief Handler for the 'thread' command.
+    ///
+    /// @param command_name Command name, i.e. 'thread'.
+    /// @param command_arguments Command arguments should contain
+    /// one string element, "client-ptr", whose value is the stringified
+    /// pointer to the client that issued the command.
+    ///
+    /// @return Returns response with map of arguments containing
+    /// a string value 'thread-id': <thread id>
+    ConstElementPtr threadCommandHandler(const std::string& /*command_name*/,
+                                       const ConstElementPtr& command_arguments) {
+        // If the number of in progress commands is less than the number
+        // of threads, then wait here until we're notified.  Otherwise,
+        // notify everyone and finish.  The idea is to force each thread
+        // to handle the same number of requests over the course of the
+        // test, making verification reliable.
+        if (num_clients_ > 1) {
+            std::unique_lock<std::mutex> lck(mutex_);
+            ++num_in_progress_;
+            if (num_in_progress_ < num_threads_) {
+                cv_.wait(lck);
+            } else {
+                num_in_progress_ = 0;
+                cv_.notify_all();
+            }
+        }
+
+        // Create the map of response arguments.
+        ElementPtr arguments = Element::createMap();
+        // First we echo the client-ptr command argument.
+        ConstElementPtr client_ptr = command_arguments->get("client-ptr");
+        if (!client_ptr) {
+            return (createAnswer(CONTROL_RESULT_ERROR, "missing client-ptr"));
+        }
+
+        arguments->set("client-ptr", client_ptr);
+
+        // Now we add the thread-id.
+        std::stringstream ss;
+        ss << std::this_thread::get_id();
+        arguments->set("thread-id", Element::create(ss.str()));
+
+        // We're done, ship it!
+        return (createAnswer(CONTROL_RESULT_SUCCESS, arguments));
+    }
+
+    /// @brief Submits one or more thread commands to a CmdHttpListener
+    ///
+    /// This function command will creates a CmdHttpListener
+    /// with the given number of threads, initiates the given
+    /// number of clients, each requesting the "thread" command,
+    /// and then iteratively runs the test's IOService until all
+    /// the clients have received their responses or an error occurs.
+    ///
+    /// It requires that the number of clients, when greater than the
+    /// number of threads, be a multiple of the number of threads.  The
+    /// thread command handler is structured in such a way as to ensure
+    /// (we hope) that each thread handles the same number of commands.
+    ///
+    /// @param num_threads - the number of threads the CmdHttpListener
+    /// should use. Must be greater than 0.
+    /// @param num_clients - the number of clients that should issue the
+    /// thread command.  Each client is used to carry out a single thread
+    /// command request.  Must be greater than 0 and a multiple of num_threads
+    /// if it is greater than num_threads.
+    ///
+    void threadListenAndRespond(size_t num_threads, size_t num_clients) {
+        // First we makes sure the parameter rules apply.
+        ASSERT_TRUE(num_threads > 0);
+        ASSERT_TRUE(num_clients > 0);
+        ASSERT_TRUE((num_clients < num_threads) || (num_clients % num_threads == 0));
+
+        num_threads_ = num_threads;
+        num_clients_ = num_clients;
+
+        // Register the thread command handler.
+        CommandMgr::instance().registerCommand("thread",
+                                               std::bind(&CmdHttpListenerTest
+                                                         ::threadCommandHandler,
+                                                         this, ph::_1, ph::_2));
+
+        // Create a listener with prescribed number of threads.
+        CmdHttpListenerPtr listener;
+        ASSERT_NO_THROW_LOG(listener.reset(new CmdHttpListener(IOAddress(SERVER_ADDRESS),
+                                                               SERVER_PORT, num_threads)));
+        ASSERT_TRUE(listener);
+
+        // Start it and verify it is listening.
+        ASSERT_NO_THROW_LOG(listener->start());
+        ASSERT_TRUE(listener->isListening());
+        EXPECT_EQ(listener->getThreadCount(), num_threads);
+
+        // Maps the number of clients served by a given thread-id.
+        std::map<std::string, int> clients_per_thread;
+
+        // Initiate the prescribed number of command requests.
+        num_in_progress_ = 0;
+        for ( auto i = 0; clients_.size() < num_clients; ++i) {
+            ASSERT_NO_THROW_LOG(startThreadCommand());
+        }
+
+        // Now we run the client-side IOService until all requests are done,
+        // errors occur or the test times out.
+        ASSERT_NO_FATAL_FAILURE(runIOService());
+
+        // Stop the listener and then verify it has stopped.
+        ASSERT_NO_THROW_LOG(listener->stop());
+        ASSERT_FALSE(listener->isListening());
+        EXPECT_EQ(listener->getThreadCount(), 0);
+
+        // Iterate over the clients, checking their outcomes.
+        size_t total_responses = 0;
+        for (auto client : clients_) {
+            // Client should have completed its receive successfully.
+            ASSERT_TRUE(client->receiveDone());
+
+            // Client response should not be empty.
+            HttpResponsePtr hr;
+            std::string response_str = client->getResponse();
+            ASSERT_FALSE(response_str.empty());
+
+            // Parse the response into an HttpResponse.
+            ASSERT_NO_THROW_LOG(hr = parseResponse(client->getResponse()));
+
+            // Now we walk the element tree to get the response data.  It should look
+            // this:
+            //
+            //  [ {
+            //       "arguments": { "client-ptr": "xxxxx",
+            //                      "thread-id": "zzzzz" },
+            //       "result": 0
+            //  } ]
+            //
+            // First we turn it into an Element tree.
+            std::string body_str = hr->getBody();
+            ConstElementPtr body;
+            ASSERT_NO_THROW_LOG(body = Element::fromJSON(hr->getBody()));
+
+            // Outermost is a list, since we're emulating agent responses.
+            ASSERT_EQ(body->getType(), Element::list);
+            ASSERT_EQ(body->size(), 1);
+
+            // Answer should be a map containing "arguments" and "results".
+            ConstElementPtr answer = body->get(0);
+            ASSERT_EQ(answer->getType(), Element::map);
+
+            // "result" should be 0.
+            ConstElementPtr result = answer->get("result");
+            ASSERT_TRUE(result);
+            ASSERT_EQ(result->getType(), Element::integer);
+            ASSERT_EQ(result->intValue(), 0);
+
+            // "arguments" is a map containing "client-ptr" and "thread-id".
+            ConstElementPtr arguments = answer->get("arguments");
+            ASSERT_TRUE(arguments);
+            ASSERT_EQ(arguments->getType(), Element::map);
+
+            // "client-ptr" is a string.
+            ConstElementPtr client_ptr = arguments->get("client-ptr");
+            ASSERT_TRUE(client_ptr);
+            ASSERT_EQ(client_ptr->getType(), Element::string);
+
+            // "thread-id" is a string.
+            ConstElementPtr thread_id = arguments->get("thread-id");
+            ASSERT_TRUE(thread_id);
+            ASSERT_EQ(thread_id->getType(), Element::string);
+            std::string thread_id_str = thread_id->stringValue();
+
+            // Make sure the response received was for this client.
+            std::stringstream ss;
+            ss << client;
+            ASSERT_EQ(client_ptr->stringValue(), ss.str());
+
+            // Bump the client count for the given thread-id.
+            auto it = clients_per_thread.find(thread_id_str);
+            if (it != clients_per_thread.end()) {
+                clients_per_thread[thread_id_str] = it->second + 1;
+            } else {
+                clients_per_thread[thread_id_str] = 1;
+            }
+
+            ++total_responses;
+        }
+
+        // We should have responses for all our clients.
+        EXPECT_EQ(total_responses, num_clients);
+
+        // Verify we have the expected number of entries in our map.
+        size_t expected_thread_count = (num_clients < num_threads ?
+                                        num_clients : num_threads);
+
+        ASSERT_EQ(clients_per_thread.size(), expected_thread_count);
+
+        // Each thread-id ought to have handled the same number of clients.
+        for (auto it : clients_per_thread) {
+            EXPECT_EQ(it.second, num_clients / clients_per_thread.size())
+                      << "thread-id: " << it.first
+                      << ", clients: " << it.second << std::endl;
+        }
+    }
+
+    /// @brief IO service used in drive the test and test clients.
+    IOService io_service_;
+
+    /// @brief Asynchronous timer service to detect timeouts.
+    IntervalTimer test_timer_;
+
+    /// @brief Asynchronous timer for running IO service for a specified amount
+    /// of time.
+    IntervalTimer run_io_service_timer_;
+
+    /// @brief List of client connections.
+    std::list<TestHttpClientPtr> clients_;
+
+    size_t num_threads_;
+    size_t num_clients_;
+    size_t num_in_progress_;
+    std::mutex mutex_;
+    std::condition_variable cv_;
+};
+
+// This test verifies that an HTTP connection can be established and used to
+// transmit an HTTP request and receive the response.
+TEST_F(CmdHttpListenerTest, basicListenAndRespond) {
+
+    // Create a listener with 1 thread.
+    CmdHttpListenerPtr listener;
+    ASSERT_NO_THROW_LOG(listener.reset(new CmdHttpListener(IOAddress(SERVER_ADDRESS),
+                                                           SERVER_PORT)));
+    ASSERT_TRUE(listener);
+
+    // Start the listener and verify it's listening with 1 thread.
+    ASSERT_NO_THROW_LOG(listener->start());
+    ASSERT_TRUE(listener->isListening());
+    EXPECT_EQ(listener->getThreadCount(), 1);
+
+    // 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_EQ(1, clients_.size());
+    ASSERT_NO_THROW(runIOService());
+    TestHttpClientPtr client = clients_.front();
+    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(&CmdHttpListenerTest::fooCommandHandler,
+                                                      this, ph::_1, ph::_2));
+    // Try posting the foo command again.
+    ASSERT_NO_THROW(startRequest("{\"command\": \"foo\"}"));
+    ASSERT_EQ(2, clients_.size());
+    ASSERT_NO_THROW(runIOService());
+    client = clients_.back();
+    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->isListening());
+    EXPECT_EQ(listener->getThreadCount(), 1);
+
+    // Stop the listener then verify it has stopped.
+    ASSERT_NO_THROW_LOG(listener->stop());
+    ASSERT_FALSE(listener->isListening());
+    EXPECT_EQ(listener->getThreadCount(), 0);
+}
+
+// Now we'll run some permutations of the number of listener threads
+// and the number of client requests.
+
+// One thread, one client.
+TEST_F(CmdHttpListenerTest, oneByOne) {
+    size_t num_threads = 1;
+    size_t num_clients = 1;
+    threadListenAndRespond(num_threads, num_clients);
+}
+
+// One thread, four clients.
+TEST_F(CmdHttpListenerTest, oneByFour) {
+    size_t num_threads = 1;
+    size_t num_clients = 4;
+    threadListenAndRespond(num_threads, num_clients);
+}
+
+// Four threads, one clients.
+TEST_F(CmdHttpListenerTest, fourByOne) {
+    size_t num_threads = 4;
+    size_t num_clients = 1;
+    threadListenAndRespond(num_threads, num_clients);
+}
+
+// Four threads, four clients.
+TEST_F(CmdHttpListenerTest, fourByFour) {
+    size_t num_threads = 4;
+    size_t num_clients = 4;
+    threadListenAndRespond(num_threads, num_clients);
+}
+
+// Four threads, eight clients.
+TEST_F(CmdHttpListenerTest, fourByEight) {
+    size_t num_threads = 4;
+    size_t num_clients = 8;
+    threadListenAndRespond(num_threads, num_clients);
+}
+
+// Six threads, eighteen clients.
+TEST_F(CmdHttpListenerTest, sixByEighteen) {
+    size_t num_threads = 6;
+    size_t num_clients = 18;
+    threadListenAndRespond(num_threads, num_clients);
+}
+
 } // end of anonymous namespace
index bfcf28d8565fd1e51f7464fa806618dde7896d56..f6cf94869b049952929b2a8ec2d31e1fd0a5fa56 100644 (file)
@@ -8,6 +8,7 @@
 #include <asiolink/asio_wrapper.h>
 #include <asiolink/interval_timer.h>
 #include <cc/data.h>
+#include <test_http_client.h>
 #include <http/client.h>
 #include <http/http_types.h>
 #include <http/listener.h>
@@ -385,231 +386,6 @@ public:
     }
 };
 
-
-/// @brief Entity which can connect to the HTTP server endpoint.
-class TestHttpClient : public boost::noncopyable {
-public:
-
-    /// @brief Constructor.
-    ///
-    /// This constructor creates new socket instance. It doesn't connect. Call
-    /// connect() to connect to the server.
-    ///
-    /// @param io_service IO service to be stopped on error.
-    explicit TestHttpClient(IOService& io_service)
-        : io_service_(io_service.get_io_service()), socket_(io_service_),
-          buf_(), response_() {
-    }
-
-    /// @brief Destructor.
-    ///
-    /// Closes the underlying socket if it is open.
-    ~TestHttpClient() {
-        close();
-    }
-
-    /// @brief Send HTTP request specified in textual format.
-    ///
-    /// @param request HTTP request in the textual format.
-    void startRequest(const std::string& request) {
-        tcp::endpoint endpoint(address::from_string(SERVER_ADDRESS),
-                               SERVER_PORT);
-        socket_.async_connect(endpoint,
-        [this, request](const boost::system::error_code& ec) {
-            if (ec) {
-                // One would expect that async_connect wouldn't return
-                // EINPROGRESS error code, but simply wait for the connection
-                // to get established before the handler is invoked. It turns out,
-                // however, that on some OSes the connect handler may receive this
-                // error code which doesn't necessarily indicate a problem.
-                // Making an attempt to write and read from this socket will
-                // typically succeed. So, we ignore this error.
-                if (ec.value() != boost::asio::error::in_progress) {
-                    ADD_FAILURE() << "error occurred while connecting: "
-                                  << ec.message();
-                    io_service_.stop();
-                    return;
-                }
-            }
-            sendRequest(request);
-        });
-    }
-
-    /// @brief Send HTTP request.
-    ///
-    /// @param request HTTP request in the textual format.
-    void sendRequest(const std::string& request) {
-        sendPartialRequest(request);
-    }
-
-    /// @brief Send part of the HTTP request.
-    ///
-    /// @param request part of the HTTP request to be sent.
-    void sendPartialRequest(std::string request) {
-        socket_.async_send(boost::asio::buffer(request.data(), request.size()),
-                           [this, request](const boost::system::error_code& ec,
-                                           std::size_t bytes_transferred) mutable {
-            if (ec) {
-                if (ec.value() == boost::asio::error::operation_aborted) {
-                    return;
-
-                } else if ((ec.value() == boost::asio::error::try_again) ||
-                           (ec.value() == boost::asio::error::would_block)) {
-                    // If we should try again make sure there is no garbage in the
-                    // bytes_transferred.
-                    bytes_transferred = 0;
-
-                } else {
-                    ADD_FAILURE() << "error occurred while connecting: "
-                                  << ec.message();
-                    io_service_.stop();
-                    return;
-                }
-            }
-
-            // Remove the part of the request which has been sent.
-            if (bytes_transferred > 0 && (request.size() <= bytes_transferred)) {
-                request.erase(0, bytes_transferred);
-            }
-
-            // Continue sending request data if there are still some data to be
-            // sent.
-            if (!request.empty()) {
-                sendPartialRequest(request);
-
-            } else {
-                // Request has been sent. Start receiving response.
-                response_.clear();
-                receivePartialResponse();
-            }
-       });
-    }
-
-    /// @brief Receive response from the server.
-    void receivePartialResponse() {
-        socket_.async_read_some(boost::asio::buffer(buf_.data(), buf_.size()),
-                                [this](const boost::system::error_code& ec,
-                                       std::size_t bytes_transferred) {
-            if (ec) {
-                // IO service stopped so simply return.
-                if (ec.value() == boost::asio::error::operation_aborted) {
-                    return;
-
-                } else if ((ec.value() == boost::asio::error::try_again) ||
-                           (ec.value() == boost::asio::error::would_block)) {
-                    // If we should try again, make sure that there is no garbage
-                    // in the bytes_transferred.
-                    bytes_transferred = 0;
-
-                } else {
-                    // Error occurred, bail...
-                    ADD_FAILURE() << "error occurred while receiving HTTP"
-                        " response from the server: " << ec.message();
-                    io_service_.stop();
-                }
-            }
-
-            if (bytes_transferred > 0) {
-                response_.insert(response_.end(), buf_.data(),
-                                 buf_.data() + bytes_transferred);
-            }
-
-            // Two consecutive new lines end the part of the response we're
-            // expecting.
-            if (response_.find("\r\n\r\n", 0) != std::string::npos) {
-                io_service_.stop();
-
-            } else {
-                receivePartialResponse();
-            }
-
-        });
-    }
-
-    /// @brief Checks if the TCP connection is still open.
-    ///
-    /// Tests the TCP connection by trying to read from the socket.
-    /// Unfortunately expected failure depends on a race between the read
-    /// and the other side close so to check if the connection is closed
-    /// please use @c isConnectionClosed instead.
-    ///
-    /// @return true if the TCP connection is open.
-    bool isConnectionAlive() {
-        // Remember the current non blocking setting.
-        const bool non_blocking_orig = socket_.non_blocking();
-        // Set the socket to non blocking mode. We're going to test if the socket
-        // returns would_block status on the attempt to read from it.
-        socket_.non_blocking(true);
-
-        // We need to provide a buffer for a call to read.
-        char data[2];
-        boost::system::error_code ec;
-        boost::asio::read(socket_, boost::asio::buffer(data, sizeof(data)), ec);
-
-        // Revert the original non_blocking flag on the socket.
-        socket_.non_blocking(non_blocking_orig);
-
-        // If the connection is alive we'd typically get would_block status code.
-        // If there are any data that haven't been read we may also get success
-        // status. We're guessing that try_again may also be returned by some
-        // implementations in some situations. Any other error code indicates a
-        // problem with the connection so we assume that the connection has been
-        // closed.
-        return (!ec || (ec.value() == boost::asio::error::try_again) ||
-                (ec.value() == boost::asio::error::would_block));
-    }
-
-    /// @brief Checks if the TCP connection is already closed.
-    ///
-    /// Tests the TCP connection by trying to read from the socket.
-    /// The read can block so this must be used to check if a connection
-    /// is alive so to check if the connection is alive please always
-    /// use @c isConnectionAlive.
-    ///
-    /// @return true if the TCP connection is closed.
-    bool isConnectionClosed() {
-        // Remember the current non blocking setting.
-        const bool non_blocking_orig = socket_.non_blocking();
-        // Set the socket to blocking mode. We're going to test if the socket
-        // returns eof status on the attempt to read from it.
-        socket_.non_blocking(false);
-
-        // We need to provide a buffer for a call to read.
-        char data[2];
-        boost::system::error_code ec;
-        boost::asio::read(socket_, boost::asio::buffer(data, sizeof(data)), ec);
-
-        // Revert the original non_blocking flag on the socket.
-        socket_.non_blocking(non_blocking_orig);
-
-        // If the connection is closed we'd typically get eof status code.
-        return (ec.value() == boost::asio::error::eof);
-    }
-
-    /// @brief Close connection.
-    void close() {
-        socket_.close();
-    }
-
-    std::string getResponse() const {
-        return (response_);
-    }
-
-private:
-
-    /// @brief Holds reference to the IO service.
-    boost::asio::io_service& io_service_;
-
-    /// @brief A socket used for the connection.
-    boost::asio::ip::tcp::socket socket_;
-
-    /// @brief Buffer into which response is written.
-    std::array<char, 8192> buf_;
-
-    /// @brief Response in the textual format.
-    std::string response_;
-};
-
 /// @brief Pointer to the TestHttpClient.
 typedef boost::shared_ptr<TestHttpClient> TestHttpClientPtr;
 
diff --git a/src/lib/http/tests/test_http_client.h b/src/lib/http/tests/test_http_client.h
new file mode 100644 (file)
index 0000000..c5ba0eb
--- /dev/null
@@ -0,0 +1,266 @@
+// Copyright (C) 2017-2021 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/.
+
+#ifndef TEST_HTTP_CLIENT_H
+#define TEST_HTTP_CLIENT_H
+
+#include <cc/data.h>
+#include <http/client.h>
+#include <http/http_types.h>
+
+#include <boost/asio/read.hpp>
+#include <boost/asio/buffer.hpp>
+#include <boost/asio/ip/tcp.hpp>
+#include <gtest/gtest.h>
+
+using namespace boost::asio::ip;
+using namespace isc::asiolink;
+
+/// @brief Entity which can connect to the HTTP server endpoint.
+class TestHttpClient : public boost::noncopyable {
+public:
+
+    /// @brief Constructor.
+    ///
+    /// This constructor creates new socket instance. It doesn't connect. Call
+    /// connect() to connect to the server.
+    ///
+    /// @param io_service IO service to be stopped on error or completion.
+    explicit TestHttpClient(IOService& io_service,
+                            const std::string& server_address = "127.0.0.1",
+                            uint16_t port = 18123)
+        : io_service_(io_service.get_io_service()), socket_(io_service_),
+          buf_(), response_(), server_address_(server_address),
+          server_port_(port), receive_done_(false) {
+    }
+
+    /// @brief Destructor.
+    ///
+    /// Closes the underlying socket if it is open.
+    ~TestHttpClient() {
+        close();
+    }
+
+    /// @brief Send HTTP request specified in textual format.
+    ///
+    /// @param request HTTP request in the textual format.
+    void startRequest(const std::string& request) {
+        tcp::endpoint endpoint(address::from_string(server_address_), server_port_);
+        socket_.async_connect(endpoint,
+        [this, request](const boost::system::error_code& ec) {
+            receive_done_ = false;
+            if (ec) {
+                // One would expect that async_connect wouldn't return
+                // EINPROGRESS error code, but simply wait for the connection
+                // to get established before the handler is invoked. It turns out,
+                // however, that on some OSes the connect handler may receive this
+                // error code which doesn't necessarily indicate a problem.
+                // Making an attempt to write and read from this socket will
+                // typically succeed. So, we ignore this error.
+                if (ec.value() != boost::asio::error::in_progress) {
+                    ADD_FAILURE() << "error occurred while connecting: "
+                                  << ec.message();
+                    io_service_.stop();
+                    return;
+                }
+            }
+            sendRequest(request);
+        });
+    }
+
+    /// @brief Send HTTP request.
+    ///
+    /// @param request HTTP request in the textual format.
+    void sendRequest(const std::string& request) {
+        sendPartialRequest(request);
+    }
+
+    /// @brief Send part of the HTTP request.
+    ///
+    /// @param request part of the HTTP request to be sent.
+    void sendPartialRequest(std::string request) {
+        socket_.async_send(boost::asio::buffer(request.data(), request.size()),
+                           [this, request](const boost::system::error_code& ec,
+                                           std::size_t bytes_transferred) mutable {
+            if (ec) {
+                if (ec.value() == boost::asio::error::operation_aborted) {
+                    return;
+
+                } else if ((ec.value() == boost::asio::error::try_again) ||
+                           (ec.value() == boost::asio::error::would_block)) {
+                    // If we should try again make sure there is no garbage in the
+                    // bytes_transferred.
+                    bytes_transferred = 0;
+
+                } else {
+                    ADD_FAILURE() << "error occurred while connecting: "
+                                  << ec.message();
+                    io_service_.stop();
+                    return;
+                }
+            }
+
+            // Remove the part of the request which has been sent.
+            if (bytes_transferred > 0 && (request.size() <= bytes_transferred)) {
+                request.erase(0, bytes_transferred);
+            }
+
+            // Continue sending request data if there are still some data to be
+            // sent.
+            if (!request.empty()) {
+                sendPartialRequest(request);
+
+            } else {
+                // Request has been sent. Start receiving response.
+                response_.clear();
+                receivePartialResponse();
+            }
+       });
+    }
+
+    /// @brief Receive response from the server.
+    void receivePartialResponse() {
+        socket_.async_read_some(boost::asio::buffer(buf_.data(), buf_.size()),
+                                [this](const boost::system::error_code& ec,
+                                       std::size_t bytes_transferred) {
+            if (ec) {
+                // IO service stopped so simply return.
+                if (ec.value() == boost::asio::error::operation_aborted) {
+                    std::cout << "this: " << this << "IO service stopped" << std::endl;
+                    return;
+
+                } else if ((ec.value() == boost::asio::error::try_again) ||
+                           (ec.value() == boost::asio::error::would_block)) {
+                    // If we should try again, make sure that there is no garbage
+                    // in the bytes_transferred.
+                    bytes_transferred = 0;
+
+                } else {
+                    // Error occurred, bail...
+                    ADD_FAILURE() << "error occurred while receiving HTTP"
+                        " response from the server: " << ec.message();
+                    io_service_.stop();
+                }
+            }
+
+            if (bytes_transferred > 0) {
+                response_.insert(response_.end(), buf_.data(),
+                                 buf_.data() + bytes_transferred);
+            }
+
+            // Two consecutive new lines end the part of the response we're
+            // expecting.
+            if (response_.find("\r\n\r\n", 0) != std::string::npos) {
+                receive_done_ = true;
+                io_service_.stop();
+            } else {
+                receivePartialResponse();
+            }
+        });
+    }
+
+    /// @brief Checks if the TCP connection is still open.
+    ///
+    /// Tests the TCP connection by trying to read from the socket.
+    /// Unfortunately expected failure depends on a race between the read
+    /// and the other side close so to check if the connection is closed
+    /// please use @c isConnectionClosed instead.
+    ///
+    /// @return true if the TCP connection is open.
+    bool isConnectionAlive() {
+        // Remember the current non blocking setting.
+        const bool non_blocking_orig = socket_.non_blocking();
+        // Set the socket to non blocking mode. We're going to test if the socket
+        // returns would_block status on the attempt to read from it.
+        socket_.non_blocking(true);
+
+        // We need to provide a buffer for a call to read.
+        char data[2];
+        boost::system::error_code ec;
+        boost::asio::read(socket_, boost::asio::buffer(data, sizeof(data)), ec);
+
+        // Revert the original non_blocking flag on the socket.
+        socket_.non_blocking(non_blocking_orig);
+
+        // If the connection is alive we'd typically get would_block status code.
+        // If there are any data that haven't been read we may also get success
+        // status. We're guessing that try_again may also be returned by some
+        // implementations in some situations. Any other error code indicates a
+        // problem with the connection so we assume that the connection has been
+        // closed.
+        return (!ec || (ec.value() == boost::asio::error::try_again) ||
+                (ec.value() == boost::asio::error::would_block));
+    }
+
+    /// @brief Checks if the TCP connection is already closed.
+    ///
+    /// Tests the TCP connection by trying to read from the socket.
+    /// The read can block so this must be used to check if a connection
+    /// is alive so to check if the connection is alive please always
+    /// use @c isConnectionAlive.
+    ///
+    /// @return true if the TCP connection is closed.
+    bool isConnectionClosed() {
+        // Remember the current non blocking setting.
+        const bool non_blocking_orig = socket_.non_blocking();
+        // Set the socket to blocking mode. We're going to test if the socket
+        // returns eof status on the attempt to read from it.
+        socket_.non_blocking(false);
+
+        // We need to provide a buffer for a call to read.
+        char data[2];
+        boost::system::error_code ec;
+        boost::asio::read(socket_, boost::asio::buffer(data, sizeof(data)), ec);
+
+        // Revert the original non_blocking flag on the socket.
+        socket_.non_blocking(non_blocking_orig);
+
+        // If the connection is closed we'd typically get eof status code.
+        return (ec.value() == boost::asio::error::eof);
+    }
+
+    /// @brief Close connection.
+    void close() {
+        socket_.close();
+    }
+
+    std::string getResponse() const {
+        return (response_);
+    }
+
+    /// @brief Returns true if the receive completed without error.
+    bool receiveDone() {
+        return (receive_done_);
+    }
+
+private:
+
+    /// @brief Holds reference to the IO service.
+    boost::asio::io_service& io_service_;
+
+    /// @brief A socket used for the connection.
+    boost::asio::ip::tcp::socket socket_;
+
+    /// @brief Buffer into which response is written.
+    std::array<char, 8192> buf_;
+
+    /// @brief Response in the textual format.
+    std::string response_;
+
+    /// @brief IP address of the server.
+    std::string server_address_;
+
+    /// @brief IP port of the server.
+    uint16_t server_port_;
+
+    /// @brief Set to true when the receive as completed successfully.
+    bool receive_done_;
+};
+
+/// @brief Pointer to the TestHttpClient.
+typedef boost::shared_ptr<TestHttpClient> TestHttpClientPtr;
+
+#endif