summaryrefslogtreecommitdiffstats
path: root/src/lib/dhcpsrv/tests/d2_udp_unittest.cc
diff options
context:
space:
mode:
Diffstat (limited to 'src/lib/dhcpsrv/tests/d2_udp_unittest.cc')
-rw-r--r--src/lib/dhcpsrv/tests/d2_udp_unittest.cc504
1 files changed, 504 insertions, 0 deletions
diff --git a/src/lib/dhcpsrv/tests/d2_udp_unittest.cc b/src/lib/dhcpsrv/tests/d2_udp_unittest.cc
new file mode 100644
index 0000000..2d83e97
--- /dev/null
+++ b/src/lib/dhcpsrv/tests/d2_udp_unittest.cc
@@ -0,0 +1,504 @@
+// Copyright (C) 2014-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/.
+
+/// @file d2_upd_unittest.cc Unit tests for D2ClientMgr UDP communications.
+/// Note these tests are not intended to verify the actual send and receive
+/// across UDP sockets. This level of testing is done in libdhcp-ddns.
+
+#include <config.h>
+#include <asiolink/asio_wrapper.h>
+#include <asiolink/io_service.h>
+#include <dhcp/iface_mgr.h>
+#include <dhcpsrv/d2_client_mgr.h>
+#include <exceptions/exceptions.h>
+
+#include <gtest/gtest.h>
+
+#include <functional>
+#include <sys/select.h>
+
+using namespace std;
+using namespace isc::dhcp;
+using namespace isc;
+namespace ph = std::placeholders;
+
+namespace {
+
+/// @brief Test fixture for exercising D2ClientMgr send management
+/// services. It inherits from D2ClientMgr to allow overriding various
+/// methods and accessing otherwise restricted member. In particular it
+/// overrides the NameChangeSender completion completion callback, allowing
+/// the injection of send errors.
+class D2ClientMgrTest : public D2ClientMgr, public ::testing::Test {
+public:
+ /// @brief If true simulates a send which completed with a failed status.
+ bool simulate_send_failure_;
+ /// @brief If true causes an exception throw in the client error handler.
+ bool error_handler_throw_;
+ /// @brief Tracks the number times the completion handler is called.
+ int callback_count_;
+ /// @brief Tracks the number of times the client error handler was called.
+ int error_handler_count_;
+
+ /// @brief Constructor
+ D2ClientMgrTest() : simulate_send_failure_(false),
+ error_handler_throw_(false),
+ callback_count_(0), error_handler_count_(0) {
+ }
+
+ /// @brief virtual Destructor
+ virtual ~D2ClientMgrTest(){
+ }
+
+ /// @brief Updates the D2ClientMgr's configuration to DDNS enabled.
+ ///
+ /// @param server_address IP address of kea-dhcp-ddns.
+ /// @param server_port IP port number of kea-dhcp-ddns.
+ /// @param protocol NCR protocol to use. (Currently only UDP is
+ /// supported).
+ void enableDdns(const std::string& server_address,
+ const size_t server_port,
+ const dhcp_ddns::NameChangeProtocol protocol) {
+ // Update the configuration with one that is enabled.
+ D2ClientConfigPtr new_cfg;
+
+ isc::asiolink::IOAddress server_ip(server_address);
+ isc::asiolink::IOAddress sender_ip(server_ip.isV4() ?
+ D2ClientConfig::DFT_V4_SENDER_IP :
+ D2ClientConfig::DFT_V6_SENDER_IP);
+
+ ASSERT_NO_THROW(new_cfg.reset(new D2ClientConfig(true,
+ server_ip, server_port,
+ sender_ip, D2ClientConfig::DFT_SENDER_PORT,
+ D2ClientConfig::DFT_MAX_QUEUE_SIZE,
+ protocol, dhcp_ddns::FMT_JSON)));
+
+ ASSERT_NO_THROW(setD2ClientConfig(new_cfg));
+ ASSERT_TRUE(ddnsEnabled());
+ }
+
+ /// @brief Checks sender's select-fd against an expected state of readiness.
+ ///
+ /// Uses select() to determine if the sender's select_fd is marked as
+ /// ready to read, and compares this against the expected state. The
+ /// select function is called with a timeout of 0.0 (non blocking).
+ ///
+ /// @param expect_ready Expected state of readiness (True if expecting
+ /// a ready to ready result, false if expecting otherwise).
+ void selectCheck(bool expect_ready) {
+ fd_set read_fds;
+ int maxfd = 0;
+
+ FD_ZERO(&read_fds);
+
+ // cppcheck-suppress redundantAssignment
+ int select_fd = -1;
+ ASSERT_NO_THROW(
+ // cppcheck-suppress redundantAssignment
+ select_fd = getSelectFd()
+ );
+
+ FD_SET(select_fd, &read_fds);
+ maxfd = select_fd;
+
+ struct timeval select_timeout;
+ select_timeout.tv_sec = 0;
+ select_timeout.tv_usec = 0;
+
+ int result = (select(maxfd + 1, &read_fds, NULL, NULL,
+ &select_timeout));
+
+ if (result < 0) {
+ const char *errstr = strerror(errno);
+ FAIL() << "select failed :" << errstr;
+ }
+
+ if (expect_ready) {
+ ASSERT_TRUE(result > 0);
+ } else {
+ ASSERT_TRUE(result == 0);
+ }
+ }
+
+ /// @brief Overrides base class completion callback.
+ ///
+ /// This method will be invoked each time a send completes. It allows
+ /// intervention prior to calling the production implementation in the
+ /// base. If simulate_send_failure_ is true, the base call impl will
+ /// be called with an error status, otherwise it will be called with
+ /// the result parameter given.
+ ///
+ /// @param result Result code of the send operation.
+ /// @param ncr NameChangeRequest which failed to send.
+ virtual void operator()(const dhcp_ddns::NameChangeSender::Result result,
+ dhcp_ddns::NameChangeRequestPtr& ncr) {
+ ++callback_count_;
+ if (simulate_send_failure_) {
+ simulate_send_failure_ = false;
+ D2ClientMgr::operator()(dhcp_ddns::NameChangeSender::ERROR, ncr);
+ } else {
+ D2ClientMgr::operator()(result, ncr);
+ }
+ }
+
+ /// @brief Serves as the "application level" client error handler.
+ ///
+ /// This method is passed into calls to startSender as the client error
+ /// handler. It should be invoked whenever the completion callback is
+ /// passed a result other than SUCCESS. If error_handler_throw_
+ /// is true it will throw an exception.
+ ///
+ /// @param result unused - Result code of the send operation.
+ /// @param ncr unused -NameChangeRequest which failed to send.
+ void error_handler(const dhcp_ddns::NameChangeSender::Result /*result*/,
+ dhcp_ddns::NameChangeRequestPtr& /*ncr*/) {
+ if (error_handler_throw_) {
+ error_handler_throw_ = false;
+ isc_throw(isc::InvalidOperation, "Simulated client handler throw");
+ }
+
+ ++error_handler_count_;
+ }
+
+ /// @brief Returns D2ClientErroHandler bound to this::error_handler_.
+ D2ClientErrorHandler getErrorHandler() {
+ return (std::bind(&D2ClientMgrTest::error_handler, this, ph::_1, ph::_2));
+ }
+
+ /// @brief Constructs a NameChangeRequest message from a fixed JSON string.
+ dhcp_ddns::NameChangeRequestPtr buildTestNcr() {
+ // Build an NCR from json string.
+ const char* ncr_str =
+ "{"
+ " \"change-type\" : 0 , "
+ " \"forward-change\" : true , "
+ " \"reverse-change\" : false , "
+ " \"fqdn\" : \"myhost.example.com.\" , "
+ " \"ip-address\" : \"192.168.2.1\" , "
+ " \"dhcid\" : \"010203040A7F8E3D\" , "
+ " \"lease-expires-on\" : \"20140121132405\" , "
+ " \"lease-length\" : 1300, "
+ " \"use-conflict-resolution\" : true "
+ "}";
+
+ return (dhcp_ddns::NameChangeRequest::fromJSON(ncr_str));
+ }
+
+ /// Expose restricted members.
+ using D2ClientMgr::getSelectFd;
+};
+
+
+/// @brief Checks that D2ClientMgr disable and enable a UDP sender.
+TEST_F(D2ClientMgrTest, udpSenderEnableDisable) {
+ // Verify DDNS is disabled by default.
+ ASSERT_FALSE(ddnsEnabled());
+
+ // Verify we are not in send mode.
+ ASSERT_FALSE(amSending());
+
+ // Enable DDNS with server at 127.0.0.1/prot 53001 via UDP.
+ enableDdns("127.0.0.1", 530001, dhcp_ddns::NCR_UDP);
+ ASSERT_FALSE(amSending());
+
+ ASSERT_NO_THROW(startSender(getErrorHandler()));
+ ASSERT_TRUE(amSending());
+
+ // Verify that we take sender out of send mode.
+ ASSERT_NO_THROW(stopSender());
+ ASSERT_FALSE(amSending());
+}
+
+/// @brief Checks D2ClientMgr queuing methods with a UDP sender.
+TEST_F(D2ClientMgrTest, udpSenderQueing) {
+ // Enable DDNS with server at 127.0.0.1/prot 53001 via UDP.
+ enableDdns("127.0.0.1", 530001, dhcp_ddns::NCR_UDP);
+ ASSERT_FALSE(amSending());
+
+ // Queue should be empty.
+ EXPECT_EQ(0, getQueueSize());
+
+ // Trying to peek past the end of the queue should throw.
+ EXPECT_THROW(peekAt(1), dhcp_ddns::NcrSenderError);
+
+ // Trying to send a NCR when not in send mode should fail.
+ dhcp_ddns::NameChangeRequestPtr ncr = buildTestNcr();
+ EXPECT_THROW(sendRequest(ncr), D2ClientError);
+
+ // Place sender in send mode.
+ ASSERT_NO_THROW(startSender(getErrorHandler()));
+ ASSERT_TRUE(amSending());
+
+ // Send should succeed now.
+ ASSERT_NO_THROW(sendRequest(ncr));
+
+ // Queue should have 1 entry.
+ EXPECT_EQ(1, getQueueSize());
+
+ // Attempt to fetch the entry we just queued.
+ dhcp_ddns::NameChangeRequestPtr ncr2;
+ ASSERT_NO_THROW(ncr2 = peekAt(0));
+
+ // Verify what we queued matches what we fetched.
+ EXPECT_TRUE(*ncr == *ncr2);
+
+ // Clearing the queue while in send mode should fail.
+ ASSERT_THROW(clearQueue(), dhcp_ddns::NcrSenderError);
+
+ // We should still have 1 in the queue.
+ EXPECT_EQ(1, getQueueSize());
+
+ // Get out of send mode.
+ ASSERT_NO_THROW(stopSender());
+ ASSERT_FALSE(amSending());
+
+ // Clear queue should succeed now.
+ ASSERT_NO_THROW(clearQueue());
+ EXPECT_EQ(0, getQueueSize());
+}
+
+/// @brief Checks that D2ClientMgr can send with a UDP sender and
+/// a private IOService.
+TEST_F(D2ClientMgrTest, udpSend) {
+ // Enable DDNS with server at 127.0.0.1/prot 53001 via UDP.
+ enableDdns("127.0.0.1", 530001, dhcp_ddns::NCR_UDP);
+
+ // Trying to fetch the select-fd when not sending should fail.
+ ASSERT_THROW(getSelectFd(), D2ClientError);
+
+ // Place sender in send mode.
+ ASSERT_NO_THROW(startSender(getErrorHandler()));
+
+ // select_fd should evaluate to NOT ready to read.
+ selectCheck(false);
+
+ // Build a test request and send it.
+ dhcp_ddns::NameChangeRequestPtr ncr = buildTestNcr();
+ ASSERT_NO_THROW(sendRequest(ncr));
+
+ // select_fd should evaluate to ready to read.
+ selectCheck(true);
+
+ // Call service handler.
+ runReadyIO();
+
+ // select_fd should evaluate to not ready to read.
+ selectCheck(false);
+}
+
+/// @brief Checks that D2ClientMgr can send with a UDP sender and
+/// an external IOService.
+TEST_F(D2ClientMgrTest, udpSendExternalIOService) {
+ // Enable DDNS with server at 127.0.0.1/prot 53001 via UDP.
+ enableDdns("127.0.0.1", 53001, dhcp_ddns::NCR_UDP);
+
+ // Place sender in send mode using an external IO service.
+ asiolink::IOService io_service;
+ ASSERT_NO_THROW(startSender(getErrorHandler(), io_service));
+
+ // select_fd should evaluate to NOT ready to read.
+ selectCheck(false);
+
+ // Build a test request and send it.
+ dhcp_ddns::NameChangeRequestPtr ncr = buildTestNcr();
+ ASSERT_NO_THROW(sendRequest(ncr));
+
+ // select_fd should evaluate to ready to read.
+ selectCheck(true);
+
+ // Call service handler.
+ runReadyIO();
+
+ // select_fd should evaluate to not ready to read.
+ selectCheck(false);
+
+ // Explicitly stop the sender. This ensures the sender's
+ // ASIO socket is closed prior to the local io_service
+ // instance goes out of scope.
+ ASSERT_NO_THROW(stopSender());
+}
+
+/// @brief Checks that D2ClientMgr can send with a UDP sender and
+/// an external IOService.
+TEST_F(D2ClientMgrTest, udpSendExternalIOService6) {
+ // Enable DDNS with server at 127.0.0.1/prot 53001 via UDP.
+ enableDdns("::1", 53001, dhcp_ddns::NCR_UDP);
+
+ // Place sender in send mode using an external IO service.
+ asiolink::IOService io_service;
+ ASSERT_NO_THROW(startSender(getErrorHandler(), io_service));
+
+ // select_fd should evaluate to NOT ready to read.
+ selectCheck(false);
+
+ // Build a test request and send it.
+ dhcp_ddns::NameChangeRequestPtr ncr = buildTestNcr();
+ ASSERT_NO_THROW(sendRequest(ncr));
+
+ // select_fd should evaluate to ready to read.
+ selectCheck(true);
+
+ // Call service handler.
+ runReadyIO();
+
+ // select_fd should evaluate to not ready to read.
+ selectCheck(false);
+
+ // Explicitly stop the sender. This ensures the sender's
+ // ASIO socket is closed prior to the local io_service
+ // instance goes out of scope.
+ ASSERT_NO_THROW(stopSender());
+}
+
+
+/// @brief Checks that D2ClientMgr invokes the client error handler
+/// when send errors occur.
+TEST_F(D2ClientMgrTest, udpSendErrorHandler) {
+ // Enable DDNS with server at 127.0.0.1/prot 53001 via UDP.
+ // Place sender in send mode.
+ enableDdns("127.0.0.1", 530001, dhcp_ddns::NCR_UDP);
+ ASSERT_NO_THROW(startSender(getErrorHandler()));
+
+ // Simulate a failed response in the send call back. This should
+ // cause the error handler to get invoked.
+ simulate_send_failure_ = true;
+
+ // Verify error count is zero.
+ ASSERT_EQ(0, error_handler_count_);
+
+ // Send a test request.
+ dhcp_ddns::NameChangeRequestPtr ncr = buildTestNcr();
+ ASSERT_NO_THROW(sendRequest(ncr));
+
+ // Call the ready handler. This should complete the message with an error.
+ ASSERT_NO_THROW(runReadyIO());
+
+ // If we executed error handler properly, the error count should one.
+ ASSERT_EQ(1, error_handler_count_);
+}
+
+
+/// @brief Checks that client error handler exceptions are handled gracefully.
+TEST_F(D2ClientMgrTest, udpSendErrorHandlerThrow) {
+ // Enable DDNS with server at 127.0.0.1/prot 53001 via UDP.
+ // Place sender in send mode.
+ enableDdns("127.0.0.1", 530001, dhcp_ddns::NCR_UDP);
+ ASSERT_NO_THROW(startSender(getErrorHandler()));
+
+ // Simulate a failed response in the send call back and
+ // force a throw in the error handler.
+ simulate_send_failure_ = true;
+ error_handler_throw_ = true;
+
+ // Verify error count is zero.
+ ASSERT_EQ(0, error_handler_count_);
+
+ // Send a test request.
+ dhcp_ddns::NameChangeRequestPtr ncr = buildTestNcr();
+ ASSERT_NO_THROW(sendRequest(ncr));
+
+ // Call the ready handler. This should complete the message with an error.
+ // The handler should throw but the exception should not escape.
+ ASSERT_NO_THROW(runReadyIO());
+
+ // If throw flag is false, then we were in the error handler should
+ // have thrown.
+ ASSERT_FALSE(error_handler_throw_);
+
+ // If error count is still zero, then we did throw.
+ ASSERT_EQ(0, error_handler_count_);
+}
+
+/// @brief Tests that D2ClientMgr registers and unregisters with IfaceMgr.
+TEST_F(D2ClientMgrTest, ifaceRegister) {
+ // Enable DDNS with server at 127.0.0.1/prot 53001 via UDP.
+ enableDdns("127.0.0.1", 530001, dhcp_ddns::NCR_UDP);
+
+ // Place sender in send mode.
+ ASSERT_NO_THROW(startSender(getErrorHandler()));
+
+ // Queue three messages.
+ for (unsigned i = 0; i < 3; ++i) {
+ dhcp_ddns::NameChangeRequestPtr ncr = buildTestNcr();
+ ASSERT_NO_THROW(sendRequest(ncr));
+ }
+
+ // Make sure queue count is correct.
+ EXPECT_EQ(3, getQueueSize());
+
+ // select_fd should evaluate to ready to read.
+ selectCheck(true);
+
+ // Calling receive should complete the first message and start the second.
+ IfaceMgr::instance().receive4(0, 0);
+
+ // Verify the callback handler was invoked, no errors counted.
+ EXPECT_EQ(2, getQueueSize());
+ ASSERT_EQ(1, callback_count_);
+ ASSERT_EQ(0, error_handler_count_);
+
+ // Stop the sender. This should complete the second message but leave
+ // the third in the queue.
+ ASSERT_NO_THROW(stopSender());
+ EXPECT_EQ(1, getQueueSize());
+ ASSERT_EQ(2, callback_count_);
+ ASSERT_EQ(0, error_handler_count_);
+
+ // Calling receive again should have no affect.
+ IfaceMgr::instance().receive4(0, 0);
+ EXPECT_EQ(1, getQueueSize());
+ ASSERT_EQ(2, callback_count_);
+ ASSERT_EQ(0, error_handler_count_);
+}
+
+/// @brief Checks that D2ClientMgr suspendUpdates works properly.
+TEST_F(D2ClientMgrTest, udpSuspendUpdates) {
+ // Enable DDNS with server at 127.0.0.1/prot 53001 via UDP.
+ // Place sender in send mode.
+ enableDdns("127.0.0.1", 530001, dhcp_ddns::NCR_UDP);
+ ASSERT_NO_THROW(startSender(getErrorHandler()));
+
+ // Send a test request.
+ for (unsigned i = 0; i < 3; ++i) {
+ dhcp_ddns::NameChangeRequestPtr ncr = buildTestNcr();
+ ASSERT_NO_THROW(sendRequest(ncr));
+ }
+ ASSERT_EQ(3, getQueueSize());
+
+ // Call the ready handler. This should complete the first message
+ // and initiate sending the second message.
+ ASSERT_NO_THROW(runReadyIO());
+
+ // Queue count should have gone down by 1.
+ ASSERT_EQ(2, getQueueSize());
+
+ // Suspend updates. This should disable updates and stop the sender.
+ ASSERT_NO_THROW(suspendUpdates());
+
+ EXPECT_FALSE(ddnsEnabled());
+ EXPECT_FALSE(amSending());
+
+ // Stopping the sender should have completed the second message's
+ // in-progress send, so queue size should be 1.
+ ASSERT_EQ(1, getQueueSize());
+}
+
+/// @brief Tests that invokeErrorHandler does not fail if there is no handler.
+TEST_F(D2ClientMgrTest, missingErrorHandler) {
+ // Ensure we aren't in send mode.
+ ASSERT_FALSE(ddnsEnabled());
+ ASSERT_FALSE(amSending());
+
+ // There is no error handler at this point, so invoking should not throw.
+ dhcp_ddns::NameChangeRequestPtr ncr;
+ ASSERT_NO_THROW(invokeClientErrorHandler(dhcp_ddns::NameChangeSender::ERROR,
+ ncr));
+
+ // Verify we didn't invoke the error handler, error count is zero.
+ ASSERT_EQ(0, error_handler_count_);
+}
+
+} // end of anonymous namespace