diff options
Diffstat (limited to 'src/lib/dhcpsrv/tests/d2_client_unittest.cc')
-rw-r--r-- | src/lib/dhcpsrv/tests/d2_client_unittest.cc | 1240 |
1 files changed, 1240 insertions, 0 deletions
diff --git a/src/lib/dhcpsrv/tests/d2_client_unittest.cc b/src/lib/dhcpsrv/tests/d2_client_unittest.cc new file mode 100644 index 0000000..c15afc5 --- /dev/null +++ b/src/lib/dhcpsrv/tests/d2_client_unittest.cc @@ -0,0 +1,1240 @@ +// Copyright (C) 2012-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/. + +#include <config.h> +#include <dhcp/option4_client_fqdn.h> +#include <dhcp/option6_client_fqdn.h> +#include <dhcpsrv/d2_client_mgr.h> +#include <testutils/test_to_element.h> +#include <exceptions/exceptions.h> +#include <util/strutil.h> + +#include <boost/algorithm/string.hpp> +#include <gtest/gtest.h> + +using namespace std; +using namespace isc::asiolink; +using namespace isc::dhcp; +using namespace isc::util; +using namespace isc::test; +using namespace isc::data; +using namespace isc; + +namespace { + +/// @brief Tests conversion of NameChangeFormat between enum and strings. +TEST(ReplaceClientNameModeTest, formatEnumConversion){ + ASSERT_EQ(D2ClientConfig::stringToReplaceClientNameMode("never"), + D2ClientConfig::RCM_NEVER); + ASSERT_EQ(D2ClientConfig::stringToReplaceClientNameMode("always"), + D2ClientConfig::RCM_ALWAYS); + ASSERT_EQ(D2ClientConfig::stringToReplaceClientNameMode("when-present"), + D2ClientConfig::RCM_WHEN_PRESENT); + ASSERT_EQ(D2ClientConfig::stringToReplaceClientNameMode("when-not-present"), + D2ClientConfig::RCM_WHEN_NOT_PRESENT); + ASSERT_THROW(D2ClientConfig::stringToReplaceClientNameMode("BOGUS"), + isc::BadValue); + + ASSERT_EQ(D2ClientConfig:: + replaceClientNameModeToString(D2ClientConfig::RCM_NEVER), + "never"); + ASSERT_EQ(D2ClientConfig:: + replaceClientNameModeToString(D2ClientConfig::RCM_ALWAYS), + "always"); + ASSERT_EQ(D2ClientConfig:: + replaceClientNameModeToString(D2ClientConfig::RCM_WHEN_PRESENT), + "when-present"); + ASSERT_EQ(D2ClientConfig:: + replaceClientNameModeToString(D2ClientConfig:: + RCM_WHEN_NOT_PRESENT), + "when-not-present"); +} + +/// @brief Checks constructors and accessors of D2ClientConfig. +TEST(D2ClientConfigTest, constructorsAndAccessors) { + D2ClientConfigPtr d2_client_config; + + // Verify default constructor creates a disabled instance. + ASSERT_NO_THROW(d2_client_config.reset(new D2ClientConfig())); + EXPECT_FALSE(d2_client_config->getEnableUpdates()); + + // Verify the enable-updates can be toggled. + d2_client_config->enableUpdates(true); + EXPECT_TRUE(d2_client_config->getEnableUpdates()); + d2_client_config->enableUpdates(false); + EXPECT_FALSE(d2_client_config->getEnableUpdates()); + + d2_client_config.reset(); + + bool enable_updates = true; + isc::asiolink::IOAddress server_ip("127.0.0.1"); + size_t server_port = 477; + isc::asiolink::IOAddress sender_ip("127.0.0.1"); + size_t sender_port = 478; + size_t max_queue_size = 2048; + dhcp_ddns::NameChangeProtocol ncr_protocol = dhcp_ddns::NCR_UDP; + dhcp_ddns::NameChangeFormat ncr_format = dhcp_ddns::FMT_JSON; + std::string generated_prefix = "the_prefix"; + std::string qualifying_suffix = "the.suffix."; + std::string hostname_char_set = "[^A-Z]"; + std::string hostname_char_replacement = "*"; + + // Verify that we can construct a valid, enabled instance. + ASSERT_NO_THROW(d2_client_config.reset(new + D2ClientConfig(enable_updates, + server_ip, + server_port, + sender_ip, + sender_port, + max_queue_size, + ncr_protocol, + ncr_format))); + ASSERT_TRUE(d2_client_config); + + // Add user context + std::string user_context = "{ \"comment\": \"bar\", \"foo\": 1 }"; + EXPECT_FALSE(d2_client_config->getContext()); + d2_client_config->setContext(Element::fromJSON(user_context)); + + // Verify that the accessors return the expected values. + EXPECT_EQ(d2_client_config->getEnableUpdates(), enable_updates); + + EXPECT_EQ(d2_client_config->getServerIp(), server_ip); + EXPECT_EQ(d2_client_config->getServerPort(), server_port); + EXPECT_EQ(d2_client_config->getSenderIp(), sender_ip); + EXPECT_EQ(d2_client_config->getSenderPort(), sender_port); + EXPECT_EQ(d2_client_config->getMaxQueueSize(), max_queue_size); + EXPECT_EQ(d2_client_config->getNcrProtocol(), ncr_protocol); + EXPECT_EQ(d2_client_config->getNcrFormat(), ncr_format); + ASSERT_TRUE(d2_client_config->getContext()); + EXPECT_EQ(d2_client_config->getContext()->str(), user_context); + + // Verify that toText called by << operator doesn't bomb. + ASSERT_NO_THROW(std::cout << "toText test:" << std::endl << + *d2_client_config << std::endl); + + // Verify what toElement returns. + std::string expected = "{\n" + "\"enable-updates\": true,\n" + "\"server-ip\": \"127.0.0.1\",\n" + "\"server-port\": 477,\n" + "\"sender-ip\": \"127.0.0.1\",\n" + "\"sender-port\": 478,\n" + "\"max-queue-size\": 2048,\n" + "\"ncr-protocol\": \"UDP\",\n" + "\"ncr-format\": \"JSON\",\n" + "\"user-context\": { \"foo\": 1, \"comment\": \"bar\" }\n" + "}\n"; + runToElementTest<D2ClientConfig>(expected, *d2_client_config); + + // Verify that constructor does not allow use of NCR_TCP. + /// @todo obviously this becomes invalid once TCP is supported. + ASSERT_THROW(d2_client_config.reset(new + D2ClientConfig(enable_updates, + server_ip, + server_port, + sender_ip, + sender_port, + max_queue_size, + dhcp_ddns::NCR_TCP, + ncr_format)), + D2ClientError); + + Optional<std::string> opt_hostname_char_set("", true); + Optional<std::string> opt_hostname_char_replacement("", true); + + // Verify that constructor handles optional hostname char stuff. + ASSERT_NO_THROW(d2_client_config.reset(new + D2ClientConfig(enable_updates, + server_ip, + server_port, + sender_ip, + sender_port, + max_queue_size, + ncr_protocol, + ncr_format))); + ASSERT_TRUE(d2_client_config); + + // Verify what toElement returns. + expected = "{\n" + "\"enable-updates\": true,\n" + "\"server-ip\": \"127.0.0.1\",\n" + "\"server-port\": 477,\n" + "\"sender-ip\": \"127.0.0.1\",\n" + "\"sender-port\": 478,\n" + "\"max-queue-size\": 2048,\n" + "\"ncr-protocol\": \"UDP\",\n" + "\"ncr-format\": \"JSON\"\n" + "}\n"; + runToElementTest<D2ClientConfig>(expected, *d2_client_config); + + /// @todo if additional validation is added to ctor, this test needs to + /// expand accordingly. +} + +/// @brief Tests the equality and inequality operators of D2ClientConfig. +TEST(D2ClientConfigTest, equalityOperator) { + D2ClientConfigPtr ref_config; + D2ClientConfigPtr test_config; + + isc::asiolink::IOAddress ref_address("127.0.0.1"); + isc::asiolink::IOAddress test_address("127.0.0.2"); + + // Create an instance to use as a reference. + ASSERT_NO_THROW(ref_config.reset(new D2ClientConfig(true, + ref_address, 477, ref_address, 478, 1024, + dhcp_ddns::NCR_UDP, dhcp_ddns::FMT_JSON))); + ASSERT_TRUE(ref_config); + + // Check a configuration that is identical to reference configuration. + ASSERT_NO_THROW(test_config.reset(new D2ClientConfig(true, + ref_address, 477, ref_address, 478, 1024, + dhcp_ddns::NCR_UDP, dhcp_ddns::FMT_JSON))); + ASSERT_TRUE(test_config); + EXPECT_TRUE(*ref_config == *test_config); + EXPECT_FALSE(*ref_config != *test_config); + + // Check a configuration that differs only by enable flag. + ASSERT_NO_THROW(test_config.reset(new D2ClientConfig(false, + ref_address, 477, ref_address, 478, 1024, + dhcp_ddns::NCR_UDP, dhcp_ddns::FMT_JSON))); + ASSERT_TRUE(test_config); + EXPECT_FALSE(*ref_config == *test_config); + EXPECT_TRUE(*ref_config != *test_config); + + // Check a configuration that differs only by server ip. + ASSERT_NO_THROW(test_config.reset(new D2ClientConfig(true, + test_address, 477, ref_address, 478, 1024, + dhcp_ddns::NCR_UDP, dhcp_ddns::FMT_JSON))); + ASSERT_TRUE(test_config); + EXPECT_FALSE(*ref_config == *test_config); + EXPECT_TRUE(*ref_config != *test_config); + + // Check a configuration that differs only by server port. + ASSERT_NO_THROW(test_config.reset(new D2ClientConfig(true, + ref_address, 333, ref_address, 478, 1024, + dhcp_ddns::NCR_UDP, dhcp_ddns::FMT_JSON))); + ASSERT_TRUE(test_config); + EXPECT_FALSE(*ref_config == *test_config); + EXPECT_TRUE(*ref_config != *test_config); + + // Check a configuration that differs only by sender ip. + ASSERT_NO_THROW(test_config.reset(new D2ClientConfig(true, + ref_address, 477, test_address, 478, 1024, + dhcp_ddns::NCR_UDP, dhcp_ddns::FMT_JSON))); + ASSERT_TRUE(test_config); + EXPECT_FALSE(*ref_config == *test_config); + EXPECT_TRUE(*ref_config != *test_config); + + // Check a configuration that differs only by sender port. + ASSERT_NO_THROW(test_config.reset(new D2ClientConfig(true, + ref_address, 477, ref_address, 333, 1024, + dhcp_ddns::NCR_UDP, dhcp_ddns::FMT_JSON))); + ASSERT_TRUE(test_config); + EXPECT_FALSE(*ref_config == *test_config); + EXPECT_TRUE(*ref_config != *test_config); + + // Check a configuration that differs only by max queue size. + ASSERT_NO_THROW(test_config.reset(new D2ClientConfig(true, + ref_address, 477, ref_address, 478, 2048, + dhcp_ddns::NCR_UDP, dhcp_ddns::FMT_JSON))); + ASSERT_TRUE(test_config); + EXPECT_FALSE(*ref_config == *test_config); + EXPECT_TRUE(*ref_config != *test_config); +} + +/// @brief Checks the D2ClientMgr constructor. +TEST(D2ClientMgr, constructor) { + D2ClientMgrPtr d2_client_mgr; + + // Verify we can construct with the default constructor. + ASSERT_NO_THROW(d2_client_mgr.reset(new D2ClientMgr())); + + // After construction, D2 configuration should be disabled. + // Fetch it and verify this is the case. + D2ClientConfigPtr original_config = d2_client_mgr->getD2ClientConfig(); + ASSERT_TRUE(original_config); + EXPECT_FALSE(original_config->getEnableUpdates()); + + // Make sure convenience method agrees. + EXPECT_FALSE(d2_client_mgr->ddnsEnabled()); +} + +/// @brief Checks passing the D2ClientMgr a valid D2 client configuration. +/// @todo Once NameChangeSender is integrated, this test needs to expand, and +/// additional scenario tests will need to be written. +TEST(D2ClientMgr, validConfig) { + D2ClientMgrPtr d2_client_mgr; + + // Construct the manager and fetch its initial configuration. + ASSERT_NO_THROW(d2_client_mgr.reset(new D2ClientMgr())); + D2ClientConfigPtr original_config = d2_client_mgr->getD2ClientConfig(); + ASSERT_TRUE(original_config); + + // Verify that we cannot set the config to an empty pointer. + D2ClientConfigPtr new_cfg; + ASSERT_THROW(d2_client_mgr->setD2ClientConfig(new_cfg), D2ClientError); + + // Create a new, enabled config. + ASSERT_NO_THROW(new_cfg.reset(new D2ClientConfig(true, + isc::asiolink::IOAddress("127.0.0.1"), 477, + isc::asiolink::IOAddress("127.0.0.1"), 478, + 1024, + dhcp_ddns::NCR_UDP, dhcp_ddns::FMT_JSON))); + + // Verify that we can assign a new, non-empty configuration. + ASSERT_NO_THROW(d2_client_mgr->setD2ClientConfig(new_cfg)); + + // Verify that we can fetch the newly assigned configuration. + D2ClientConfigPtr updated_config = d2_client_mgr->getD2ClientConfig(); + ASSERT_TRUE(updated_config); + EXPECT_TRUE(updated_config->getEnableUpdates()); + + // Make sure convenience method agrees with the updated configuration. + EXPECT_TRUE(d2_client_mgr->ddnsEnabled()); + + // Make sure the configuration we fetched is the one we assigned, + // and not the original configuration. + EXPECT_EQ(*new_cfg, *updated_config); + EXPECT_NE(*original_config, *updated_config); +} + +/// @brief Checks passing the D2ClientMgr a valid D2 client configuration +/// using IPv6 service. +TEST(D2ClientMgr, ipv6Config) { + D2ClientMgrPtr d2_client_mgr; + + // Construct the manager and fetch its initial configuration. + ASSERT_NO_THROW(d2_client_mgr.reset(new D2ClientMgr())); + D2ClientConfigPtr original_config = d2_client_mgr->getD2ClientConfig(); + ASSERT_TRUE(original_config); + + // Create a new, enabled config. + D2ClientConfigPtr new_cfg; + ASSERT_NO_THROW(new_cfg.reset(new D2ClientConfig(true, + isc::asiolink::IOAddress("::1"), 477, + isc::asiolink::IOAddress("::1"), 478, + 1024, + dhcp_ddns::NCR_UDP, dhcp_ddns::FMT_JSON))); + + // Verify that we can assign a new, non-empty configuration. + ASSERT_NO_THROW(d2_client_mgr->setD2ClientConfig(new_cfg)); + + // Verify that we can fetch the newly assigned configuration. + D2ClientConfigPtr updated_config = d2_client_mgr->getD2ClientConfig(); + ASSERT_TRUE(updated_config); + EXPECT_TRUE(updated_config->getEnableUpdates()); + + // Make sure convenience method agrees with the updated configuration. + EXPECT_TRUE(d2_client_mgr->ddnsEnabled()); + + // Make sure the configuration we fetched is the one we assigned, + // and not the original configuration. + EXPECT_EQ(*new_cfg, *updated_config); + EXPECT_NE(*original_config, *updated_config); +} + +/// @brief Test class for execerising manager functions that are +/// influenced by DDNS parameters. +class D2ClientMgrParamsTest : public ::testing::Test { +public: + /// @brief Constructor + D2ClientMgrParamsTest() = default; + + /// @brief Destructor + virtual ~D2ClientMgrParamsTest() = default; + +private: + /// @brief Prepares the class for a test. + virtual void SetUp() { + // Create a subnet and then a DdnsParams instance. + // We'll use the subnet's setters to alter DDNS parameter values. + subnet_.reset(new Subnet4(IOAddress("192.0.2.2"), 16, 1, 2, 3, 10)); + ddns_params_.reset(new DdnsParams(subnet_, true)); + } + + /// @brief Cleans up after the test. + virtual void TearDown() {}; + +public: + /// @brief Acts as the "selected" subnet. It is passed into the + /// constructor of ddns_params_. This allows DDNS parameters to + /// be modified via setters on subnet_. + Subnet4Ptr subnet_; + /// @brief Parameter instance based into D2ClientMgr functions + DdnsParamsPtr ddns_params_; +}; + +/// @brief Tests that analyzeFqdn detects invalid combination of both the +/// client S and N flags set to true. +TEST(D2ClientMgr, analyzeFqdnInvalidCombination) { + D2ClientMgr mgr; + bool server_s = false; + bool server_n = false; + + DdnsParams ddns_params; + + // client S=1 N=1 is invalid. analyzeFqdn should throw. + ASSERT_THROW(mgr.analyzeFqdn(true, true, server_s, server_n, ddns_params), + isc::BadValue); +} + +/// @brief Tests that analyzeFqdn generates correct server S and N flags when +/// updates are enabled and all overrides are off. +TEST_F(D2ClientMgrParamsTest, analyzeFqdnEnabledNoOverrides) { + D2ClientMgr mgr; + bool server_s = false; + bool server_n = false; + + // Create enabled configuration with all controls off (no overrides). + subnet_->setDdnsSendUpdates(true); + subnet_->setDdnsOverrideNoUpdate(false); + subnet_->setDdnsOverrideClientUpdate(false); + subnet_->setDdnsReplaceClientNameMode(D2ClientConfig::RCM_NEVER); + subnet_->setDdnsGeneratedPrefix(""); + subnet_->setDdnsQualifyingSuffix(""); + subnet_->setHostnameCharSet(""); + subnet_->setHostnameCharReplacement(""); + + // client S=0 N=0 means client wants to do forward update. + // server S should be 0 (server is not doing forward updates) + // and server N should be 0 (server doing reverse updates) + mgr.analyzeFqdn(false, false, server_s, server_n, *ddns_params_); + EXPECT_FALSE(server_s); + EXPECT_FALSE(server_n); + + // client S=1 N=0 means client wants server to do forward update. + // server S should be 1 (server is doing forward updates) + // and server N should be 0 (server doing updates) + mgr.analyzeFqdn(true, false, server_s, server_n, *ddns_params_); + EXPECT_TRUE(server_s); + EXPECT_FALSE(server_n); + + + // client S=0 N=1 means client wants no one to do forward updates. + // server S should be 0 (server is not forward updates) + // and server N should be 1 (server is not doing any updates) + mgr.analyzeFqdn(false, true, server_s, server_n, *ddns_params_); + EXPECT_FALSE(server_s); + EXPECT_TRUE(server_n); +} + +/// @brief Tests that analyzeFqdn generates correct server S and N flags when +/// updates are enabled and override-no-update is on. +TEST_F(D2ClientMgrParamsTest, analyzeFqdnEnabledOverrideNoUpdate) { + D2ClientMgr mgr; + bool server_s = false; + bool server_n = false; + + // Create enabled configuration with override-no-update true. + subnet_->setDdnsSendUpdates(true); + subnet_->setDdnsOverrideNoUpdate(true); + subnet_->setDdnsOverrideClientUpdate(false); + subnet_->setDdnsReplaceClientNameMode(D2ClientConfig::RCM_NEVER); + subnet_->setDdnsGeneratedPrefix(""); + subnet_->setDdnsQualifyingSuffix(""); + subnet_->setHostnameCharSet(""); + subnet_->setHostnameCharReplacement(""); + + // client S=0 N=0 means client wants to do forward update. + // server S should be 0 (server is not doing forward updates) + // and server N should be 0 (server is doing reverse updates) + mgr.analyzeFqdn(false, false, server_s, server_n, *ddns_params_); + EXPECT_FALSE(server_s); + EXPECT_FALSE(server_n); + + // client S=1 N=0 means client wants server to do forward update. + // server S should be 1 (server is doing forward updates) + // and server N should be 0 (server doing updates) + mgr.analyzeFqdn(true, false, server_s, server_n, *ddns_params_); + EXPECT_TRUE(server_s); + EXPECT_FALSE(server_n); + + // client S=0 N=1 means client wants no one to do forward updates. + // server S should be 1 (server is doing forward updates) + // and server N should be 0 (server is doing updates) + mgr.analyzeFqdn(false, true, server_s, server_n, *ddns_params_); + EXPECT_TRUE(server_s); + EXPECT_FALSE(server_n); +} + +/// @brief Tests that analyzeFqdn generates correct server S and N flags when +/// updates are enabled and override-client-update is on. +TEST_F(D2ClientMgrParamsTest, analyzeFqdnEnabledOverrideClientUpdate) { + D2ClientMgr mgr; + bool server_s = false; + bool server_n = false; + + // Create enabled configuration with override-client-update true. + subnet_->setDdnsSendUpdates(true); + subnet_->setDdnsOverrideNoUpdate(false); + subnet_->setDdnsOverrideClientUpdate(true); + subnet_->setDdnsReplaceClientNameMode(D2ClientConfig::RCM_NEVER); + subnet_->setDdnsGeneratedPrefix(""); + subnet_->setDdnsQualifyingSuffix(""); + subnet_->setHostnameCharSet(""); + subnet_->setHostnameCharReplacement(""); + + // client S=0 N=0 means client wants to do forward update. + // server S should be 1 (server is doing forward updates) + // and server N should be 0 (server doing updates) + mgr.analyzeFqdn(false, false, server_s, server_n, *ddns_params_); + EXPECT_TRUE(server_s); + EXPECT_FALSE(server_n); + + // client S=1 N=0 means client wants server to do forward update. + // server S should be 1 (server is doing forward updates) + // and server N should be 0 (server doing updates) + mgr.analyzeFqdn(true, false, server_s, server_n, *ddns_params_); + EXPECT_TRUE(server_s); + EXPECT_FALSE(server_n); + + // client S=0 N=1 means client wants no one to do forward updates. + // server S should be 0 (server is not forward updates) + // and server N should be 1 (server is not doing any updates) + mgr.analyzeFqdn(false, true, server_s, server_n, *ddns_params_); + EXPECT_FALSE(server_s); + EXPECT_TRUE(server_n); +} + +/// @brief Verifies the adustFqdnFlags template with Option4ClientFqdn objects. +/// Ensures that the method can set the N, S, and O flags properly. +/// Other permutations are covered by analyzeFqdnFlag tests. +TEST_F(D2ClientMgrParamsTest, adjustFqdnFlagsV4) { + D2ClientMgr mgr; + Option4ClientFqdnPtr request; + Option4ClientFqdnPtr response; + + // Create enabled configuration with override-no-update true. + subnet_->setDdnsSendUpdates(true); + subnet_->setDdnsOverrideNoUpdate(true); + subnet_->setDdnsOverrideClientUpdate(false); + subnet_->setDdnsReplaceClientNameMode(D2ClientConfig::RCM_NEVER); + subnet_->setDdnsGeneratedPrefix(""); + subnet_->setDdnsQualifyingSuffix(""); + subnet_->setHostnameCharSet(""); + subnet_->setHostnameCharReplacement(""); + + // client S=0 N=0 means client wants to do forward update. + // server S should be 0 (server is not doing forward updates) + // and server N should be 0 (server is doing reverse updates) + // and server O should be 0 + request.reset(new Option4ClientFqdn(0, Option4ClientFqdn::RCODE_CLIENT(), + "", Option4ClientFqdn::PARTIAL)); + response.reset(new Option4ClientFqdn(*request)); + response->resetFlags(); + + mgr.adjustFqdnFlags<Option4ClientFqdn>(*request, *response, *ddns_params_); + EXPECT_FALSE(response->getFlag(Option4ClientFqdn::FLAG_S)); + EXPECT_FALSE(response->getFlag(Option4ClientFqdn::FLAG_N)); + EXPECT_FALSE(response->getFlag(Option4ClientFqdn::FLAG_O)); + + // client S=1 N=0 means client wants server to do forward update. + // server S should be 1 (server is doing forward updates) + // and server N should be 0 (server doing updates) + // and server O should be 0 + request.reset(new Option4ClientFqdn(Option4ClientFqdn::FLAG_S, + Option4ClientFqdn::RCODE_CLIENT(), + "", Option4ClientFqdn::PARTIAL)); + response.reset(new Option4ClientFqdn(*request)); + response->resetFlags(); + + mgr.adjustFqdnFlags<Option4ClientFqdn>(*request, *response, *ddns_params_); + EXPECT_TRUE(response->getFlag(Option4ClientFqdn::FLAG_S)); + EXPECT_FALSE(response->getFlag(Option4ClientFqdn::FLAG_N)); + EXPECT_FALSE(response->getFlag(Option4ClientFqdn::FLAG_O)); + + // client S=0 N=1 means client wants no one to do updates + // server S should be 1 (server is doing forward updates) + // and server N should be 0 (server doing updates) + // and O should be 1 (overriding client S) + request.reset(new Option4ClientFqdn(Option4ClientFqdn::FLAG_N, + Option4ClientFqdn::RCODE_CLIENT(), + "", Option4ClientFqdn::PARTIAL)); + response.reset(new Option4ClientFqdn(*request)); + response->resetFlags(); + + mgr.adjustFqdnFlags<Option4ClientFqdn>(*request, *response, *ddns_params_); + EXPECT_TRUE(response->getFlag(Option4ClientFqdn::FLAG_S)); + EXPECT_FALSE(response->getFlag(Option4ClientFqdn::FLAG_N)); + EXPECT_TRUE(response->getFlag(Option4ClientFqdn::FLAG_O)); +} + +/// @brief Verified the getUpdateDirections template method with +/// Option4ClientFqdn objects. +TEST(D2ClientMgr, updateDirectionsV4) { + D2ClientMgr mgr; + Option4ClientFqdnPtr response; + + bool do_forward = false; + bool do_reverse = false; + + // Response S=0, N=0 should mean do reverse only. + response.reset(new Option4ClientFqdn(0, + Option4ClientFqdn::RCODE_CLIENT(), + "", Option4ClientFqdn::PARTIAL)); + mgr.getUpdateDirections(*response, do_forward, do_reverse); + EXPECT_FALSE(do_forward); + EXPECT_TRUE(do_reverse); + + // Response S=0, N=1 should mean don't do either. + response.reset(new Option4ClientFqdn(Option4ClientFqdn::FLAG_N, + Option4ClientFqdn::RCODE_CLIENT(), + "", Option4ClientFqdn::PARTIAL)); + mgr.getUpdateDirections(*response, do_forward, do_reverse); + EXPECT_FALSE(do_forward); + EXPECT_FALSE(do_reverse); + + // Response S=1, N=0 should mean do both. + response.reset(new Option4ClientFqdn(Option4ClientFqdn::FLAG_S, + Option4ClientFqdn::RCODE_CLIENT(), + "", Option4ClientFqdn::PARTIAL)); + mgr.getUpdateDirections(*response, do_forward, do_reverse); + EXPECT_TRUE(do_forward); + EXPECT_TRUE(do_reverse); + + // Response S=1, N=1 isn't possible. +} + +/// @brief Tests the qualifyName method's ability to construct FQDNs +TEST_F(D2ClientMgrParamsTest, qualifyName) { + D2ClientMgr mgr; + bool do_not_dot = false; + bool do_dot = true; + + // Create enabled configuration + subnet_->setDdnsSendUpdates(true); + subnet_->setDdnsOverrideNoUpdate(false); + subnet_->setDdnsOverrideClientUpdate(false); + subnet_->setDdnsReplaceClientNameMode(D2ClientConfig::RCM_NEVER); + subnet_->setDdnsGeneratedPrefix("prefix"); + subnet_->setDdnsQualifyingSuffix("suffix.com"); + subnet_->setHostnameCharSet(""); + subnet_->setHostnameCharReplacement(""); + + // Verify that the qualifying suffix gets appended with a trailing dot added. + std::string partial_name = "somehost"; + std::string qualified_name = mgr.qualifyName(partial_name, *ddns_params_, do_dot); + EXPECT_EQ("somehost.suffix.com.", qualified_name); + + // Verify that the qualifying suffix gets appended without a trailing dot. + partial_name = "somehost"; + qualified_name = mgr.qualifyName(partial_name, *ddns_params_, do_not_dot); + EXPECT_EQ("somehost.suffix.com", qualified_name); + + // Verify that an empty suffix and false flag, does not change the name + subnet_->setDdnsQualifyingSuffix(""); + partial_name = "somehost"; + qualified_name = mgr.qualifyName(partial_name, *ddns_params_, do_not_dot); + EXPECT_EQ("somehost", qualified_name); + + // Verify that a qualifying suffix that already has a trailing + // dot gets appended without doubling the dot. + subnet_->setDdnsQualifyingSuffix("hasdot.com."); + qualified_name = mgr.qualifyName(partial_name, *ddns_params_, do_dot); + EXPECT_EQ("somehost.hasdot.com.", qualified_name); + + // Verify that the qualifying suffix gets appended without an + // extraneous dot when partial_name ends with a "." + qualified_name = mgr.qualifyName("somehost.", *ddns_params_, do_dot); + EXPECT_EQ("somehost.hasdot.com.", qualified_name); + + // Verify that a name with a trailing dot does not get an extraneous + // dot when the suffix is blank + subnet_->setDdnsQualifyingSuffix(""); + qualified_name = mgr.qualifyName("somehost.", *ddns_params_, do_dot); + EXPECT_EQ("somehost.", qualified_name); + + // Verify that a name with no trailing dot gets just a dot when the + // suffix is blank + qualified_name = mgr.qualifyName("somehost", *ddns_params_, do_dot); + EXPECT_EQ("somehost.", qualified_name); + + // Verify that a name with no trailing dot does not get dotted when the + // suffix is blank and trailing dot is false + qualified_name = mgr.qualifyName("somehost", *ddns_params_, do_not_dot); + EXPECT_EQ("somehost", qualified_name); + + // Verify that a name with trailing dot gets "undotted" when the + // suffix is blank and trailing dot is false + qualified_name = mgr.qualifyName("somehost.", *ddns_params_, do_not_dot); + EXPECT_EQ("somehost", qualified_name); + +} + +/// @brief Tests the qualifyName method's ability to avoid duplicating +/// qualifying suffix. +TEST_F(D2ClientMgrParamsTest, qualifyNameWithoutDuplicatingSuffix) { + D2ClientMgr mgr; + bool do_dot = true; + + // Create enabled configuration + subnet_->setDdnsSendUpdates(true); + subnet_->setDdnsOverrideNoUpdate(false); + subnet_->setDdnsOverrideClientUpdate(false); + subnet_->setDdnsReplaceClientNameMode(D2ClientConfig::RCM_NEVER); + subnet_->setDdnsGeneratedPrefix("prefix"); + subnet_->setDdnsQualifyingSuffix("suffix.com"); + subnet_->setHostnameCharSet(""); + subnet_->setHostnameCharReplacement(""); + + // Verify that the qualifying suffix does not get appended when the + // input name has the suffix but no trailing dot. + std::string partial_name = "somehost.suffix.com"; + std::string qualified_name = mgr.qualifyName(partial_name, *ddns_params_, do_dot); + EXPECT_EQ("somehost.suffix.com.", qualified_name); + + // Verify that the qualifying suffix does not get appended when the + // input name has the suffix and a trailing dot. + partial_name = "somehost.suffix.com."; + qualified_name = mgr.qualifyName(partial_name, *ddns_params_, do_dot); + EXPECT_EQ("somehost.suffix.com.", qualified_name); + + // Verify that the qualifying suffix does get appended when the + // input name has the suffix embedded in it but does not begin + // at a label boundary. + partial_name = "somehost.almostsuffix.com"; + qualified_name = mgr.qualifyName(partial_name, *ddns_params_, do_dot); + EXPECT_EQ("somehost.almostsuffix.com.suffix.com.", qualified_name); + + // Verify that the qualifying suffix does get appended when the + // input name has the suffix embedded in it. + partial_name = "somehost.suffix.com.org"; + qualified_name = mgr.qualifyName(partial_name, *ddns_params_, do_dot); + EXPECT_EQ("somehost.suffix.com.org.suffix.com.", qualified_name); + + // Verify that the qualifying suffix does not get appended when the + // input name is the suffix itself. + partial_name = "suffix.com"; + qualified_name = mgr.qualifyName(partial_name, *ddns_params_, do_dot); + EXPECT_EQ("suffix.com.", qualified_name); + + subnet_->setDdnsQualifyingSuffix("one.two.suffix.com"); + partial_name = "two.suffix.com"; + qualified_name = mgr.qualifyName(partial_name, *ddns_params_, do_dot); + EXPECT_EQ("two.suffix.com.one.two.suffix.com.", qualified_name); +} + +/// @brief Tests the generateFdqn method's ability to construct FQDNs +TEST_F(D2ClientMgrParamsTest, generateFqdn) { + D2ClientMgr mgr; + bool do_dot = true; + + // Create enabled configuration + subnet_->setDdnsSendUpdates(true); + subnet_->setDdnsOverrideNoUpdate(false); + subnet_->setDdnsOverrideClientUpdate(false); + subnet_->setDdnsReplaceClientNameMode(D2ClientConfig::RCM_NEVER); + subnet_->setDdnsGeneratedPrefix("prefix"); + subnet_->setDdnsQualifyingSuffix("suffix.com"); + subnet_->setHostnameCharSet(""); + subnet_->setHostnameCharReplacement(""); + + // Verify that it works with an IPv4 address. + asiolink::IOAddress v4address("192.0.2.75"); + EXPECT_EQ("prefix-192-0-2-75.suffix.com.", + mgr.generateFqdn(v4address, *ddns_params_, do_dot)); + + // Verify that it works with an IPv6 address. + asiolink::IOAddress v6address("2001:db8::2"); + EXPECT_EQ("prefix-2001-db8--2.suffix.com.", + mgr.generateFqdn(v6address, *ddns_params_, do_dot)); + + // Create a disabled config. + subnet_->setDdnsSendUpdates(false); + + // Verify names generate properly with a disabled configuration. + EXPECT_EQ("prefix-192-0-2-75.suffix.com.", + mgr.generateFqdn(v4address, *ddns_params_, do_dot)); + EXPECT_EQ("prefix-2001-db8--2.suffix.com.", + mgr.generateFqdn(v6address, *ddns_params_, do_dot)); +} + +/// @brief Tests adjustDomainName template method with Option4ClientFqdn +TEST_F(D2ClientMgrParamsTest, adjustDomainNameV4) { + D2ClientMgr mgr; + + // Create enabled configuration + subnet_->setDdnsSendUpdates(true); + subnet_->setDdnsOverrideNoUpdate(false); + subnet_->setDdnsOverrideClientUpdate(false); + subnet_->setDdnsReplaceClientNameMode(D2ClientConfig::RCM_NEVER); + subnet_->setDdnsGeneratedPrefix("prefix"); + subnet_->setDdnsQualifyingSuffix("suffix.com"); + subnet_->setHostnameCharSet(""); + subnet_->setHostnameCharReplacement(""); + + struct Scenario { + std::string description_; + D2ClientConfig::ReplaceClientNameMode mode_; + std::string client_name_; + Option4ClientFqdn::DomainNameType client_name_type_; + std::string expected_name_; + Option4ClientFqdn::DomainNameType expected_name_type_; + }; + + std::vector<Scenario> scenarios = { + { + "RCM_NEVER #1, empty client name", + D2ClientConfig::RCM_NEVER, + "", Option4ClientFqdn::PARTIAL, + "", Option4ClientFqdn::PARTIAL + }, + { + "RCM_NEVER #2, partial client name", + D2ClientConfig::RCM_NEVER, + "myhost", Option4ClientFqdn::PARTIAL, + "myhost.suffix.com.", Option4ClientFqdn::FULL + }, + { + "RCM_NEVER #3, full client name", + D2ClientConfig::RCM_NEVER, + "myhost.example.com.", Option4ClientFqdn::FULL, + "myhost.example.com.", Option4ClientFqdn::FULL + }, + { + "RCM_ALWAYS #1, empty client name", + D2ClientConfig::RCM_ALWAYS, + "", Option4ClientFqdn::PARTIAL, + "", Option4ClientFqdn::PARTIAL + }, + { + "RCM_ALWAYS #2, partial client name", + D2ClientConfig::RCM_ALWAYS, + "myhost", Option4ClientFqdn::PARTIAL, + "", Option4ClientFqdn::PARTIAL + }, + { + "RCM_ALWAYS #3, full client name", + D2ClientConfig::RCM_ALWAYS, + "myhost.example.com.", Option4ClientFqdn::FULL, + "", Option4ClientFqdn::PARTIAL + }, + { + "RCM_WHEN_PRESENT #1, empty client name", + D2ClientConfig::RCM_WHEN_PRESENT, + "", Option4ClientFqdn::PARTIAL, + "", Option4ClientFqdn::PARTIAL + }, + { + "RCM_WHEN_PRESENT #2, partial client name", + D2ClientConfig::RCM_WHEN_PRESENT, + "myhost", Option4ClientFqdn::PARTIAL, + "", Option4ClientFqdn::PARTIAL + }, + { + "RCM_WHEN_PRESENT #3, full client name", + D2ClientConfig::RCM_WHEN_PRESENT, + "myhost.example.com.", Option4ClientFqdn::FULL, + "", Option4ClientFqdn::PARTIAL + }, + { + "RCM_WHEN_NOT_PRESENT #1, empty client name", + D2ClientConfig::RCM_WHEN_NOT_PRESENT, + "", Option4ClientFqdn::PARTIAL, + "", Option4ClientFqdn::PARTIAL + }, + { + "RCM_WHEN_NOT_PRESENT #2, partial client name", + D2ClientConfig::RCM_WHEN_NOT_PRESENT, + "myhost", Option4ClientFqdn::PARTIAL, + "myhost.suffix.com.", Option4ClientFqdn::FULL + }, + { + "RCM_WHEN_NOT_PRESENT #3, full client name", + D2ClientConfig::RCM_WHEN_NOT_PRESENT, + "myhost.example.com.", Option4ClientFqdn::FULL, + "myhost.example.com.", Option4ClientFqdn::FULL, + } + }; + + for (auto scenario : scenarios) { + SCOPED_TRACE(scenario.description_); + { + subnet_->setDdnsReplaceClientNameMode(scenario.mode_); + Option4ClientFqdn request (0, Option4ClientFqdn::RCODE_CLIENT(), + scenario.client_name_, + scenario.client_name_type_); + + Option4ClientFqdn response(request); + mgr.adjustDomainName<Option4ClientFqdn>(request, response, *ddns_params_); + EXPECT_EQ(scenario.expected_name_, response.getDomainName()); + EXPECT_EQ(scenario.expected_name_type_, response.getDomainNameType()); + } + } +} + + +/// @brief Tests adjustDomainName template method with Option6ClientFqdn +TEST_F(D2ClientMgrParamsTest, adjustDomainNameV6) { + D2ClientMgr mgr; + + // Create enabled configuration + subnet_->setDdnsSendUpdates(true); + subnet_->setDdnsOverrideNoUpdate(false); + subnet_->setDdnsOverrideClientUpdate(false); + subnet_->setDdnsReplaceClientNameMode(D2ClientConfig::RCM_NEVER); + subnet_->setDdnsGeneratedPrefix("prefix"); + subnet_->setDdnsQualifyingSuffix("suffix.com"); + subnet_->setHostnameCharSet(""); + subnet_->setHostnameCharReplacement(""); + + struct Scenario { + std::string description_; + D2ClientConfig::ReplaceClientNameMode mode_; + std::string client_name_; + Option6ClientFqdn::DomainNameType client_name_type_; + std::string expected_name_; + Option6ClientFqdn::DomainNameType expected_name_type_; + }; + + std::vector<Scenario> scenarios = { + { + "RCM_NEVER #1, empty client name", + D2ClientConfig::RCM_NEVER, + "", Option6ClientFqdn::PARTIAL, + "", Option6ClientFqdn::PARTIAL + }, + { + "RCM_NEVER #2, partial client name", + D2ClientConfig::RCM_NEVER, + "myhost", Option6ClientFqdn::PARTIAL, + "myhost.suffix.com.", Option6ClientFqdn::FULL + }, + { + "RCM_NEVER #3, full client name", + D2ClientConfig::RCM_NEVER, + "myhost.example.com.", Option6ClientFqdn::FULL, + "myhost.example.com.", Option6ClientFqdn::FULL + }, + { + "RCM_ALWAYS #1, empty client name", + D2ClientConfig::RCM_ALWAYS, + "", Option6ClientFqdn::PARTIAL, + "", Option6ClientFqdn::PARTIAL + }, + { + "RCM_ALWAYS #2, partial client name", + D2ClientConfig::RCM_ALWAYS, + "myhost", Option6ClientFqdn::PARTIAL, + "", Option6ClientFqdn::PARTIAL + }, + { + "RCM_ALWAYS #3, full client name", + D2ClientConfig::RCM_ALWAYS, + "myhost.example.com.", Option6ClientFqdn::FULL, + "", Option6ClientFqdn::PARTIAL + }, + { + "RCM_WHEN_PRESENT #1, empty client name", + D2ClientConfig::RCM_WHEN_PRESENT, + "", Option6ClientFqdn::PARTIAL, + "", Option6ClientFqdn::PARTIAL + }, + { + "RCM_WHEN_PRESENT #2, partial client name", + D2ClientConfig::RCM_WHEN_PRESENT, + "myhost", Option6ClientFqdn::PARTIAL, + "", Option6ClientFqdn::PARTIAL + }, + { + "RCM_WHEN_PRESENT #3, full client name", + D2ClientConfig::RCM_WHEN_PRESENT, + "myhost.example.com.", Option6ClientFqdn::FULL, + "", Option6ClientFqdn::PARTIAL + }, + { + "RCM_WHEN_NOT_PRESENT #1, empty client name", + D2ClientConfig::RCM_WHEN_NOT_PRESENT, + "", Option6ClientFqdn::PARTIAL, + "", Option6ClientFqdn::PARTIAL + }, + { + "RCM_WHEN_NOT_PRESENT #2, partial client name", + D2ClientConfig::RCM_WHEN_NOT_PRESENT, + "myhost", Option6ClientFqdn::PARTIAL, + "myhost.suffix.com.", Option6ClientFqdn::FULL + }, + { + "RCM_WHEN_NOT_PRESENT #3, full client name", + D2ClientConfig::RCM_WHEN_NOT_PRESENT, + "myhost.example.com.", Option6ClientFqdn::FULL, + "myhost.example.com.", Option6ClientFqdn::FULL, + } + }; + + for (auto scenario : scenarios) { + SCOPED_TRACE(scenario.description_); + { + subnet_->setDdnsReplaceClientNameMode(scenario.mode_); + Option6ClientFqdn request(0, scenario.client_name_, + scenario.client_name_type_); + + Option6ClientFqdn response(request); + mgr.adjustDomainName<Option6ClientFqdn>(request, response, *ddns_params_); + EXPECT_EQ(scenario.expected_name_, response.getDomainName()); + EXPECT_EQ(scenario.expected_name_type_, response.getDomainNameType()); + } + } +} + +/// @brief Verifies the adustFqdnFlags template with Option6ClientFqdn objects. +/// Ensures that the method can set the N, S, and O flags properly. +/// Other permutations are covered by analyzeFqdnFlags tests. +TEST_F(D2ClientMgrParamsTest, adjustFqdnFlagsV6) { + D2ClientMgr mgr; + Option6ClientFqdnPtr request; + Option6ClientFqdnPtr response; + + // Create enabled configuration with override-no-update true. + subnet_->setDdnsSendUpdates(true); + subnet_->setDdnsOverrideNoUpdate(true); + subnet_->setDdnsOverrideClientUpdate(false); + subnet_->setDdnsReplaceClientNameMode(D2ClientConfig::RCM_NEVER); + subnet_->setDdnsGeneratedPrefix(""); + subnet_->setDdnsQualifyingSuffix(""); + subnet_->setHostnameCharSet(""); + subnet_->setHostnameCharReplacement(""); + + // client S=0 N=0 means client wants to do forward update. + // server S should be 0 (server is not doing forward updates) + // and server N should be 0 (server doing reverse updates) + // and server O should be 0 + request.reset(new Option6ClientFqdn(0, "", Option6ClientFqdn::PARTIAL)); + response.reset(new Option6ClientFqdn(*request)); + response->resetFlags(); + + mgr.adjustFqdnFlags<Option6ClientFqdn>(*request, *response, *ddns_params_); + EXPECT_FALSE(response->getFlag(Option6ClientFqdn::FLAG_S)); + EXPECT_FALSE(response->getFlag(Option6ClientFqdn::FLAG_N)); + EXPECT_FALSE(response->getFlag(Option6ClientFqdn::FLAG_O)); + + // client S=1 N=0 means client wants server to do forward update. + // server S should be 1 (server is doing forward updates) + // and server N should be 0 (server doing updates) + // and server O should be 0 + request.reset(new Option6ClientFqdn(Option6ClientFqdn::FLAG_S, + "", Option6ClientFqdn::PARTIAL)); + response.reset(new Option6ClientFqdn(*request)); + response->resetFlags(); + + mgr.adjustFqdnFlags<Option6ClientFqdn>(*request, *response, *ddns_params_); + EXPECT_TRUE(response->getFlag(Option6ClientFqdn::FLAG_S)); + EXPECT_FALSE(response->getFlag(Option6ClientFqdn::FLAG_N)); + EXPECT_FALSE(response->getFlag(Option6ClientFqdn::FLAG_O)); + + // client S=0 N=1 means client wants no one to do updates + // server S should be 1 (server is doing forward updates) + // and server N should be 0 (server doing updates) + // and O should be 1 (overriding client S) + request.reset(new Option6ClientFqdn(Option6ClientFqdn::FLAG_N, + "", Option6ClientFqdn::PARTIAL)); + response.reset(new Option6ClientFqdn(*request)); + response->resetFlags(); + + mgr.adjustFqdnFlags<Option6ClientFqdn>(*request, *response, *ddns_params_); + EXPECT_TRUE(response->getFlag(Option6ClientFqdn::FLAG_S)); + EXPECT_FALSE(response->getFlag(Option6ClientFqdn::FLAG_N)); + EXPECT_TRUE(response->getFlag(Option6ClientFqdn::FLAG_O)); +} + + +/// @brief Verified the getUpdateDirections template method with +/// Option6ClientFqdn objects. +TEST(D2ClientMgr, updateDirectionsV6) { + D2ClientMgr mgr; + Option6ClientFqdnPtr response; + + bool do_forward = false; + bool do_reverse = false; + + // Response S=0, N=0 should mean do reverse only. + response.reset(new Option6ClientFqdn(0, + "", Option6ClientFqdn::PARTIAL)); + mgr.getUpdateDirections(*response, do_forward, do_reverse); + EXPECT_FALSE(do_forward); + EXPECT_TRUE(do_reverse); + + // Response S=0, N=1 should mean don't do either. + response.reset(new Option6ClientFqdn(Option6ClientFqdn::FLAG_N, + "", Option6ClientFqdn::PARTIAL)); + mgr.getUpdateDirections(*response, do_forward, do_reverse); + EXPECT_FALSE(do_forward); + EXPECT_FALSE(do_reverse); + + // Response S=1, N=0 should mean do both. + response.reset(new Option6ClientFqdn(Option6ClientFqdn::FLAG_S, + "", Option6ClientFqdn::PARTIAL)); + mgr.getUpdateDirections(*response, do_forward, do_reverse); + EXPECT_TRUE(do_forward); + EXPECT_TRUE(do_reverse); + + // Response S=1, N=1 isn't possible. +} + +/// @brief Tests v4 FQDN name sanitizing +TEST_F(D2ClientMgrParamsTest, sanitizeFqdnV4) { + D2ClientMgr mgr; + + // Create enabled configuration with override-no-update true. + subnet_->setDdnsSendUpdates(true); + subnet_->setDdnsOverrideNoUpdate(false); + subnet_->setDdnsOverrideClientUpdate(false); + subnet_->setDdnsReplaceClientNameMode(D2ClientConfig::RCM_NEVER); + subnet_->setDdnsGeneratedPrefix("prefix"); + subnet_->setDdnsQualifyingSuffix("suffix.com"); + subnet_->setHostnameCharSet("[^A-Za-z0-9-]"); + subnet_->setHostnameCharReplacement("x"); + + // Get the sanitizer. + str::StringSanitizerPtr hostname_sanitizer; + ASSERT_NO_THROW(hostname_sanitizer = ddns_params_->getHostnameSanitizer()); + ASSERT_TRUE(hostname_sanitizer); + + struct Scenario { + std::string description_; + std::string client_name_; + Option4ClientFqdn::DomainNameType name_type_; + std::string expected_name_; + }; + + std::vector<Scenario> scenarios = { + { + "full FQDN, name unchanged", + "One.123.example.com.", + Option4ClientFqdn::FULL, + "one.123.example.com." + }, + { + "partial FQDN, name unchanged, but qualified", + "One.123", + Option4ClientFqdn::PARTIAL, + "one.123.suffix.com." + }, + { + "full FQDN, scrubbed", + "O#n^e.123.ex&a*mple.com.", + Option4ClientFqdn::FULL, + "oxnxe.123.exxaxmple.com." + }, + { + "partial FQDN, scrubbed and qualified", + "One.1+2|3", + Option4ClientFqdn::PARTIAL, + "one.1x2x3.suffix.com." + }, + { + "full FQDN with characters that get escaped", + "O n e.123.exa(m)ple.com.", + Option4ClientFqdn::FULL, + "oxnxe.123.exaxmxple.com." + }, + { + "full FQDN with escape sequences", + "O\032n\032e.123.example.com.", + Option4ClientFqdn::FULL, + "oxnxe.123.example.com." + } + }; + + for (auto scenario : scenarios) { + SCOPED_TRACE(scenario.description_); + { + Option4ClientFqdn request(0, Option4ClientFqdn::RCODE_CLIENT(), + scenario.client_name_, scenario.name_type_); + Option4ClientFqdn response(request); + + mgr.adjustDomainName<Option4ClientFqdn>(request, response, *ddns_params_); + EXPECT_EQ(scenario.expected_name_, response.getDomainName()); + EXPECT_EQ(Option4ClientFqdn::FULL, response.getDomainNameType()); + } + } +} + +/// @brief Tests v6 FQDN name sanitizing +/// @todo This test currently verifies that Option6ClientFqdn::DomainName +/// downcases strings used to construct it. For some reason, currently +/// unknown, Option4ClientFqdn preserves the case, while Option6ClientFqdn +/// downcases it (see setDomainName() in both classes. See Trac #5700. +TEST_F(D2ClientMgrParamsTest, sanitizeFqdnV6) { + D2ClientMgr mgr; + + // Create enabled configuration with override-no-update true. + subnet_->setDdnsSendUpdates(true); + subnet_->setDdnsOverrideNoUpdate(false); + subnet_->setDdnsOverrideClientUpdate(false); + subnet_->setDdnsReplaceClientNameMode(D2ClientConfig::RCM_NEVER); + subnet_->setDdnsGeneratedPrefix("prefix"); + subnet_->setDdnsQualifyingSuffix("suffix.com"); + subnet_->setHostnameCharSet("[^A-Za-z0-9-]"); + subnet_->setHostnameCharReplacement("x"); + + // Get the sanitizer. + str::StringSanitizerPtr hostname_sanitizer; + ASSERT_NO_THROW(hostname_sanitizer = ddns_params_->getHostnameSanitizer()); + ASSERT_TRUE(hostname_sanitizer); + + struct Scenario { + std::string description_; + std::string client_name_; + Option6ClientFqdn::DomainNameType name_type_; + std::string expected_name_; + }; + + std::vector<Scenario> scenarios = { + { + "full FQDN, name unchanged", + "One.123.example.com.", + Option6ClientFqdn::FULL, + "one.123.example.com." + }, + { + "partial FQDN, name unchanged, but qualified", + "One.123", + Option6ClientFqdn::PARTIAL, + "one.123.suffix.com." + }, + { + "full FQDN, scrubbed", + "O#n^e.123.ex&a*mple.com.", + Option6ClientFqdn::FULL, + "oxnxe.123.exxaxmple.com." + }, + { + "partial FQDN, scrubbed and qualified", + "One.1+2|3", + Option6ClientFqdn::PARTIAL, + "one.1x2x3.suffix.com." + }, + { + "full FQDN with characters that get escaped", + "O n e.123.exa(m)ple.com.", + Option6ClientFqdn::FULL, + "oxnxe.123.exaxmxple.com." + }, + { + "full FQDN with escape sequences", + "O\032n\032e.123.example.com.", + Option6ClientFqdn::FULL, + "oxnxe.123.example.com." + } + }; + + Option6ClientFqdnPtr response; + for (auto scenario : scenarios) { + SCOPED_TRACE(scenario.description_); + { + Option6ClientFqdn request(0, scenario.client_name_, scenario.name_type_); + Option6ClientFqdn response(request); + + mgr.adjustDomainName<Option6ClientFqdn>(request, response, *ddns_params_); + EXPECT_EQ(scenario.expected_name_, response.getDomainName()); + EXPECT_EQ(Option6ClientFqdn::FULL, response.getDomainNameType()); + } + } +} + +} // end of anonymous namespace |