From: Francis Dupont Date: Sun, 28 Dec 2025 10:07:28 +0000 (+0100) Subject: [#4283] Added large UTs and reorg X-Git-Tag: Kea-3.1.5~87 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=f0923843bf9bfa5c53af4b2c1536f9261c0b9f70;p=thirdparty%2Fkea.git [#4283] Added large UTs and reorg --- diff --git a/src/lib/tcp/tests/common_client_test.cc b/src/lib/tcp/tests/common_client_test.cc new file mode 100644 index 0000000000..70e00debb9 --- /dev/null +++ b/src/lib/tcp/tests/common_client_test.cc @@ -0,0 +1,49 @@ +// Copyright (C) 2017-2025 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 + +namespace isc { +namespace tcp { +namespace test { + +/// @brief Completion checker. +/// +/// Messages are by 2 byte length header and data of this length. +/// +/// @param response Response to check. +/// @param error_msg Reference to the error message. +/// @return status (0 not complete, > 0 complete, < 0 error). +int +TestCompleteCheck(const WireDataPtr& response, std::string& error_msg) { + if (!response) { + error_msg = "response is null"; + return (-1); + } + size_t length = response->size(); + if (length < 2) { + return (-2); + } + const std::vector& content = *response; + size_t wanted = (content[0] << 8) | content[1]; + if (wanted + 2 == response->size()) { + // Complete. + return (1); + } else if (wanted + 2 > response->size()) { + // Not complete. + return (0); + } else { + // Overrun. + error_msg = "overrun"; + return (-3); + } +} + +} +} +} diff --git a/src/lib/tcp/tests/common_client_test.h b/src/lib/tcp/tests/common_client_test.h index c96a328bd6..3baa327a3a 100644 --- a/src/lib/tcp/tests/common_client_test.h +++ b/src/lib/tcp/tests/common_client_test.h @@ -7,6 +7,12 @@ #ifndef COMMON_CLIENT_TEST_H #define COMMON_CLIENT_TEST_H +using namespace isc; +using namespace isc::asiolink; +using namespace isc::tcp; +using namespace isc::util; +namespace ph = std::placeholders; + namespace isc { namespace tcp { namespace test { @@ -37,30 +43,7 @@ const long TEST_TIMEOUT = 10000; /// @param response Response to check. /// @param error_msg Reference to the error message. /// @return status (0 not complete, > 0 complete, < 0 error). -static int -TestCompleteCheck(const WireDataPtr& response, std::string& error_msg) { - if (!response) { - error_msg = "response is null"; - return (-1); - } - size_t length = response->size(); - if (length < 2) { - return (-2); - } - const std::vector& content = *response; - size_t wanted = (content[0] << 8) | content[1]; - if (wanted + 2 == response->size()) { - // Complete. - return (1); - } else if (wanted + 2 > response->size()) { - // Not complete. - return (0); - } else { - // Overrun. - error_msg = "overrun"; - return (-3); - } -} +int TestCompleteCheck(const WireDataPtr& response, std::string& error_msg); /// @brief Derivation of TcpResponse used for testing. class TcpTestResponse : public TcpResponse { @@ -131,6 +114,10 @@ public: response.reset(new TcpTestResponse(bad)); asyncSendResponse(response); return; + } else if (request_str.find("Large", 0) != std::string::npos) { + response_str.resize(0xffff); + } else if (request_str.size() > 60000) { + response_str = "large!"; } else { response_str = request_str; } @@ -221,998 +208,6 @@ protected: /// @brief Defines a pointer to a TcpTestListener. typedef boost::shared_ptr TcpTestListenerPtr; -/// @brief Test fixture class for @ref TcpListener. -class TcpListenerTest : public ::testing::Test { -public: - - /// @brief Constructor. - /// - /// Starts test timer which detects timeouts. - TcpListenerTest() - : io_service_(new IOService()), test_timer_(io_service_), - run_io_service_timer_(io_service_) { - test_timer_.setup(std::bind(&TcpListenerTest::timeoutHandler, this, true), - TEST_TIMEOUT, IntervalTimer::ONE_SHOT); - } - - /// @brief Destructor. - /// - /// Removes active TCP clients. - virtual ~TcpListenerTest() { - test_timer_.cancel(); - io_service_->stopAndPoll(); - } - - /// @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. - /// - /// @param timeout Optional value specifying for how long the io service - /// should be ran. - void runIOService(long timeout = 0) { - io_service_->stop(); - io_service_->restart(); - - if (timeout > 0) { - run_io_service_timer_.setup(std::bind(&TcpListenerTest::timeoutHandler, - this, false), - timeout, IntervalTimer::ONE_SHOT); - } - io_service_->run(); - io_service_->stopAndPoll(false); - } - - /// @brief IO service used in the tests. - IOServicePtr 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 Test fixture class for testing TCP client. -class BaseTcpClientTest : public TcpListenerTest { -public: - - /// @brief Constructor. - BaseTcpClientTest() - : TcpListenerTest(), listener_(), listener2_(), listener3_(), - server_context_(), client_context_(), client_context2_() { - } - - /// @brief Destructor. - virtual ~BaseTcpClientTest() { - listener_->stop(); - listener2_->stop(); - listener3_->stop(); - io_service_->stopAndPoll(); - MultiThreadingMgr::instance().setMode(false); - } - - /// @brief Creates TCP request. - /// - /// It includes a parameter with a specified value. - /// - /// @param parameter_name Parameter to be included. - /// @param value Parameter value. - template - WireDataPtr createRequest(const std::string& parameter_name, - const ValueType& value) { - std::ostringstream oss; - oss << parameter_name << "=" << value; - std::string data = oss.str(); - std::vector content; - content.resize(2 + data.size()); - content[0] = (data.size() >> 8) & 0xff; - content[1] = data.size() & 0xff; - memmove(&content[2], &data[0], content.size() - 2); - WireDataPtr request(new WireData(content)); - return (request); - } - - /// @brief Test that two consecutive requests can be sent over the same - /// connection (if persistent, if not persistent two connections will - /// be used). - /// - /// @param persistent Persistent flag. - void testConsecutiveRequests(bool persistent) { - // Start the server. - ASSERT_NO_THROW(listener_->start()); - - // Create a client and specify the server. - TcpClient client(io_service_, false); - asiolink::IOAddress address("127.0.0.1"); - uint16_t port = 18123; - - // Initiate request to the server. - WireDataPtr request1 = createRequest("sequence", 1); - WireDataPtr response1(new WireData()); - unsigned resp_num = 0; - ASSERT_NO_THROW(client.asyncSendRequest(address, port, - client_context_, - request1, response1, - persistent, TestCompleteCheck, - [this, &resp_num](const boost::system::error_code& ec, - const WireDataPtr&, - const std::string&) { - if (++resp_num > 1) { - io_service_->stop(); - } - if (ec) { - ADD_FAILURE() << "asyncSendRequest failed: " << ec.message(); - } - })); - - // Initiate another request to the destination. - WireDataPtr request2 = createRequest("sequence", 2); - WireDataPtr response2(new WireData()); - ASSERT_NO_THROW(client.asyncSendRequest(address, port, - client_context_, - request2, response2, - persistent, TestCompleteCheck, - [this, &resp_num](const boost::system::error_code& ec, - const WireDataPtr&, - const std::string&) { - if (++resp_num > 1) { - io_service_->stop(); - } - if (ec) { - ADD_FAILURE() << "asyncSendRequest failed: " << ec.message(); - } - })); - - // Actually trigger the requests. The requests should be handlded by the - // server one after another. While the first request is being processed - // the server should queue another one. - ASSERT_NO_THROW(runIOService()); - - // Make sure that the received responses are different. - ASSERT_TRUE(response1); - ASSERT_TRUE(response2); - EXPECT_NE(*response1, *response2); - } - - /// @brief Test that the client can communicate with two different - /// destinations simultaneously. - void testMultipleDestinations() { - // Start two servers running on different ports. - ASSERT_NO_THROW(listener_->start()); - ASSERT_NO_THROW(listener2_->start()); - - // Create the client. It will be communicating with the two servers. - TcpClient client(io_service_, false); - - // Specify the addresses and ports of the servers. - IOAddress address1("127.0.0.1"); - uint16_t port1 = 18123; - IOAddress address2("::1"); - uint16_t port2 = 18124; - - // Create a request to the first server. - WireDataPtr request1 = createRequest("sequence", 1); - WireDataPtr response1(new WireData()); - unsigned resp_num = 0; - ASSERT_NO_THROW(client.asyncSendRequest(address1, port1, - client_context_, - request1, response1, - true, TestCompleteCheck, - [this, &resp_num](const boost::system::error_code& ec, - const WireDataPtr&, - const std::string&) { - if (++resp_num > 1) { - io_service_->stop(); - } - if (ec) { - ADD_FAILURE() << "asyncSendRequest failed: " << ec.message(); - } - })); - - // Create a request to the second server. - WireDataPtr request2 = createRequest("sequence", 2); - WireDataPtr response2(new WireData()); - ASSERT_NO_THROW(client.asyncSendRequest(address2, port2, - client_context_, - request2, response2, - true, TestCompleteCheck, - [this, &resp_num](const boost::system::error_code& ec, - const WireDataPtr&, - const std::string&) { - if (++resp_num > 1) { - io_service_->stop(); - } - if (ec) { - ADD_FAILURE() << "asyncSendRequest failed: " << ec.message(); - } - })); - - // Actually trigger the requests. - ASSERT_NO_THROW(runIOService()); - - // Make sure we have received two different responses. - ASSERT_TRUE(response1); - ASSERT_TRUE(response2); - EXPECT_NE(*response1, *response2); - } - - /// @brief Test that the client can communicate with the same destination - /// address and port but with different TLS contexts too. - void testMultipleTlsContexts() { - // Start only one server. - ASSERT_NO_THROW(listener_->start()); - - // Create the client. - TcpClient client(io_service_, false); - - // Specify the address and port of the server. - asiolink::IOAddress address("127.0.0.1"); - uint16_t port = 18123; - - // Create a request to the first server. - WireDataPtr request1 = createRequest("sequence", 1); - WireDataPtr response1(new WireData()); - unsigned resp_num = 0; - ASSERT_NO_THROW(client.asyncSendRequest(address, port, - client_context_, - request1, response1, - true, TestCompleteCheck, - [this, &resp_num](const boost::system::error_code& ec, - const WireDataPtr&, - const std::string&) { - if (++resp_num > 1) { - io_service_->stop(); - } - if (ec) { - ADD_FAILURE() << "asyncSendRequest failed: " << ec.message(); - } - })); - - // Create a request with the second TLS context. - WireDataPtr request2 = createRequest("sequence", 2); - WireDataPtr response2(new WireData()); - ASSERT_NO_THROW(client.asyncSendRequest(address, port, - client_context2_, - request2, response2, - true, TestCompleteCheck, - [this, &resp_num](const boost::system::error_code& ec, - const WireDataPtr&, - const std::string&) { - if (++resp_num > 1) { - io_service_->stop(); - } - if (ec) { - ADD_FAILURE() << "asyncSendRequest failed: " << ec.message(); - } - })); - - // Actually trigger the requests. - ASSERT_NO_THROW(runIOService()); - - // Make sure we have received two different responses. - ASSERT_TRUE(response1); - ASSERT_TRUE(response2); - EXPECT_NE(*response1, *response2); - } - - /// @brief Test that idle connection can be resumed for second request. - void testIdleConnection() { - // Start the server that has short idle timeout. It closes the idle - // connection after 200ms. - ASSERT_NO_THROW(listener3_->start()); - - // Create the client that will communicate with this server. - TcpClient client(io_service_, false); - - // Specify the address and port of this server. - asiolink::IOAddress address("127.0.0.1"); - uint16_t port = 18125; - - // Create the first request. - WireDataPtr request1 = createRequest("sequence", 1); - WireDataPtr response1(new WireData()); - ASSERT_NO_THROW(client.asyncSendRequest(address, port, - client_context_, - request1, response1, - true, TestCompleteCheck, - [this](const boost::system::error_code& ec, const WireDataPtr&, - const std::string&) { - io_service_->stop(); - if (ec) { - ADD_FAILURE() << "asyncSendRequest failed: " << ec.message(); - } - })); - - // Run the IO service until the response is received. - ASSERT_NO_THROW(runIOService()); - - // Make sure the response has been received. - ASSERT_TRUE(response1); - EXPECT_EQ(request1->size(), response1->size()); - - // Delay the generation of the second request by 2x server idle timeout. - // This should be enough to cause the server to close the connection. - ASSERT_NO_THROW(runIOService(SHORT_IDLE_TIMEOUT * 2)); - - // Create another request. - WireDataPtr request2 = createRequest("sequence", 2); - WireDataPtr response2(new WireData()); - ASSERT_NO_THROW(client.asyncSendRequest(address, port, - client_context_, - request2, response2, - true, TestCompleteCheck, - [this](const boost::system::error_code& ec, const WireDataPtr&, - const std::string&) { - io_service_->stop(); - if (ec) { - ADD_FAILURE() << "asyncSendRequest failed: " << ec.message(); - } - })); - - // Actually trigger the second request. - ASSERT_NO_THROW(runIOService()); - - // Make sure that the server has responded. - ASSERT_TRUE(response2); - EXPECT_EQ(request2->size(), response2->size()); - EXPECT_NE(*response1, *response2); - } - - /// @brief This test verifies that the client returns IO error code when the - /// server is unreachable. - void testUnreachable () { - // Create the client. - TcpClient client(io_service_, false); - - // Specify the address and port of the server. This server is down. - asiolink::IOAddress address("127.0.0.1"); - uint16_t port = 18123; - - // Create the request. - WireDataPtr request = createRequest("sequence", 1); - WireDataPtr response(new WireData()); - ASSERT_NO_THROW(client.asyncSendRequest(address, port, - client_context_, - request, response, - true, TestCompleteCheck, - [this](const boost::system::error_code& ec, - const WireDataPtr&, - const std::string&) { - io_service_->stop(); - // The server should have returned an IO error. - if (!ec) { - ADD_FAILURE() << "asyncSendRequest didn't fail"; - } - })); - - // Actually trigger the request. - ASSERT_NO_THROW(runIOService()); - } - - void testMalformedResponse() { - // Start the server. - ASSERT_NO_THROW(listener_->start()); - - // Create the client. - TcpClient client(io_service_, false); - - // Specify the address and port of the server. - asiolink::IOAddress address("127.0.0.1"); - uint16_t port = 18123; - - WireDataPtr request = createRequest("Malformed", "..."); - WireDataPtr response(new WireData()); - ASSERT_NO_THROW(client.asyncSendRequest(address, port, - client_context_, - request, response, - true, TestCompleteCheck, - [this](const boost::system::error_code& ec, - const WireDataPtr& response, - const std::string& parsing_error) { - io_service_->stop(); - // There should be no IO error (answer from the server is received). - if (ec) { - ADD_FAILURE() << "asyncSendRequest failed: " << ec.message(); - } - // The response object is null. - EXPECT_FALSE(response); - // The message parsing error should be returned. - EXPECT_FALSE(parsing_error.empty()); - })); - - // Actually trigger the request. - ASSERT_NO_THROW(runIOService()); - } - - /// @brief Test that client times out when it doesn't receive the entire - /// response from the server within a desired time. - void testClientRequestTimeout() { - // Start the server. - ASSERT_NO_THROW(listener_->start()); - - // Create the client. - TcpClient client(io_service_, false); - - // Specify the address and port of the server. - asiolink::IOAddress address("127.0.0.1"); - uint16_t port = 18123; - - unsigned cb_num = 0; - - WireDataPtr request1 = createRequest("Partial", "..."); - WireDataPtr response1(new WireData()); - // This value will be set to true if the connection close callback is - // invoked upon time out. - auto connection_closed = false; - ASSERT_NO_THROW(client.asyncSendRequest(address, port, - client_context_, - request1, response1, - true, TestCompleteCheck, - [this, &cb_num](const boost::system::error_code& ec, - const WireDataPtr& response, - const std::string&) { - if (++cb_num > 1) { - io_service_->stop(); - } - // In this particular case we know exactly the type of the - // IO error returned, because the client explicitly sets this - // error code. - EXPECT_TRUE(ec.value() == boost::asio::error::timed_out); - // There should be no response returned. - EXPECT_FALSE(response); - }, - TcpClient::RequestTimeout(100), - TcpClient::ConnectHandler(), - TcpClient::HandshakeHandler(), - [&connection_closed](const int) { - // This callback is called when the connection gets closed - // by the client. - connection_closed = true; - }) - ); - - // Create another request after the timeout. It should be handled ok. - WireDataPtr request2 = createRequest("sequence", 1); - WireDataPtr response2(new WireData()); - ASSERT_NO_THROW(client.asyncSendRequest(address, port, - client_context_, - request2, response2, - true, TestCompleteCheck, - [this, &cb_num](const boost::system::error_code& /*ec*/, - const WireDataPtr&, - const std::string&) { - if (++cb_num > 1) { - io_service_->stop(); - } - })); - - // Actually trigger the requests. - ASSERT_NO_THROW(runIOService()); - // Make sure that the client has closed the connection upon timeout. - EXPECT_TRUE(connection_closed); - } - - /// @brief Test that client times out when connection takes too long. - void testClientConnectTimeout() { - // Start the server. - ASSERT_NO_THROW(listener_->start()); - - // Create the client. - TcpClient client(io_service_, false); - - // Specify the address and port of the server. - asiolink::IOAddress address("127.0.0.1"); - uint16_t port = 18123; - - unsigned cb_num = 0; - - WireDataPtr request = createRequest("sequence", 1); - WireDataPtr response(new WireData()); - ASSERT_NO_THROW(client.asyncSendRequest(address, port, - client_context_, - request, response, - true, TestCompleteCheck, - [this, &cb_num](const boost::system::error_code& ec, - const WireDataPtr& response, - const std::string&) { - if (++cb_num > 1) { - io_service_->stop(); - } - // In this particular case we know exactly the type of the - // IO error returned, because the client explicitly sets this - // error code. - EXPECT_TRUE(ec.value() == boost::asio::error::timed_out); - // There should be no response returned. - EXPECT_FALSE(response); - - }, - TcpClient::RequestTimeout(100), - - // This callback is invoked upon an attempt to connect to the - // server. The false value indicates to the TcpClient to not - // try to send a request to the server. This simulates the - // case of connect() taking very long and should eventually - // cause the transaction to time out. - [](const boost::system::error_code& /*ec*/, int) { - return (false); - })); - - // Create another request after the timeout. It should be handled ok. - ASSERT_NO_THROW(client.asyncSendRequest(address, port, - client_context_, - request, response, - true, TestCompleteCheck, - [this, &cb_num](const boost::system::error_code& /*ec*/, - const WireDataPtr&, - const std::string&) { - if (++cb_num > 1) { - io_service_->stop(); - } - })); - - // Actually trigger the requests. - ASSERT_NO_THROW(runIOService()); - } - - /// @brief Tests the behavior of the TCP client when the premature - /// timeout occurs. - /// - /// The premature timeout may occur when the system clock is moved - /// during the transaction. This test simulates this behavior by - /// starting new transaction and waiting for the timeout to occur - /// before the IO service is ran. The timeout handler is invoked - /// first and it resets the transaction state. This test verifies - /// that the started transaction tears down gracefully after the - /// transaction state is reset. - /// - /// There are two variants of this test. The first variant schedules - /// one transaction before running the IO service. The second variant - /// schedules two transactions prior to running the IO service. The - /// second transaction is queued, so it is expected that it doesn't - /// time out and it runs successfully. - /// - /// @param queue_two_requests Boolean value indicating if a single - /// transaction should be queued (false), or two (true). - void testClientRequestLateStart(const bool queue_two_requests) { - // Start the server. - ASSERT_NO_THROW(listener_->start()); - - // Create the client. - TcpClient client(io_service_, false); - - // Specify the address and port of the server. - asiolink::IOAddress address("127.0.0.1"); - uint16_t port = 18123; - - // Generate first request. - WireDataPtr request1 = createRequest("sequence", 1); - WireDataPtr response1(new WireData()); - - // Use very short timeout to make sure that it occurs before we actually - // run the transaction. - ASSERT_NO_THROW(client.asyncSendRequest(address, port, - client_context_, - request1, response1, - true, TestCompleteCheck, - [](const boost::system::error_code& ec, - const WireDataPtr& response, - const std::string&) { - - // In this particular case we know exactly the type of the - // IO error returned, because the client explicitly sets this - // error code. - EXPECT_TRUE(ec.value() == boost::asio::error::timed_out); - // There should be no response returned. - EXPECT_FALSE(response); - }, - TcpClient::RequestTimeout(1))); - - if (queue_two_requests) { - WireDataPtr request2 = createRequest("sequence", 2); - WireDataPtr response2(new WireData()); - ASSERT_NO_THROW(client.asyncSendRequest(address, port, - client_context_, - request2, response2, - true, TestCompleteCheck, - [](const boost::system::error_code& ec, - const WireDataPtr& response, - const std::string&) { - - // This second request should be successful. - if (ec) { - ADD_FAILURE() << "asyncSendRequest failed: " << ec.message(); - } - EXPECT_TRUE(response); - })); - } - - // This waits for 3ms to make sure that the timeout occurs before we - // run the transaction. This leads to an unusual situation that the - // transaction state is reset as a result of the timeout but the - // transaction is alive. We want to make sure that the client can - // gracefully deal with this situation. - usleep(3000); - - // Run the transaction and hope it will gracefully tear down. - ASSERT_NO_THROW(runIOService(100)); - - // Now try to send another request to make sure that the client - // is healthy. - WireDataPtr request3 = createRequest("sequence", 3); - WireDataPtr response3(new WireData()); - ASSERT_NO_THROW(client.asyncSendRequest(address, port, - client_context_, - request3, response3, - true, TestCompleteCheck, - [this](const boost::system::error_code& ec, - const WireDataPtr&, - const std::string&) { - io_service_->stop(); - - // Everything should be ok. - if (ec) { - ADD_FAILURE() << "asyncSendRequest failed: " << ec.message(); - } - })); - - // Actually trigger the requests. - ASSERT_NO_THROW(runIOService()); - } - - /// @brief Tests that underlying TCP socket can be registered and - /// unregistered via connection and close callbacks. - /// - /// It conducts to consecutive requests over the same client. - /// - /// @param persistent Persistent flag. - void testConnectCloseCallbacks(bool persistent) { - // Start the server. - ASSERT_NO_THROW(listener_->start()); - - // Create a client and specify the address and port of the server. - TcpClient client(io_service_, false); - asiolink::IOAddress address("127.0.0.1"); - uint16_t port = 18123; - - // Initiate request to the server. - WireDataPtr request1 = createRequest("sequence", 1); - WireDataPtr response1(new WireData()); - unsigned resp_num = 0; - ExternalMonitor monitor(!!client_context_); - - ASSERT_NO_THROW(client.asyncSendRequest(address, port, - client_context_, - request1, response1, - persistent, TestCompleteCheck, - [this, &resp_num](const boost::system::error_code& ec, - const WireDataPtr&, - const std::string&) { - if (++resp_num > 1) { - io_service_->stop(); - } - - if (ec) { - ADD_FAILURE() << "asyncSendRequest failed: " << ec.message(); - - } - }, - TcpClient::RequestTimeout(10000), - std::bind(&ExternalMonitor::connectHandler, &monitor, ph::_1, ph::_2), - std::bind(&ExternalMonitor::handshakeHandler, &monitor, ph::_1, ph::_2), - std::bind(&ExternalMonitor::closeHandler, &monitor, ph::_1) - )); - - // Initiate another request to the destination. - WireDataPtr request2 = createRequest("sequence", 2); - WireDataPtr response2(new WireData()); - ASSERT_NO_THROW(client.asyncSendRequest(address, port, - client_context_, - request2, response2, - persistent, TestCompleteCheck, - [this, &resp_num](const boost::system::error_code& ec, - const WireDataPtr&, - const std::string&) { - if (++resp_num > 1) { - io_service_->stop(); - } - if (ec) { - ADD_FAILURE() << "asyncSendRequest failed: " << ec.message(); - } - }, - TcpClient::RequestTimeout(10000), - std::bind(&ExternalMonitor::connectHandler, &monitor, ph::_1, ph::_2), - std::bind(&ExternalMonitor::handshakeHandler, &monitor, ph::_1, ph::_2), - std::bind(&ExternalMonitor::closeHandler, &monitor, ph::_1) - )); - - // Actually trigger the requests. The requests should be handlded by the - // server one after another. While the first request is being processed - // the server should queue another one. - ASSERT_NO_THROW(runIOService()); - - // We should have had 2 connect invocations, no closes - // and a valid registered fd - EXPECT_EQ(2, monitor.connect_cnt_); - EXPECT_EQ(0, monitor.close_cnt_); - EXPECT_GT(monitor.registered_fd_, -1); - - // Make sure that the received responses are different. - ASSERT_TRUE(response1); - ASSERT_TRUE(response2); - EXPECT_NE(*response1, *response2); - - // Stopping the client the close the connection. - client.stop(); - - // We should have had 2 connect invocations, 1 closes - // and an invalid registered fd - EXPECT_EQ(2, monitor.connect_cnt_); - EXPECT_EQ(1, monitor.close_cnt_); - EXPECT_EQ(-1, monitor.registered_fd_); - } - - /// @brief Tests detection and handling out-of-band socket events - /// - /// It initiates a transaction and verifies that a mid-transaction call - /// to TcpClient::closeIfOutOfBand() has no affect on the connection. - /// After successful completion of the transaction, a second call to - /// TcpClient::closeIfOutOfBand() is made. This should result in the - /// connection being closed. - /// This step is repeated to verify that after an OOB closure, transactions - /// to the same destination can be processed. - /// - /// Lastly, we verify that TcpClient::stop() closes the connection correctly. - /// - /// @param persistent Persistent flag. - void testCloseIfOutOfBand(bool persistent) { - // Start the server. - ASSERT_NO_THROW(listener_->start()); - - // Create a client and specify the address and port of the server. - TcpClient client(io_service_, false); - asiolink::IOAddress address("127.0.0.1"); - uint16_t port = 18123; - - // Initiate request to the server. - WireDataPtr request1 = createRequest("sequence", 1); - WireDataPtr response1(new WireData()); - unsigned resp_num = 0; - ExternalMonitor monitor(!!client_context_); - - ASSERT_NO_THROW(client.asyncSendRequest(address, port, - client_context_, - request1, response1, - persistent, TestCompleteCheck, - [this, &client, &resp_num, &monitor](const boost::system::error_code& ec, - const WireDataPtr&, - const std::string&) { - if (++resp_num == 1) { - io_service_->stop(); - } - - // We should have 1 connect. - EXPECT_EQ(1, monitor.connect_cnt_); - // We should have 1 handshake with TLS. - if (client_context_) { - EXPECT_EQ(1, monitor.handshake_cnt_); - } - // We should have 0 closes - EXPECT_EQ(0, monitor.close_cnt_); - // We should have a valid fd. - ASSERT_GT(monitor.registered_fd_, -1); - int orig_fd = monitor.registered_fd_; - - // Test our socket for OOBness. - client.closeIfOutOfBand(monitor.registered_fd_); - - // Since we're in a transaction, we should have no closes and - // the same valid fd. - EXPECT_EQ(0, monitor.close_cnt_); - ASSERT_EQ(monitor.registered_fd_, orig_fd); - - if (ec) { - ADD_FAILURE() << "asyncSendRequest failed: " << ec.message(); - } - }, - TcpClient::RequestTimeout(10000), - std::bind(&ExternalMonitor::connectHandler, &monitor, ph::_1, ph::_2), - std::bind(&ExternalMonitor::handshakeHandler, &monitor, ph::_1, ph::_2), - std::bind(&ExternalMonitor::closeHandler, &monitor, ph::_1) - )); - - // Actually trigger the requests. The requests should be handlded by the - // server one after another. While the first request is being processed - // the server should queue another one. - ASSERT_NO_THROW(runIOService()); - - // Make sure that we received a response. - ASSERT_TRUE(response1); - EXPECT_EQ(request1->size(), response1->size()); - - // We should have had 1 connect invocations, no closes - // and a valid registered fd - EXPECT_EQ(1, monitor.connect_cnt_); - EXPECT_EQ(0, monitor.close_cnt_); - EXPECT_GT(monitor.registered_fd_, -1); - - // Test our socket for OOBness. - client.closeIfOutOfBand(monitor.registered_fd_); - - // Since we're in a transaction, we should have no closes and - // the same valid fd. - EXPECT_EQ(1, monitor.close_cnt_); - EXPECT_EQ(-1, monitor.registered_fd_); - - // Now let's do another request to the destination to verify that - // we'll reopen the connection without issue. - WireDataPtr request2 = createRequest("sequence", 2); - WireDataPtr response2(new WireData()); - resp_num = 0; - ASSERT_NO_THROW(client.asyncSendRequest(address, port, - client_context_, - request2, response2, - persistent, TestCompleteCheck, - [this, &client, &resp_num, &monitor](const boost::system::error_code& ec, - const WireDataPtr&, - const std::string&) { - if (++resp_num == 1) { - io_service_->stop(); - } - - // We should have 2 connects. - EXPECT_EQ(2, monitor.connect_cnt_); - // We should have 2 handshakes when TLS is enabled. - if (client_context_) { - EXPECT_EQ(2, monitor.handshake_cnt_); - } - // We should have 1 close. - EXPECT_EQ(1, monitor.close_cnt_); - // We should have a valid fd. - ASSERT_GT(monitor.registered_fd_, -1); - int orig_fd = monitor.registered_fd_; - - // Test our socket for OOBness. - client.closeIfOutOfBand(monitor.registered_fd_); - - // Since we're in a transaction, we should have no closes and - // the same valid fd. - EXPECT_EQ(1, monitor.close_cnt_); - ASSERT_EQ(monitor.registered_fd_, orig_fd); - - if (ec) { - ADD_FAILURE() << "asyncSendRequest failed: " << ec.message(); - } - }, - TcpClient::RequestTimeout(10000), - std::bind(&ExternalMonitor::connectHandler, &monitor, ph::_1, ph::_2), - std::bind(&ExternalMonitor::handshakeHandler, &monitor, ph::_1, ph::_2), - std::bind(&ExternalMonitor::closeHandler, &monitor, ph::_1) - )); - - // Actually trigger the requests. The requests should be handlded by the - // server one after another. While the first request is being processed - // the server should queue another one. - ASSERT_NO_THROW(runIOService()); - - // Make sure that we received the second response. - ASSERT_TRUE(response2); - EXPECT_EQ(request2->size(), response2->size()); - EXPECT_NE(*response1, *response2); - - // Stopping the client the close the connection. - client.stop(); - - // We should have had 2 connect invocations, 2 closes - // and an invalid registered fd - EXPECT_EQ(2, monitor.connect_cnt_); - EXPECT_EQ(2, monitor.close_cnt_); - EXPECT_EQ(-1, monitor.registered_fd_); - } - - /// @brief Simulates external registery of Connection TCP sockets - /// - /// Provides methods compatible with Connection callbacks for connect - /// and close operations. - class ExternalMonitor { - public: - /// @brief Constructor - /// - /// @param use_tls Use TLS flag. - ExternalMonitor(bool use_tls = false) - : use_tls_(use_tls), registered_fd_(-1), connect_cnt_(0), - handshake_cnt_(0), close_cnt_(0) { - } - - /// @brief Connect callback handler - /// @param ec Error status of the ASIO connect - /// @param tcp_native_fd socket descriptor to register - bool connectHandler(const boost::system::error_code& ec, int tcp_native_fd) { - ++connect_cnt_; - if ((!ec || (ec.value() == boost::asio::error::in_progress)) - && (tcp_native_fd >= 0)) { - registered_fd_ = tcp_native_fd; - return (true); - } else if ((ec.value() == boost::asio::error::already_connected) - && (registered_fd_ != tcp_native_fd)) { - return (false); - } - - // ec indicates an error, return true, so that error can be handled - // by Connection logic. - return (true); - } - - /// @brief Handshake callback handler - /// @param ec Error status of the ASIO connect - bool handshakeHandler(const boost::system::error_code&, int) { - if (use_tls_) { - ++handshake_cnt_; - } else { - ADD_FAILURE() << "handshake callback handler is called"; - } - // ec indicates an error, return true, so that error can be handled - // by Connection logic. - return (true); - } - - /// @brief Close callback handler - /// - /// @param tcp_native_fd socket descriptor to register - void closeHandler(int tcp_native_fd) { - ++close_cnt_; - EXPECT_EQ(tcp_native_fd, registered_fd_) << "closeHandler fd mismatch"; - if (tcp_native_fd >= 0) { - registered_fd_ = -1; - } - } - - /// @brief Use TLS flag. - bool use_tls_; - - /// @brief Keeps track of socket currently "registered" for external monitoring. - int registered_fd_; - - /// @brief Tracks how many times the connect callback is invoked. - int connect_cnt_; - - /// @brief Tracks how many times the handshake callback is invoked. - int handshake_cnt_; - - /// @brief Tracks how many times the close callback is invoked. - int close_cnt_; - }; - - /// @brief Instance of the listener used in the tests. - std::unique_ptr listener_; - - /// @brief Instance of the second listener used in the tests. - std::unique_ptr listener2_; - - /// @brief Instance of the third listener used in the tests (with short idle - /// timeout). - std::unique_ptr listener3_; - - /// @brief Server TLS context. - TlsContextPtr server_context_; - - /// @brief Client TLS context. - TlsContextPtr client_context_; - - /// @brief Alternate client TLS context. - TlsContextPtr client_context2_; -}; - } } } diff --git a/src/lib/tcp/tests/common_client_unittests.h b/src/lib/tcp/tests/common_client_unittests.h new file mode 100644 index 0000000000..da469e13d9 --- /dev/null +++ b/src/lib/tcp/tests/common_client_unittests.h @@ -0,0 +1,1119 @@ +// Copyright (C) 2017-2025 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 COMMON_CLIENT_UNITTESTS_H +#define COMMON_CLIENT_UNITTESTS_H + +namespace isc { +namespace tcp { +namespace test { + +/// @brief Test fixture class for @ref TcpListener. +class TcpListenerTest : public ::testing::Test { +public: + + /// @brief Constructor. + /// + /// Starts test timer which detects timeouts. + TcpListenerTest() + : io_service_(new IOService()), test_timer_(io_service_), + run_io_service_timer_(io_service_) { + test_timer_.setup(std::bind(&TcpListenerTest::timeoutHandler, this, true), + TEST_TIMEOUT, IntervalTimer::ONE_SHOT); + } + + /// @brief Destructor. + /// + /// Removes active TCP clients. + virtual ~TcpListenerTest() { + test_timer_.cancel(); + io_service_->stopAndPoll(); + } + + /// @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. + /// + /// @param timeout Optional value specifying for how long the io service + /// should be ran. + void runIOService(long timeout = 0) { + io_service_->stop(); + io_service_->restart(); + + if (timeout > 0) { + run_io_service_timer_.setup(std::bind(&TcpListenerTest::timeoutHandler, + this, false), + timeout, IntervalTimer::ONE_SHOT); + } + io_service_->run(); + io_service_->stopAndPoll(false); + } + + /// @brief IO service used in the tests. + IOServicePtr 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 Test fixture class for testing TCP client. +class BaseTcpClientTest : public TcpListenerTest { +public: + + /// @brief Constructor. + BaseTcpClientTest() + : TcpListenerTest(), listener_(), listener2_(), listener3_(), + server_context_(), client_context_(), client_context2_() { + } + + /// @brief Destructor. + virtual ~BaseTcpClientTest() { + listener_->stop(); + listener2_->stop(); + listener3_->stop(); + io_service_->stopAndPoll(); + MultiThreadingMgr::instance().setMode(false); + } + + /// @brief Creates TCP request. + /// + /// It includes a parameter with a specified value. + /// + /// @param parameter_name Parameter to be included. + /// @param value Parameter value. + template + WireDataPtr createRequest(const std::string& parameter_name, + const ValueType& value) { + std::ostringstream oss; + oss << parameter_name << "=" << value; + std::string data = oss.str(); + std::vector content; + content.resize(2 + data.size()); + content[0] = (data.size() >> 8) & 0xff; + content[1] = data.size() & 0xff; + memmove(&content[2], &data[0], content.size() - 2); + WireDataPtr request(new WireData(content)); + return (request); + } + + /// @brief Test a request. + void testSingleRequest() { + // Start the server. + ASSERT_NO_THROW(listener_->start()); + + // Create a client and specify the server. + TcpClient client(io_service_, false); + asiolink::IOAddress address("127.0.0.1"); + uint16_t port = 18123; + + // Initiate request to the server. + WireDataPtr request = createRequest("sequence", 0); + WireDataPtr response(new WireData()); + ASSERT_NO_THROW(client.asyncSendRequest(address, port, + client_context_, + request, response, + true, TestCompleteCheck, + [this](const boost::system::error_code& ec, + const WireDataPtr&, + const std::string&) { + io_service_->stop(); + if (ec) { + ADD_FAILURE() << "asyncSendRequest failed: " << ec.message(); + } + })); + + // Actually trigger the request. The request should be handlded by the + // server. + ASSERT_NO_THROW(runIOService()); + + // Make sure that the response was received. + ASSERT_TRUE(response); + EXPECT_EQ(request->size(), response->size()); + } + + /// @brief Test a large request. + void testLargeRequest() { + // Start the server. + ASSERT_NO_THROW(listener_->start()); + + // Create a client and specify the server. + TcpClient client(io_service_, false); + asiolink::IOAddress address("127.0.0.1"); + uint16_t port = 18123; + + // Initiate request to the server. + std::vector large = { 0xff, 0xff }; + large.resize(0xffff + 2); + WireDataPtr request(new WireData(large)); + WireDataPtr response(new WireData()); + ASSERT_NO_THROW(client.asyncSendRequest(address, port, + client_context_, + request, response, + true, TestCompleteCheck, + [this](const boost::system::error_code& ec, + const WireDataPtr&, + const std::string&) { + io_service_->stop(); + if (ec) { + ADD_FAILURE() << "asyncSendRequest failed: " << ec.message(); + } + })); + + // Actually trigger the request. The request should be handlded by the + // server. + ASSERT_NO_THROW(runIOService()); + + // Make sure that the response was received. + ASSERT_TRUE(response); + const std::vector expected = { + 0x00, 0x06, 0x6c, 0x61, 0x72, 0x67, 0x65, 0x21 }; + ASSERT_EQ(8, response->size()); + EXPECT_EQ(expected, *response); + } + + /// @brief Test a large response. + void testLargeResponse() { + // Start the server. + ASSERT_NO_THROW(listener_->start()); + + // Create a client and specify the server. + TcpClient client(io_service_, false); + asiolink::IOAddress address("127.0.0.1"); + uint16_t port = 18123; + + // Initiate request to the server. + WireDataPtr request = createRequest("Large", 0); + WireDataPtr response(new WireData()); + ASSERT_NO_THROW(client.asyncSendRequest(address, port, + client_context_, + request, response, + true, TestCompleteCheck, + [this](const boost::system::error_code& ec, + const WireDataPtr&, + const std::string&) { + io_service_->stop(); + if (ec) { + ADD_FAILURE() << "asyncSendRequest failed: " << ec.message(); + } + })); + + // Actually trigger the request. The request should be handlded by the + // server. + ASSERT_NO_THROW(runIOService()); + + // Make sure that the response was received. + ASSERT_TRUE(response); + EXPECT_EQ(0xffff + 2, response->size()); + } + + /// @brief Test that two consecutive requests can be sent over the same + /// connection (if persistent, if not persistent two connections will + /// be used). + /// + /// @param persistent Persistent flag. + void testConsecutiveRequests(bool persistent) { + // Start the server. + ASSERT_NO_THROW(listener_->start()); + + // Create a client and specify the server. + TcpClient client(io_service_, false); + asiolink::IOAddress address("127.0.0.1"); + uint16_t port = 18123; + + // Initiate request to the server. + WireDataPtr request1 = createRequest("sequence", 1); + WireDataPtr response1(new WireData()); + unsigned resp_num = 0; + ASSERT_NO_THROW(client.asyncSendRequest(address, port, + client_context_, + request1, response1, + persistent, TestCompleteCheck, + [this, &resp_num](const boost::system::error_code& ec, + const WireDataPtr&, + const std::string&) { + if (++resp_num > 1) { + io_service_->stop(); + } + if (ec) { + ADD_FAILURE() << "asyncSendRequest failed: " << ec.message(); + } + })); + + // Initiate another request to the destination. + WireDataPtr request2 = createRequest("sequence", 2); + WireDataPtr response2(new WireData()); + ASSERT_NO_THROW(client.asyncSendRequest(address, port, + client_context_, + request2, response2, + persistent, TestCompleteCheck, + [this, &resp_num](const boost::system::error_code& ec, + const WireDataPtr&, + const std::string&) { + if (++resp_num > 1) { + io_service_->stop(); + } + if (ec) { + ADD_FAILURE() << "asyncSendRequest failed: " << ec.message(); + } + })); + + // Actually trigger the requests. The requests should be handlded by the + // server one after another. While the first request is being processed + // the server should queue another one. + ASSERT_NO_THROW(runIOService()); + + // Make sure that the received responses are different. + ASSERT_TRUE(response1); + ASSERT_TRUE(response2); + EXPECT_NE(*response1, *response2); + } + + /// @brief Test that the client can communicate with two different + /// destinations simultaneously. + void testMultipleDestinations() { + // Start two servers running on different ports. + ASSERT_NO_THROW(listener_->start()); + ASSERT_NO_THROW(listener2_->start()); + + // Create the client. It will be communicating with the two servers. + TcpClient client(io_service_, false); + + // Specify the addresses and ports of the servers. + IOAddress address1("127.0.0.1"); + uint16_t port1 = 18123; + IOAddress address2("::1"); + uint16_t port2 = 18124; + + // Create a request to the first server. + WireDataPtr request1 = createRequest("sequence", 1); + WireDataPtr response1(new WireData()); + unsigned resp_num = 0; + ASSERT_NO_THROW(client.asyncSendRequest(address1, port1, + client_context_, + request1, response1, + true, TestCompleteCheck, + [this, &resp_num](const boost::system::error_code& ec, + const WireDataPtr&, + const std::string&) { + if (++resp_num > 1) { + io_service_->stop(); + } + if (ec) { + ADD_FAILURE() << "asyncSendRequest failed: " << ec.message(); + } + })); + + // Create a request to the second server. + WireDataPtr request2 = createRequest("sequence", 2); + WireDataPtr response2(new WireData()); + ASSERT_NO_THROW(client.asyncSendRequest(address2, port2, + client_context_, + request2, response2, + true, TestCompleteCheck, + [this, &resp_num](const boost::system::error_code& ec, + const WireDataPtr&, + const std::string&) { + if (++resp_num > 1) { + io_service_->stop(); + } + if (ec) { + ADD_FAILURE() << "asyncSendRequest failed: " << ec.message(); + } + })); + + // Actually trigger the requests. + ASSERT_NO_THROW(runIOService()); + + // Make sure we have received two different responses. + ASSERT_TRUE(response1); + ASSERT_TRUE(response2); + EXPECT_NE(*response1, *response2); + } + + /// @brief Test that the client can communicate with the same destination + /// address and port but with different TLS contexts too. + void testMultipleTlsContexts() { + // Start only one server. + ASSERT_NO_THROW(listener_->start()); + + // Create the client. + TcpClient client(io_service_, false); + + // Specify the address and port of the server. + asiolink::IOAddress address("127.0.0.1"); + uint16_t port = 18123; + + // Create a request to the first server. + WireDataPtr request1 = createRequest("sequence", 1); + WireDataPtr response1(new WireData()); + unsigned resp_num = 0; + ASSERT_NO_THROW(client.asyncSendRequest(address, port, + client_context_, + request1, response1, + true, TestCompleteCheck, + [this, &resp_num](const boost::system::error_code& ec, + const WireDataPtr&, + const std::string&) { + if (++resp_num > 1) { + io_service_->stop(); + } + if (ec) { + ADD_FAILURE() << "asyncSendRequest failed: " << ec.message(); + } + })); + + // Create a request with the second TLS context. + WireDataPtr request2 = createRequest("sequence", 2); + WireDataPtr response2(new WireData()); + ASSERT_NO_THROW(client.asyncSendRequest(address, port, + client_context2_, + request2, response2, + true, TestCompleteCheck, + [this, &resp_num](const boost::system::error_code& ec, + const WireDataPtr&, + const std::string&) { + if (++resp_num > 1) { + io_service_->stop(); + } + if (ec) { + ADD_FAILURE() << "asyncSendRequest failed: " << ec.message(); + } + })); + + // Actually trigger the requests. + ASSERT_NO_THROW(runIOService()); + + // Make sure we have received two different responses. + ASSERT_TRUE(response1); + ASSERT_TRUE(response2); + EXPECT_NE(*response1, *response2); + } + + /// @brief Test that idle connection can be resumed for second request. + void testIdleConnection() { + // Start the server that has short idle timeout. It closes the idle + // connection after 200ms. + ASSERT_NO_THROW(listener3_->start()); + + // Create the client that will communicate with this server. + TcpClient client(io_service_, false); + + // Specify the address and port of this server. + asiolink::IOAddress address("127.0.0.1"); + uint16_t port = 18125; + + // Create the first request. + WireDataPtr request1 = createRequest("sequence", 1); + WireDataPtr response1(new WireData()); + ASSERT_NO_THROW(client.asyncSendRequest(address, port, + client_context_, + request1, response1, + true, TestCompleteCheck, + [this](const boost::system::error_code& ec, const WireDataPtr&, + const std::string&) { + io_service_->stop(); + if (ec) { + ADD_FAILURE() << "asyncSendRequest failed: " << ec.message(); + } + })); + + // Run the IO service until the response is received. + ASSERT_NO_THROW(runIOService()); + + // Make sure the response has been received. + ASSERT_TRUE(response1); + EXPECT_EQ(request1->size(), response1->size()); + + // Delay the generation of the second request by 2x server idle timeout. + // This should be enough to cause the server to close the connection. + ASSERT_NO_THROW(runIOService(SHORT_IDLE_TIMEOUT * 2)); + + // Create another request. + WireDataPtr request2 = createRequest("sequence", 2); + WireDataPtr response2(new WireData()); + ASSERT_NO_THROW(client.asyncSendRequest(address, port, + client_context_, + request2, response2, + true, TestCompleteCheck, + [this](const boost::system::error_code& ec, const WireDataPtr&, + const std::string&) { + io_service_->stop(); + if (ec) { + ADD_FAILURE() << "asyncSendRequest failed: " << ec.message(); + } + })); + + // Actually trigger the second request. + ASSERT_NO_THROW(runIOService()); + + // Make sure that the server has responded. + ASSERT_TRUE(response2); + EXPECT_EQ(request2->size(), response2->size()); + EXPECT_NE(*response1, *response2); + } + + /// @brief This test verifies that the client returns IO error code when the + /// server is unreachable. + void testUnreachable () { + // Create the client. + TcpClient client(io_service_, false); + + // Specify the address and port of the server. This server is down. + asiolink::IOAddress address("127.0.0.1"); + uint16_t port = 18123; + + // Create the request. + WireDataPtr request = createRequest("sequence", 1); + WireDataPtr response(new WireData()); + ASSERT_NO_THROW(client.asyncSendRequest(address, port, + client_context_, + request, response, + true, TestCompleteCheck, + [this](const boost::system::error_code& ec, + const WireDataPtr&, + const std::string&) { + io_service_->stop(); + // The server should have returned an IO error. + if (!ec) { + ADD_FAILURE() << "asyncSendRequest didn't fail"; + } + })); + + // Actually trigger the request. + ASSERT_NO_THROW(runIOService()); + } + + void testMalformedResponse() { + // Start the server. + ASSERT_NO_THROW(listener_->start()); + + // Create the client. + TcpClient client(io_service_, false); + + // Specify the address and port of the server. + asiolink::IOAddress address("127.0.0.1"); + uint16_t port = 18123; + + WireDataPtr request = createRequest("Malformed", "..."); + WireDataPtr response(new WireData()); + ASSERT_NO_THROW(client.asyncSendRequest(address, port, + client_context_, + request, response, + true, TestCompleteCheck, + [this](const boost::system::error_code& ec, + const WireDataPtr& response, + const std::string& parsing_error) { + io_service_->stop(); + // There should be no IO error (answer from the server is received). + if (ec) { + ADD_FAILURE() << "asyncSendRequest failed: " << ec.message(); + } + // The response object is null. + EXPECT_FALSE(response); + // The message parsing error should be returned. + EXPECT_FALSE(parsing_error.empty()); + })); + + // Actually trigger the request. + ASSERT_NO_THROW(runIOService()); + } + + /// @brief Test that client times out when it doesn't receive the entire + /// response from the server within a desired time. + void testClientRequestTimeout() { + // Start the server. + ASSERT_NO_THROW(listener_->start()); + + // Create the client. + TcpClient client(io_service_, false); + + // Specify the address and port of the server. + asiolink::IOAddress address("127.0.0.1"); + uint16_t port = 18123; + + unsigned cb_num = 0; + + WireDataPtr request1 = createRequest("Partial", "..."); + WireDataPtr response1(new WireData()); + // This value will be set to true if the connection close callback is + // invoked upon time out. + auto connection_closed = false; + ASSERT_NO_THROW(client.asyncSendRequest(address, port, + client_context_, + request1, response1, + true, TestCompleteCheck, + [this, &cb_num](const boost::system::error_code& ec, + const WireDataPtr& response, + const std::string&) { + if (++cb_num > 1) { + io_service_->stop(); + } + // In this particular case we know exactly the type of the + // IO error returned, because the client explicitly sets this + // error code. + EXPECT_TRUE(ec.value() == boost::asio::error::timed_out); + // There should be no response returned. + EXPECT_FALSE(response); + }, + TcpClient::RequestTimeout(100), + TcpClient::ConnectHandler(), + TcpClient::HandshakeHandler(), + [&connection_closed](const int) { + // This callback is called when the connection gets closed + // by the client. + connection_closed = true; + }) + ); + + // Create another request after the timeout. It should be handled ok. + WireDataPtr request2 = createRequest("sequence", 1); + WireDataPtr response2(new WireData()); + ASSERT_NO_THROW(client.asyncSendRequest(address, port, + client_context_, + request2, response2, + true, TestCompleteCheck, + [this, &cb_num](const boost::system::error_code& /*ec*/, + const WireDataPtr&, + const std::string&) { + if (++cb_num > 1) { + io_service_->stop(); + } + })); + + // Actually trigger the requests. + ASSERT_NO_THROW(runIOService()); + // Make sure that the client has closed the connection upon timeout. + EXPECT_TRUE(connection_closed); + } + + /// @brief Test that client times out when connection takes too long. + void testClientConnectTimeout() { + // Start the server. + ASSERT_NO_THROW(listener_->start()); + + // Create the client. + TcpClient client(io_service_, false); + + // Specify the address and port of the server. + asiolink::IOAddress address("127.0.0.1"); + uint16_t port = 18123; + + unsigned cb_num = 0; + + WireDataPtr request = createRequest("sequence", 1); + WireDataPtr response(new WireData()); + ASSERT_NO_THROW(client.asyncSendRequest(address, port, + client_context_, + request, response, + true, TestCompleteCheck, + [this, &cb_num](const boost::system::error_code& ec, + const WireDataPtr& response, + const std::string&) { + if (++cb_num > 1) { + io_service_->stop(); + } + // In this particular case we know exactly the type of the + // IO error returned, because the client explicitly sets this + // error code. + EXPECT_TRUE(ec.value() == boost::asio::error::timed_out); + // There should be no response returned. + EXPECT_FALSE(response); + + }, + TcpClient::RequestTimeout(100), + + // This callback is invoked upon an attempt to connect to the + // server. The false value indicates to the TcpClient to not + // try to send a request to the server. This simulates the + // case of connect() taking very long and should eventually + // cause the transaction to time out. + [](const boost::system::error_code& /*ec*/, int) { + return (false); + })); + + // Create another request after the timeout. It should be handled ok. + ASSERT_NO_THROW(client.asyncSendRequest(address, port, + client_context_, + request, response, + true, TestCompleteCheck, + [this, &cb_num](const boost::system::error_code& /*ec*/, + const WireDataPtr&, + const std::string&) { + if (++cb_num > 1) { + io_service_->stop(); + } + })); + + // Actually trigger the requests. + ASSERT_NO_THROW(runIOService()); + } + + /// @brief Tests the behavior of the TCP client when the premature + /// timeout occurs. + /// + /// The premature timeout may occur when the system clock is moved + /// during the transaction. This test simulates this behavior by + /// starting new transaction and waiting for the timeout to occur + /// before the IO service is ran. The timeout handler is invoked + /// first and it resets the transaction state. This test verifies + /// that the started transaction tears down gracefully after the + /// transaction state is reset. + /// + /// There are two variants of this test. The first variant schedules + /// one transaction before running the IO service. The second variant + /// schedules two transactions prior to running the IO service. The + /// second transaction is queued, so it is expected that it doesn't + /// time out and it runs successfully. + /// + /// @param queue_two_requests Boolean value indicating if a single + /// transaction should be queued (false), or two (true). + void testClientRequestLateStart(const bool queue_two_requests) { + // Start the server. + ASSERT_NO_THROW(listener_->start()); + + // Create the client. + TcpClient client(io_service_, false); + + // Specify the address and port of the server. + asiolink::IOAddress address("127.0.0.1"); + uint16_t port = 18123; + + // Generate first request. + WireDataPtr request1 = createRequest("sequence", 1); + WireDataPtr response1(new WireData()); + + // Use very short timeout to make sure that it occurs before we actually + // run the transaction. + ASSERT_NO_THROW(client.asyncSendRequest(address, port, + client_context_, + request1, response1, + true, TestCompleteCheck, + [](const boost::system::error_code& ec, + const WireDataPtr& response, + const std::string&) { + + // In this particular case we know exactly the type of the + // IO error returned, because the client explicitly sets this + // error code. + EXPECT_TRUE(ec.value() == boost::asio::error::timed_out); + // There should be no response returned. + EXPECT_FALSE(response); + }, + TcpClient::RequestTimeout(1))); + + if (queue_two_requests) { + WireDataPtr request2 = createRequest("sequence", 2); + WireDataPtr response2(new WireData()); + ASSERT_NO_THROW(client.asyncSendRequest(address, port, + client_context_, + request2, response2, + true, TestCompleteCheck, + [](const boost::system::error_code& ec, + const WireDataPtr& response, + const std::string&) { + + // This second request should be successful. + if (ec) { + ADD_FAILURE() << "asyncSendRequest failed: " << ec.message(); + } + EXPECT_TRUE(response); + })); + } + + // This waits for 3ms to make sure that the timeout occurs before we + // run the transaction. This leads to an unusual situation that the + // transaction state is reset as a result of the timeout but the + // transaction is alive. We want to make sure that the client can + // gracefully deal with this situation. + usleep(3000); + + // Run the transaction and hope it will gracefully tear down. + ASSERT_NO_THROW(runIOService(100)); + + // Now try to send another request to make sure that the client + // is healthy. + WireDataPtr request3 = createRequest("sequence", 3); + WireDataPtr response3(new WireData()); + ASSERT_NO_THROW(client.asyncSendRequest(address, port, + client_context_, + request3, response3, + true, TestCompleteCheck, + [this](const boost::system::error_code& ec, + const WireDataPtr&, + const std::string&) { + io_service_->stop(); + + // Everything should be ok. + if (ec) { + ADD_FAILURE() << "asyncSendRequest failed: " << ec.message(); + } + })); + + // Actually trigger the requests. + ASSERT_NO_THROW(runIOService()); + } + + /// @brief Tests that underlying TCP socket can be registered and + /// unregistered via connection and close callbacks. + /// + /// It conducts to consecutive requests over the same client. + /// + /// @param persistent Persistent flag. + void testConnectCloseCallbacks(bool persistent) { + // Start the server. + ASSERT_NO_THROW(listener_->start()); + + // Create a client and specify the address and port of the server. + TcpClient client(io_service_, false); + asiolink::IOAddress address("127.0.0.1"); + uint16_t port = 18123; + + // Initiate request to the server. + WireDataPtr request1 = createRequest("sequence", 1); + WireDataPtr response1(new WireData()); + unsigned resp_num = 0; + ExternalMonitor monitor(!!client_context_); + + ASSERT_NO_THROW(client.asyncSendRequest(address, port, + client_context_, + request1, response1, + persistent, TestCompleteCheck, + [this, &resp_num](const boost::system::error_code& ec, + const WireDataPtr&, + const std::string&) { + if (++resp_num > 1) { + io_service_->stop(); + } + + if (ec) { + ADD_FAILURE() << "asyncSendRequest failed: " << ec.message(); + + } + }, + TcpClient::RequestTimeout(10000), + std::bind(&ExternalMonitor::connectHandler, &monitor, ph::_1, ph::_2), + std::bind(&ExternalMonitor::handshakeHandler, &monitor, ph::_1, ph::_2), + std::bind(&ExternalMonitor::closeHandler, &monitor, ph::_1) + )); + + // Initiate another request to the destination. + WireDataPtr request2 = createRequest("sequence", 2); + WireDataPtr response2(new WireData()); + ASSERT_NO_THROW(client.asyncSendRequest(address, port, + client_context_, + request2, response2, + persistent, TestCompleteCheck, + [this, &resp_num](const boost::system::error_code& ec, + const WireDataPtr&, + const std::string&) { + if (++resp_num > 1) { + io_service_->stop(); + } + if (ec) { + ADD_FAILURE() << "asyncSendRequest failed: " << ec.message(); + } + }, + TcpClient::RequestTimeout(10000), + std::bind(&ExternalMonitor::connectHandler, &monitor, ph::_1, ph::_2), + std::bind(&ExternalMonitor::handshakeHandler, &monitor, ph::_1, ph::_2), + std::bind(&ExternalMonitor::closeHandler, &monitor, ph::_1) + )); + + // Actually trigger the requests. The requests should be handlded by the + // server one after another. While the first request is being processed + // the server should queue another one. + ASSERT_NO_THROW(runIOService()); + + // We should have had 2 connect invocations, no closes + // and a valid registered fd + EXPECT_EQ(2, monitor.connect_cnt_); + EXPECT_EQ(0, monitor.close_cnt_); + EXPECT_GT(monitor.registered_fd_, -1); + + // Make sure that the received responses are different. + ASSERT_TRUE(response1); + ASSERT_TRUE(response2); + EXPECT_NE(*response1, *response2); + + // Stopping the client the close the connection. + client.stop(); + + // We should have had 2 connect invocations, 1 closes + // and an invalid registered fd + EXPECT_EQ(2, monitor.connect_cnt_); + EXPECT_EQ(1, monitor.close_cnt_); + EXPECT_EQ(-1, monitor.registered_fd_); + } + + /// @brief Tests detection and handling out-of-band socket events + /// + /// It initiates a transaction and verifies that a mid-transaction call + /// to TcpClient::closeIfOutOfBand() has no affect on the connection. + /// After successful completion of the transaction, a second call to + /// TcpClient::closeIfOutOfBand() is made. This should result in the + /// connection being closed. + /// This step is repeated to verify that after an OOB closure, transactions + /// to the same destination can be processed. + /// + /// Lastly, we verify that TcpClient::stop() closes the connection correctly. + /// + /// @param persistent Persistent flag. + void testCloseIfOutOfBand(bool persistent) { + // Start the server. + ASSERT_NO_THROW(listener_->start()); + + // Create a client and specify the address and port of the server. + TcpClient client(io_service_, false); + asiolink::IOAddress address("127.0.0.1"); + uint16_t port = 18123; + + // Initiate request to the server. + WireDataPtr request1 = createRequest("sequence", 1); + WireDataPtr response1(new WireData()); + unsigned resp_num = 0; + ExternalMonitor monitor(!!client_context_); + + ASSERT_NO_THROW(client.asyncSendRequest(address, port, + client_context_, + request1, response1, + persistent, TestCompleteCheck, + [this, &client, &resp_num, &monitor](const boost::system::error_code& ec, + const WireDataPtr&, + const std::string&) { + if (++resp_num == 1) { + io_service_->stop(); + } + + // We should have 1 connect. + EXPECT_EQ(1, monitor.connect_cnt_); + // We should have 1 handshake with TLS. + if (client_context_) { + EXPECT_EQ(1, monitor.handshake_cnt_); + } + // We should have 0 closes + EXPECT_EQ(0, monitor.close_cnt_); + // We should have a valid fd. + ASSERT_GT(monitor.registered_fd_, -1); + int orig_fd = monitor.registered_fd_; + + // Test our socket for OOBness. + client.closeIfOutOfBand(monitor.registered_fd_); + + // Since we're in a transaction, we should have no closes and + // the same valid fd. + EXPECT_EQ(0, monitor.close_cnt_); + ASSERT_EQ(monitor.registered_fd_, orig_fd); + + if (ec) { + ADD_FAILURE() << "asyncSendRequest failed: " << ec.message(); + } + }, + TcpClient::RequestTimeout(10000), + std::bind(&ExternalMonitor::connectHandler, &monitor, ph::_1, ph::_2), + std::bind(&ExternalMonitor::handshakeHandler, &monitor, ph::_1, ph::_2), + std::bind(&ExternalMonitor::closeHandler, &monitor, ph::_1) + )); + + // Actually trigger the requests. The requests should be handlded by the + // server one after another. While the first request is being processed + // the server should queue another one. + ASSERT_NO_THROW(runIOService()); + + // Make sure that we received a response. + ASSERT_TRUE(response1); + EXPECT_EQ(request1->size(), response1->size()); + + // We should have had 1 connect invocations, no closes + // and a valid registered fd + EXPECT_EQ(1, monitor.connect_cnt_); + EXPECT_EQ(0, monitor.close_cnt_); + EXPECT_GT(monitor.registered_fd_, -1); + + // Test our socket for OOBness. + client.closeIfOutOfBand(monitor.registered_fd_); + + // Since we're in a transaction, we should have no closes and + // the same valid fd. + EXPECT_EQ(1, monitor.close_cnt_); + EXPECT_EQ(-1, monitor.registered_fd_); + + // Now let's do another request to the destination to verify that + // we'll reopen the connection without issue. + WireDataPtr request2 = createRequest("sequence", 2); + WireDataPtr response2(new WireData()); + resp_num = 0; + ASSERT_NO_THROW(client.asyncSendRequest(address, port, + client_context_, + request2, response2, + persistent, TestCompleteCheck, + [this, &client, &resp_num, &monitor](const boost::system::error_code& ec, + const WireDataPtr&, + const std::string&) { + if (++resp_num == 1) { + io_service_->stop(); + } + + // We should have 2 connects. + EXPECT_EQ(2, monitor.connect_cnt_); + // We should have 2 handshakes when TLS is enabled. + if (client_context_) { + EXPECT_EQ(2, monitor.handshake_cnt_); + } + // We should have 1 close. + EXPECT_EQ(1, monitor.close_cnt_); + // We should have a valid fd. + ASSERT_GT(monitor.registered_fd_, -1); + int orig_fd = monitor.registered_fd_; + + // Test our socket for OOBness. + client.closeIfOutOfBand(monitor.registered_fd_); + + // Since we're in a transaction, we should have no closes and + // the same valid fd. + EXPECT_EQ(1, monitor.close_cnt_); + ASSERT_EQ(monitor.registered_fd_, orig_fd); + + if (ec) { + ADD_FAILURE() << "asyncSendRequest failed: " << ec.message(); + } + }, + TcpClient::RequestTimeout(10000), + std::bind(&ExternalMonitor::connectHandler, &monitor, ph::_1, ph::_2), + std::bind(&ExternalMonitor::handshakeHandler, &monitor, ph::_1, ph::_2), + std::bind(&ExternalMonitor::closeHandler, &monitor, ph::_1) + )); + + // Actually trigger the requests. The requests should be handlded by the + // server one after another. While the first request is being processed + // the server should queue another one. + ASSERT_NO_THROW(runIOService()); + + // Make sure that we received the second response. + ASSERT_TRUE(response2); + EXPECT_EQ(request2->size(), response2->size()); + EXPECT_NE(*response1, *response2); + + // Stopping the client the close the connection. + client.stop(); + + // We should have had 2 connect invocations, 2 closes + // and an invalid registered fd + EXPECT_EQ(2, monitor.connect_cnt_); + EXPECT_EQ(2, monitor.close_cnt_); + EXPECT_EQ(-1, monitor.registered_fd_); + } + + /// @brief Simulates external registery of Connection TCP sockets + /// + /// Provides methods compatible with Connection callbacks for connect + /// and close operations. + class ExternalMonitor { + public: + /// @brief Constructor + /// + /// @param use_tls Use TLS flag. + ExternalMonitor(bool use_tls = false) + : use_tls_(use_tls), registered_fd_(-1), connect_cnt_(0), + handshake_cnt_(0), close_cnt_(0) { + } + + /// @brief Connect callback handler + /// @param ec Error status of the ASIO connect + /// @param tcp_native_fd socket descriptor to register + bool connectHandler(const boost::system::error_code& ec, int tcp_native_fd) { + ++connect_cnt_; + if ((!ec || (ec.value() == boost::asio::error::in_progress)) + && (tcp_native_fd >= 0)) { + registered_fd_ = tcp_native_fd; + return (true); + } else if ((ec.value() == boost::asio::error::already_connected) + && (registered_fd_ != tcp_native_fd)) { + return (false); + } + + // ec indicates an error, return true, so that error can be handled + // by Connection logic. + return (true); + } + + /// @brief Handshake callback handler + /// @param ec Error status of the ASIO connect + bool handshakeHandler(const boost::system::error_code&, int) { + if (use_tls_) { + ++handshake_cnt_; + } else { + ADD_FAILURE() << "handshake callback handler is called"; + } + // ec indicates an error, return true, so that error can be handled + // by Connection logic. + return (true); + } + + /// @brief Close callback handler + /// + /// @param tcp_native_fd socket descriptor to register + void closeHandler(int tcp_native_fd) { + ++close_cnt_; + EXPECT_EQ(tcp_native_fd, registered_fd_) << "closeHandler fd mismatch"; + if (tcp_native_fd >= 0) { + registered_fd_ = -1; + } + } + + /// @brief Use TLS flag. + bool use_tls_; + + /// @brief Keeps track of socket currently "registered" for external monitoring. + int registered_fd_; + + /// @brief Tracks how many times the connect callback is invoked. + int connect_cnt_; + + /// @brief Tracks how many times the handshake callback is invoked. + int handshake_cnt_; + + /// @brief Tracks how many times the close callback is invoked. + int close_cnt_; + }; + + /// @brief Instance of the listener used in the tests. + std::unique_ptr listener_; + + /// @brief Instance of the second listener used in the tests. + std::unique_ptr listener2_; + + /// @brief Instance of the third listener used in the tests (with short idle + /// timeout). + std::unique_ptr listener3_; + + /// @brief Server TLS context. + TlsContextPtr server_context_; + + /// @brief Client TLS context. + TlsContextPtr client_context_; + + /// @brief Alternate client TLS context. + TlsContextPtr client_context2_; +}; + +} +} +} +#endif // COMMON_CLIENT_UNITTESTS_H diff --git a/src/lib/tcp/tests/meson.build b/src/lib/tcp/tests/meson.build index 4e35d74488..2f832e73ca 100644 --- a/src/lib/tcp/tests/meson.build +++ b/src/lib/tcp/tests/meson.build @@ -4,6 +4,7 @@ endif kea_tcp_tests = executable( 'kea-tcp-tests', + 'common_client_test.cc', 'mt_client_unittests.cc', 'mt_tcp_listener_mgr_unittests.cc', 'run_unittests.cc', diff --git a/src/lib/tcp/tests/mt_client_unittests.cc b/src/lib/tcp/tests/mt_client_unittests.cc index b7e45f1738..89249bc248 100644 --- a/src/lib/tcp/tests/mt_client_unittests.cc +++ b/src/lib/tcp/tests/mt_client_unittests.cc @@ -23,14 +23,8 @@ #include #include -using namespace isc::asiolink; -using namespace isc::util; -namespace ph = std::placeholders; #include -using namespace isc; -using namespace isc::data; -using namespace isc::tcp; using namespace isc::tcp::test; namespace { diff --git a/src/lib/tcp/tests/tcp_client_unittests.cc b/src/lib/tcp/tests/tcp_client_unittests.cc index 4ecc35ebc4..e6a40e07bf 100644 --- a/src/lib/tcp/tests/tcp_client_unittests.cc +++ b/src/lib/tcp/tests/tcp_client_unittests.cc @@ -24,14 +24,9 @@ #include #include -using namespace isc::asiolink; -using namespace isc::util; -namespace ph = std::placeholders; #include +#include -using namespace boost::asio::ip; -using namespace isc::data; -using namespace isc::tcp; using namespace isc::tcp::test; namespace { @@ -64,6 +59,39 @@ public: virtual ~TcpClientTest() = default; }; +/// @brief Test a single request. +TEST_F(TcpClientTest, singleRequest) { + ASSERT_NO_FATAL_FAILURE(testSingleRequest()); +} + +/// @brief Test a single request. +TEST_F(TcpClientTest, singleRequestMultiThreading) { + MultiThreadingMgr::instance().setMode(true); + ASSERT_NO_FATAL_FAILURE(testSingleRequest()); +} + +/// @brief Test a large request. +TEST_F(TcpClientTest, largeRequest) { + ASSERT_NO_FATAL_FAILURE(testLargeRequest()); +} + +/// @brief Test a large request. +TEST_F(TcpClientTest, largeRequestMultiThreading) { + MultiThreadingMgr::instance().setMode(true); + ASSERT_NO_FATAL_FAILURE(testLargeRequest()); +} + +/// @brief Test a large response. +TEST_F(TcpClientTest, largeResponse) { + ASSERT_NO_FATAL_FAILURE(testLargeResponse()); +} + +/// @brief Test a large response. +TEST_F(TcpClientTest, largeResponseMultiThreading) { + MultiThreadingMgr::instance().setMode(true); + ASSERT_NO_FATAL_FAILURE(testLargeResponse()); +} + // Test that two consecutive requests can be sent over the same (persistent) // connection. TEST_F(TcpClientTest, consecutiveRequests) { diff --git a/src/lib/tcp/tests/tls_client_unittests.cc b/src/lib/tcp/tests/tls_client_unittests.cc index 68cd039cd3..f91bd3c42f 100644 --- a/src/lib/tcp/tests/tls_client_unittests.cc +++ b/src/lib/tcp/tests/tls_client_unittests.cc @@ -35,16 +35,10 @@ #endif #endif -using namespace isc::asiolink; -using namespace isc::util; -namespace ph = std::placeholders; #include +#include -using namespace boost::asio; -using namespace boost::asio::ip; using namespace isc::asiolink::test; -using namespace isc::data; -using namespace isc::tcp; using namespace isc::tcp::test; namespace { @@ -80,6 +74,39 @@ public: virtual ~TlsClientTest() = default; }; +/// @brief Test a single request. +TEST_F(TlsClientTest, singleRequest) { + ASSERT_NO_FATAL_FAILURE(testSingleRequest()); +} + +/// @brief Test a single request. +TEST_F(TlsClientTest, singleRequestMultiThreading) { + MultiThreadingMgr::instance().setMode(true); + ASSERT_NO_FATAL_FAILURE(testSingleRequest()); +} + +/// @brief Test a large request. +TEST_F(TlsClientTest, largeRequest) { + ASSERT_NO_FATAL_FAILURE(testLargeRequest()); +} + +/// @brief Test a large request. +TEST_F(TlsClientTest, largeRequestMultiThreading) { + MultiThreadingMgr::instance().setMode(true); + ASSERT_NO_FATAL_FAILURE(testLargeRequest()); +} + +/// @brief Test a large response. +TEST_F(TlsClientTest, largeResponse) { + ASSERT_NO_FATAL_FAILURE(testLargeResponse()); +} + +/// @brief Test a large response. +TEST_F(TlsClientTest, largeResponseMultiThreading) { + MultiThreadingMgr::instance().setMode(true); + ASSERT_NO_FATAL_FAILURE(testLargeResponse()); +} + #ifndef DISABLE_SOME_TESTS // Test that two consecutive requests can be sent over the same (persistent) // connection.