diff options
Diffstat (limited to 'src/lib/dhcpsrv/tests/shared_network_parser_unittest.cc')
-rw-r--r-- | src/lib/dhcpsrv/tests/shared_network_parser_unittest.cc | 1023 |
1 files changed, 1023 insertions, 0 deletions
diff --git a/src/lib/dhcpsrv/tests/shared_network_parser_unittest.cc b/src/lib/dhcpsrv/tests/shared_network_parser_unittest.cc new file mode 100644 index 0000000..4af574a --- /dev/null +++ b/src/lib/dhcpsrv/tests/shared_network_parser_unittest.cc @@ -0,0 +1,1023 @@ +// Copyright (C) 2017-2022 Internet Systems Consortium, Inc. ("ISC") +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +#include <config.h> +#include <asiolink/io_address.h> +#include <cc/data.h> +#include <dhcp/dhcp4.h> +#include <dhcp/dhcp6.h> +#include <dhcp/option.h> +#include <dhcp/option4_addrlst.h> +#include <dhcp/option6_addrlst.h> +#include <dhcp/tests/iface_mgr_test_config.h> +#include <dhcpsrv/cfg_option.h> +#include <dhcpsrv/parsers/shared_network_parser.h> +#include <testutils/gtest_utils.h> +#include <gtest/gtest.h> +#include <string> + +using namespace isc; +using namespace isc::asiolink; +using namespace isc::data; +using namespace isc::dhcp; +using namespace isc::dhcp::test; + +namespace { +class SharedNetworkParserTest : public ::testing::Test { +public: + + /// @brief Structure for describing a single relay test scenario + struct RelayTest { + /// @brief Description of the test scenario, used for logging + std::string description_; + /// @brief JSON configuration body of the "relay" element + std::string json_content_; + /// @brief indicates if parsing should pass or fail + bool should_parse_; + /// @brief list of addresses expected after parsing + IOAddressList addresses_; + }; + + /// @brief virtual destructor + virtual ~SharedNetworkParserTest(){}; + + /// @brief Fetch valid shared network configuration JSON text + virtual std::string getWorkingConfig() const = 0; + ElementPtr makeTestConfig(const std::string& name, const std::string& json_content) { + // Create working config element tree + ElementPtr config = Element::fromJSON(getWorkingConfig()); + + // Create test element contents + ElementPtr content = Element::fromJSON(json_content); + + // Add the test element to working config + config->set(name, content); + return (config); + } + + /// @brief Executes a single "relay" parsing scenario + /// + /// Each test pass consists of the following steps: + /// -# Attempt to parse the given JSON text + /// -# If parsing is expected to fail and it does return otherwise fatal fail + /// -# If parsing is expected to succeed, fatal fail if it does not + /// -# Verify the network's relay address list matches the expected list + /// in size and content. + /// + /// @param test RelayTest which describes the test to conduct + void relayTest(const RelayTest& test) { + ElementPtr test_config; + ASSERT_NO_THROW(test_config = + makeTestConfig("relay", test.json_content_)); + + // Init our ref to a place holder + Network4 dummy; + Network& network = dummy; + + // If parsing should fail, call parse expecting a throw. + if (!test.should_parse_) { + ASSERT_THROW(network = parseIntoNetwork(test_config), DhcpConfigError); + // No throw so test outcome is correct, nothing else to do. + return; + } + + // Should parse without error, let's see if it does. + ASSERT_NO_THROW(network = parseIntoNetwork(test_config)); + + // It parsed, are the number of entries correct? + ASSERT_EQ(test.addresses_.size(), network.getRelayAddresses().size()); + + // Are the expected addresses in the list? + for (auto exp_address = test.addresses_.begin(); exp_address != test.addresses_.end(); + ++exp_address) { + EXPECT_TRUE(network.hasRelayAddress(*exp_address)) + << " expected address: " << (*exp_address).toText() << " not found" ; + } + } + + /// @brief Attempts to parse the given configuration into a shared network + /// + /// Virtual function used by relayTest() to parse a test configuration. + /// Implementation should not catch parsing exceptions. + /// + /// @param test_config JSON configuration text to parse + /// @return A reference to the Network created if parsing is successful + virtual Network& parseIntoNetwork(ConstElementPtr test_config) = 0; +}; + + +/// @brief Test fixture class for SharedNetwork4Parser class. +class SharedNetwork4ParserTest : public SharedNetworkParserTest { +public: + + /// @brief Creates valid shared network configuration. + /// + /// @return Valid shared network configuration. + virtual std::string getWorkingConfig() const { + std::string config = "{" + " \"authoritative\": true," + " \"boot-file-name\": \"/dev/null\"," + " \"client-class\": \"srv1\"," + " \"interface\": \"eth1961\"," + " \"match-client-id\": true," + " \"name\": \"bird\"," + " \"next-server\": \"10.0.0.1\"," + " \"rebind-timer\": 199," + " \"relay\": { \"ip-addresses\": [ \"10.1.1.1\" ] }," + " \"renew-timer\": 99," + " \"reservations-global\": false," + " \"reservations-in-subnet\": true," + " \"reservations-out-of-pool\": true," + " \"server-hostname\": \"example.org\"," + " \"require-client-classes\": [ \"runner\" ]," + " \"user-context\": { \"comment\": \"example\" }," + " \"valid-lifetime\": 399," + " \"min-valid-lifetime\": 299," + " \"max-valid-lifetime\": 499," + " \"calculate-tee-times\": true," + " \"t1-percent\": 0.345," + " \"t2-percent\": 0.721," + " \"ddns-send-updates\": true," + " \"ddns-override-no-update\": true," + " \"ddns-override-client-update\": true," + " \"ddns-replace-client-name\": \"always\"," + " \"ddns-generated-prefix\": \"prefix\"," + " \"ddns-qualifying-suffix\": \"example.com.\"," + " \"hostname-char-set\": \"[^A-Z]\"," + " \"hostname-char-replacement\": \"x\"," + " \"store-extended-info\": true," + " \"cache-threshold\": 0.123," + " \"cache-max-age\": 123," + " \"ddns-update-on-renew\": true," + " \"option-data\": [" + " {" + " \"name\": \"domain-name-servers\"," + " \"data\": \"192.0.2.3\"" + " }" + " ]," + " \"subnet4\": [" + " {" + " \"id\": 1," + " \"subnet\": \"10.1.2.0/24\"," + " \"interface\": \"\"," + " \"renew-timer\": 100," + " \"rebind-timer\": 200," + " \"valid-lifetime\": 300," + " \"min-valid-lifetime\": 200," + " \"max-valid-lifetime\": 400," + " \"match-client-id\": false," + " \"authoritative\": false," + " \"next-server\": \"\"," + " \"server-hostname\": \"\"," + " \"boot-file-name\": \"\"," + " \"client-class\": \"\"," + " \"require-client-classes\": []\n," + " \"reservations-global\": false," + " \"reservations-in-subnet\": true," + " \"reservations-out-of-pool\": false," + " \"4o6-interface\": \"\"," + " \"4o6-interface-id\": \"\"," + " \"4o6-subnet\": \"\"," + " \"calculate-tee-times\": true," + " \"t1-percent\": .45," + " \"t2-percent\": .65," + " \"hostname-char-set\": \"\"," + " \"cache-threshold\": .20," + " \"cache-max-age\": 50" + " }," + " {" + " \"id\": 2," + " \"subnet\": \"192.0.2.0/24\"," + " \"interface\": \"\"," + " \"renew-timer\": 10," + " \"rebind-timer\": 20," + " \"valid-lifetime\": 30," + " \"match-client-id\": false," + " \"authoritative\": false," + " \"next-server\": \"\"," + " \"server-hostname\": \"\"," + " \"boot-file-name\": \"\"," + " \"client-class\": \"\"," + " \"require-client-classes\": []\n," + " \"reservations-global\": false," + " \"reservations-in-subnet\": true," + " \"reservations-out-of-pool\": false," + " \"4o6-interface\": \"\"," + " \"4o6-interface-id\": \"\"," + " \"4o6-subnet\": \"\"," + " \"calculate-tee-times\": false," + " \"t1-percent\": .40," + " \"t2-percent\": .80," + " \"cache-threshold\": .15," + " \"cache-max-age\": 5" + " }" + " ]" + "}"; + + return (config); + } + + virtual Network& parseIntoNetwork(ConstElementPtr test_config) { + // Parse configuration. + SharedNetwork4Parser parser; + network_ = parser.parse(test_config); + return (*network_); + } + +private: + SharedNetwork4Ptr network_; +}; + +// This test verifies that shared network parser for IPv4 works properly +// in a positive test scenario. +TEST_F(SharedNetwork4ParserTest, parse) { + IfaceMgrTestConfig ifmgr(true); + + // Basic configuration for shared network. A bunch of parameters + // have to be specified for subnets because subnet parsers expect + // that default and global values are set. + std::string config = getWorkingConfig(); + ElementPtr config_element = Element::fromJSON(config); + + // Parse configuration specified above. + SharedNetwork4Parser parser; + SharedNetwork4Ptr network; + + ASSERT_NO_THROW_LOG(network = parser.parse(config_element)); + ASSERT_TRUE(network); + + // Check basic parameters. + EXPECT_TRUE(network->getAuthoritative()); + EXPECT_EQ("srv1", network->getClientClass().get()); + EXPECT_EQ("bird", network->getName()); + EXPECT_EQ("eth1961", network->getIface().get()); + EXPECT_EQ(99, network->getT1()); + EXPECT_EQ(199, network->getT2()); + EXPECT_EQ(399, network->getValid()); + EXPECT_EQ(299, network->getValid().getMin()); + EXPECT_EQ(499, network->getValid().getMax()); + EXPECT_TRUE(network->getCalculateTeeTimes()); + EXPECT_EQ(0.345, network->getT1Percent()); + EXPECT_EQ(0.721, network->getT2Percent()); + EXPECT_EQ("/dev/null", network->getFilename().get()); + EXPECT_EQ("10.0.0.1", network->getSiaddr().get().toText()); + EXPECT_EQ("example.org", network->getSname().get()); + EXPECT_FALSE(network->getReservationsGlobal()); + EXPECT_TRUE(network->getReservationsInSubnet()); + EXPECT_TRUE(network->getReservationsOutOfPool()); + EXPECT_TRUE(network->getDdnsSendUpdates().get()); + EXPECT_TRUE(network->getDdnsOverrideNoUpdate().get()); + EXPECT_TRUE(network->getDdnsOverrideClientUpdate().get()); + EXPECT_EQ(D2ClientConfig::RCM_ALWAYS, network->getDdnsReplaceClientNameMode().get()); + EXPECT_EQ("prefix", network->getDdnsGeneratedPrefix().get()); + EXPECT_EQ("example.com.", network->getDdnsQualifyingSuffix().get()); + EXPECT_EQ("[^A-Z]", network->getHostnameCharSet().get()); + EXPECT_EQ("x", network->getHostnameCharReplacement().get()); + EXPECT_TRUE(network->getStoreExtendedInfo().get()); + EXPECT_EQ(0.123, network->getCacheThreshold()); + EXPECT_EQ(123, network->getCacheMaxAge()); + EXPECT_TRUE(network->getDdnsUpdateOnRenew().get()); + + // Relay information. + auto relay_info = network->getRelayInfo(); + EXPECT_EQ(1, relay_info.getAddresses().size()); + EXPECT_TRUE(relay_info.containsAddress(IOAddress("10.1.1.1"))); + + // Required client classes. + auto required = network->getRequiredClasses(); + ASSERT_EQ(1, required.size()); + EXPECT_EQ("runner", *required.cbegin()); + + // Check user context. + ConstElementPtr context = network->getContext(); + ASSERT_TRUE(context); + EXPECT_TRUE(context->get("comment")); + + // Subnet with id 1 + Subnet4Ptr subnet = network->getSubnet(SubnetID(1)); + ASSERT_TRUE(subnet); + EXPECT_EQ("10.1.2.0", subnet->get().first.toText()); + EXPECT_EQ(300, subnet->getValid()); + EXPECT_EQ(200, subnet->getValid().getMin()); + EXPECT_EQ(400, subnet->getValid().getMax()); + EXPECT_FALSE(subnet->getHostnameCharSet().unspecified()); + EXPECT_EQ("", subnet->getHostnameCharSet().get()); + + // Subnet with id 2 + subnet = network->getSubnet(SubnetID(2)); + ASSERT_TRUE(subnet); + EXPECT_EQ("192.0.2.0", subnet->get().first.toText()); + EXPECT_EQ(30, subnet->getValid()); + EXPECT_EQ(30, subnet->getValid().getMin()); + EXPECT_EQ(30, subnet->getValid().getMax()); + EXPECT_EQ("[^A-Z]", subnet->getHostnameCharSet().get()); + EXPECT_EQ("x", subnet->getHostnameCharReplacement().get()); + + // DHCP options + ConstCfgOptionPtr cfg_option = network->getCfgOption(); + ASSERT_TRUE(cfg_option); + OptionDescriptor opt_dns_servers = cfg_option->get("dhcp4", + DHO_DOMAIN_NAME_SERVERS); + ASSERT_TRUE(opt_dns_servers.option_); + Option4AddrLstPtr dns_servers = boost::dynamic_pointer_cast< + Option4AddrLst>(opt_dns_servers.option_); + ASSERT_TRUE(dns_servers); + Option4AddrLst::AddressContainer addresses = dns_servers->getAddresses(); + ASSERT_EQ(1, addresses.size()); + EXPECT_EQ("192.0.2.3", addresses[0].toText()); +} + +// This test verifies that parser throws an exception when mandatory parameter +// "name" is not specified. +TEST_F(SharedNetwork4ParserTest, missingName) { + // Remove a name parameter from the valid configuration. + std::string config = getWorkingConfig(); + ElementPtr config_element = Element::fromJSON(config); + ASSERT_NO_THROW(config_element->remove("name")); + + // Parse configuration specified above. + SharedNetwork4Parser parser; + SharedNetwork4Ptr network; + ASSERT_THROW(network = parser.parse(config_element), DhcpConfigError); +} + +// This test verifies that it's possible to specify client-class, +// match-client-id, and authoritative on shared-network level. +TEST_F(SharedNetwork4ParserTest, clientClassMatchClientIdAuthoritative) { + IfaceMgrTestConfig ifmgr(true); + + std::string config = getWorkingConfig(); + ElementPtr config_element = Element::fromJSON(config); + + config_element->set("authoritative", Element::create(true)); + config_element->set("match-client-id", Element::create(false)); + config_element->set("client-class", Element::create("alpha")); + + // Parse configuration specified above. + SharedNetwork4Parser parser; + SharedNetwork4Ptr network; + network = parser.parse(config_element); + ASSERT_TRUE(network); + + EXPECT_EQ("alpha", network->getClientClass().get()); + + EXPECT_FALSE(network->getMatchClientId()); + + EXPECT_TRUE(network->getAuthoritative()); +} + +// This test verifies that parsing of the "relay" element. +// It checks both valid and invalid scenarios. +TEST_F(SharedNetwork4ParserTest, relayInfoTests) { + IfaceMgrTestConfig ifmgr(true); + + // Create the vector of test scenarios. + std::vector<RelayTest> tests = { + { + "valid ip-address #1", + "{ \"ip-address\": \"192.168.2.1\" }", + true, + { asiolink::IOAddress("192.168.2.1") } + }, + { + "invalid ip-address #1", + "{ \"ip-address\": \"not an address\" }", + false, + { } + }, + { + "invalid ip-address #2", + "{ \"ip-address\": \"2001:db8::1\" }", + false, + { } + }, + { + "valid ip-addresses #1", + "{ \"ip-addresses\": [ ] }", + true, + {} + }, + { + "valid ip-addresses #2", + "{ \"ip-addresses\": [ \"192.168.2.1\" ] }", + true, + { asiolink::IOAddress("192.168.2.1") } + }, + { + "valid ip-addresses #3", + "{ \"ip-addresses\": [ \"192.168.2.1\", \"192.168.2.2\" ] }", + true, + { asiolink::IOAddress("192.168.2.1"), asiolink::IOAddress("192.168.2.2") } + }, + { + "invalid ip-addresses #1", + "{ \"ip-addresses\": [ \"not an address\" ] }", + false, + { } + }, + { + "invalid ip-addresses #2", + "{ \"ip-addresses\": [ \"2001:db8::1\" ] }", + false, + { } + }, + { + "invalid both ip-address and ip-addresses", + "{" + " \"ip-address\": \"192.168.2.1\", " + " \"ip-addresses\": [ \"192.168.2.1\", \"192.168.2.2\" ]" + " }", + false, + { } + }, + { + "invalid neither ip-address nor ip-addresses", + "{}", + false, + { } + } + }; + + // Iterate over the test scenarios, verifying each prescribed + // outcome. + for (auto test = tests.begin(); test != tests.end(); ++test) { + { + SCOPED_TRACE((*test).description_); + relayTest(*test); + } + } +} + +// This test verifies that the optional interface check works as expected. +TEST_F(SharedNetwork4ParserTest, iface) { + // Basic configuration for shared network. + std::string config = getWorkingConfig(); + ElementPtr config_element = Element::fromJSON(config); + + // Parse configuration specified above. + + // The interface check can be disabled. + SharedNetwork4Parser parser_no_check(false); + SharedNetwork4Ptr network; + EXPECT_NO_THROW(network = parser_no_check.parse(config_element)); + ASSERT_TRUE(network); + EXPECT_FALSE(network->getIface().unspecified()); + EXPECT_EQ("eth1961", network->getIface().get()); + + // Retry with the interface check enabled. + SharedNetwork4Parser parser; + EXPECT_THROW(parser.parse(config_element), DhcpConfigError); + + // Configure default test interfaces. + IfaceMgrTestConfig ifmgr(true); + + EXPECT_NO_THROW(network = parser_no_check.parse(config_element)); + ASSERT_TRUE(network); + EXPECT_FALSE(network->getIface().unspecified()); + EXPECT_EQ("eth1961", network->getIface().get()); + + EXPECT_NO_THROW(network = parser.parse(config_element)); + ASSERT_TRUE(network); + EXPECT_FALSE(network->getIface().unspecified()); + EXPECT_EQ("eth1961", network->getIface().get()); +} + +// This test verifies that shared network parser for IPv4 works properly +// when using invalid renew and rebind timers. +TEST_F(SharedNetwork4ParserTest, parseWithInvalidRenewRebind) { + IfaceMgrTestConfig ifmgr(true); + + // Basic configuration for shared network. + std::string config = getWorkingConfig(); + ElementPtr config_element = Element::fromJSON(config); + ConstElementPtr valid_element = config_element->get("rebind-timer"); + int64_t value = valid_element->intValue(); + valid_element = config_element->get("renew-timer"); + ElementPtr mutable_element = boost::const_pointer_cast<Element>(valid_element); + mutable_element->setValue(value + 1); + + // Parse configuration specified above. + SharedNetwork4Parser parser; + SharedNetwork4Ptr network; + + ASSERT_THROW(network = parser.parse(config_element), DhcpConfigError); + ASSERT_FALSE(network); +} + +// This test verifies that shared network parser for IPv4 works properly +// when renew and rebind timers are equal. +TEST_F(SharedNetwork4ParserTest, parseValidWithEqualRenewRebind) { + IfaceMgrTestConfig ifmgr(true); + + // Basic configuration for shared network. + std::string config = getWorkingConfig(); + ElementPtr config_element = Element::fromJSON(config); + ConstElementPtr valid_element = config_element->get("rebind-timer"); + int64_t value = valid_element->intValue(); + valid_element = config_element->get("renew-timer"); + ElementPtr mutable_element = boost::const_pointer_cast<Element>(valid_element); + mutable_element->setValue(value); + + // Parse configuration specified above. + SharedNetwork4Parser parser; + SharedNetwork4Ptr network; + + ASSERT_NO_THROW(network = parser.parse(config_element)); + ASSERT_TRUE(network); +} + +/// @brief Test fixture class for SharedNetwork6Parser class. +class SharedNetwork6ParserTest : public SharedNetworkParserTest { +public: + + /// @brief Constructor. + SharedNetwork6ParserTest() + : SharedNetworkParserTest(), network_(), use_iface_id_(false) { + } + + /// @brief Creates valid shared network configuration. + /// + /// @return Valid shared network configuration. + virtual std::string getWorkingConfig() const { + std::string config = "{" + " \"client-class\": \"srv1\"," + + std::string(use_iface_id_ ? "\"interface-id\": " : "\"interface\": ") + + "\"eth1961\"," + " \"name\": \"bird\"," + " \"preferred-lifetime\": 211," + " \"min-preferred-lifetime\": 111," + " \"max-preferred-lifetime\": 311," + " \"rapid-commit\": true," + " \"rebind-timer\": 199," + " \"relay\": { \"ip-addresses\": [ \"2001:db8:1::1\" ] }," + " \"renew-timer\": 99," + " \"require-client-classes\": [ \"runner\" ]," + " \"reservations-global\": false," + " \"reservations-in-subnet\": true," + " \"reservations-out-of-pool\": true," + " \"user-context\": { }," + " \"valid-lifetime\": 399," + " \"min-valid-lifetime\": 299," + " \"max-valid-lifetime\": 499," + " \"calculate-tee-times\": true," + " \"t1-percent\": 0.345," + " \"t2-percent\": 0.721," + " \"ddns-send-updates\": true," + " \"ddns-override-no-update\": true," + " \"ddns-override-client-update\": true," + " \"ddns-replace-client-name\": \"always\"," + " \"ddns-generated-prefix\": \"prefix\"," + " \"ddns-qualifying-suffix\": \"example.com.\"," + " \"hostname-char-set\": \"[^A-Z]\"," + " \"hostname-char-replacement\": \"x\"," + " \"store-extended-info\": true," + " \"cache-threshold\": 0.123," + " \"cache-max-age\": 123," + " \"ddns-update-on-renew\": true," + " \"option-data\": [" + " {" + " \"name\": \"dns-servers\"," + " \"data\": \"2001:db8:1::cafe\"" + " }" + " ]," + " \"subnet6\": [" + " {" + " \"id\": 1," + " \"subnet\": \"3000::/16\"," + " \"interface\": \"\"," + " \"interface-id\": \"\"," + " \"renew-timer\": 100," + " \"rebind-timer\": 200," + " \"preferred-lifetime\": 300," + " \"min-preferred-lifetime\": 200," + " \"max-preferred-lifetime\": 400," + " \"valid-lifetime\": 400," + " \"min-valid-lifetime\": 300," + " \"max-valid-lifetime\": 500," + " \"client-class\": \"\"," + " \"require-client-classes\": []\n," + " \"reservations-global\": false," + " \"reservations-in-subnet\": true," + " \"reservations-out-of-pool\": false," + " \"rapid-commit\": false," + " \"hostname-char-set\": \"\"" + " }," + " {" + " \"id\": 2," + " \"subnet\": \"2001:db8:1::/64\"," + " \"interface\": \"\"," + " \"interface-id\": \"\"," + " \"renew-timer\": 10," + " \"rebind-timer\": 20," + " \"preferred-lifetime\": 30," + " \"valid-lifetime\": 40," + " \"client-class\": \"\"," + " \"require-client-classes\": []\n," + " \"reservations-global\": false," + " \"reservations-in-subnet\": true," + " \"reservations-out-of-pool\": false," + " \"rapid-commit\": false" + " }" + " ]" + "}"; + + return (config); + } + + virtual Network& parseIntoNetwork(ConstElementPtr test_config) { + // Parse configuration. + SharedNetwork6Parser parser; + network_ = parser.parse(test_config); + return (*network_); + } + +public: + + SharedNetwork6Ptr network_; + + /// Boolean flag indicating if the interface-id should be used instead + /// of interface. + bool use_iface_id_; +}; + +// This test verifies that shared network parser for IPv6 works properly +// in a positive test scenario. +TEST_F(SharedNetwork6ParserTest, parse) { + IfaceMgrTestConfig ifmgr(true); + + // Basic configuration for shared network. A bunch of parameters + // have to be specified for subnets because subnet parsers expect + // that default and global values are set. + std::string config = getWorkingConfig(); + ElementPtr config_element = Element::fromJSON(config); + + // Parse configuration specified above. + SharedNetwork6Parser parser; + SharedNetwork6Ptr network; + ASSERT_NO_THROW(network = parser.parse(config_element)); + ASSERT_TRUE(network); + + // Check basic parameters. + EXPECT_EQ("srv1", network->getClientClass().get()); + EXPECT_EQ("bird", network->getName()); + EXPECT_EQ("eth1961", network->getIface().get()); + EXPECT_EQ(211, network->getPreferred()); + EXPECT_EQ(111, network->getPreferred().getMin()); + EXPECT_EQ(311, network->getPreferred().getMax()); + EXPECT_TRUE(network->getRapidCommit()); + EXPECT_EQ(99, network->getT1()); + EXPECT_EQ(199, network->getT2()); + EXPECT_EQ(399, network->getValid()); + EXPECT_EQ(299, network->getValid().getMin()); + EXPECT_EQ(499, network->getValid().getMax()); + EXPECT_TRUE(network->getCalculateTeeTimes()); + EXPECT_EQ(0.345, network->getT1Percent()); + EXPECT_EQ(0.721, network->getT2Percent()); + EXPECT_TRUE(network->getDdnsSendUpdates().get()); + EXPECT_TRUE(network->getDdnsOverrideNoUpdate().get()); + EXPECT_TRUE(network->getDdnsOverrideClientUpdate().get()); + EXPECT_EQ(D2ClientConfig::RCM_ALWAYS, network->getDdnsReplaceClientNameMode().get()); + EXPECT_EQ("prefix", network->getDdnsGeneratedPrefix().get()); + EXPECT_EQ("example.com.", network->getDdnsQualifyingSuffix().get()); + EXPECT_EQ("[^A-Z]", network->getHostnameCharSet().get()); + EXPECT_EQ("x", network->getHostnameCharReplacement().get()); + EXPECT_TRUE(network->getStoreExtendedInfo().get()); + EXPECT_EQ(0.123, network->getCacheThreshold()); + EXPECT_EQ(123, network->getCacheMaxAge()); + EXPECT_TRUE(network->getDdnsUpdateOnRenew().get()); + + // Relay information. + auto relay_info = network->getRelayInfo(); + EXPECT_EQ(1, relay_info.getAddresses().size()); + EXPECT_TRUE(relay_info.containsAddress(IOAddress("2001:db8:1::1"))); + + // Required client classes. + auto required = network->getRequiredClasses(); + ASSERT_EQ(1, required.size()); + EXPECT_EQ("runner", *required.cbegin()); + + // Check user context. + ConstElementPtr context = network->getContext(); + ASSERT_TRUE(context); + EXPECT_EQ(0, context->size()); + + // Subnet with id 1 + Subnet6Ptr subnet = network->getSubnet(SubnetID(1)); + ASSERT_TRUE(subnet); + EXPECT_EQ("3000::", subnet->get().first.toText()); + EXPECT_EQ(300, subnet->getPreferred()); + EXPECT_EQ(200, subnet->getPreferred().getMin()); + EXPECT_EQ(400, subnet->getPreferred().getMax()); + EXPECT_EQ(400, subnet->getValid()); + EXPECT_EQ(300, subnet->getValid().getMin()); + EXPECT_EQ(500, subnet->getValid().getMax()); + EXPECT_FALSE(subnet->getHostnameCharSet().unspecified()); + EXPECT_EQ("", subnet->getHostnameCharSet().get()); + + // Subnet with id 2 + subnet = network->getSubnet(SubnetID(2)); + ASSERT_TRUE(subnet); + EXPECT_EQ("2001:db8:1::", subnet->get().first.toText()); + EXPECT_EQ(30, subnet->getPreferred()); + EXPECT_EQ(30, subnet->getPreferred().getMin()); + EXPECT_EQ(30, subnet->getPreferred().getMax()); + EXPECT_EQ(40, subnet->getValid()); + EXPECT_EQ(40, subnet->getValid().getMin()); + EXPECT_EQ(40, subnet->getValid().getMax()); + EXPECT_EQ("[^A-Z]", subnet->getHostnameCharSet().get()); + EXPECT_EQ("x", subnet->getHostnameCharReplacement().get()); + + // DHCP options + ConstCfgOptionPtr cfg_option = network->getCfgOption(); + ASSERT_TRUE(cfg_option); + OptionDescriptor opt_dns_servers = cfg_option->get("dhcp6", + D6O_NAME_SERVERS); + ASSERT_TRUE(opt_dns_servers.option_); + Option6AddrLstPtr dns_servers = boost::dynamic_pointer_cast< + Option6AddrLst>(opt_dns_servers.option_); + ASSERT_TRUE(dns_servers); + Option6AddrLst::AddressContainer addresses = dns_servers->getAddresses(); + ASSERT_EQ(1, addresses.size()); + EXPECT_EQ("2001:db8:1::cafe", addresses[0].toText()); +} + +// This test verifies that shared network parser for IPv6 works properly +// in a positive test scenario. +TEST_F(SharedNetwork6ParserTest, parseWithInterfaceId) { + IfaceMgrTestConfig ifmgr(true); + + // Use the configuration with interface-id instead of interface parameter. + use_iface_id_ = true; + std::string config = getWorkingConfig(); + ElementPtr config_element = Element::fromJSON(config); + + // Parse configuration specified above. + SharedNetwork6Parser parser; + SharedNetwork6Ptr network; + ASSERT_NO_THROW(network = parser.parse(config_element)); + ASSERT_TRUE(network); + + // Check that interface-id has been parsed. + auto opt_iface_id = network->getInterfaceId(); + ASSERT_TRUE(opt_iface_id); +} + +// This test verifies that shared network parser for IPv6 works properly +// when using invalid renew and rebind timers. +TEST_F(SharedNetwork6ParserTest, parseWithInvalidRenewRebind) { + IfaceMgrTestConfig ifmgr(true); + + // Use the configuration with interface-id instead of interface parameter. + use_iface_id_ = true; + std::string config = getWorkingConfig(); + ElementPtr config_element = Element::fromJSON(config); + ConstElementPtr valid_element = config_element->get("rebind-timer"); + int64_t value = valid_element->intValue(); + valid_element = config_element->get("renew-timer"); + ElementPtr mutable_element = boost::const_pointer_cast<Element>(valid_element); + mutable_element->setValue(value + 1); + + // Parse configuration specified above. + SharedNetwork6Parser parser; + SharedNetwork6Ptr network; + + ASSERT_THROW(network = parser.parse(config_element), DhcpConfigError); + ASSERT_FALSE(network); +} + +// This test verifies that shared network parser for IPv6 works properly +// when renew and rebind timers are equal. +TEST_F(SharedNetwork6ParserTest, parseValidWithEqualRenewRebind) { + IfaceMgrTestConfig ifmgr(true); + + // Use the configuration with interface-id instead of interface parameter. + use_iface_id_ = true; + std::string config = getWorkingConfig(); + ElementPtr config_element = Element::fromJSON(config); + ConstElementPtr valid_element = config_element->get("rebind-timer"); + int64_t value = valid_element->intValue(); + valid_element = config_element->get("renew-timer"); + ElementPtr mutable_element = boost::const_pointer_cast<Element>(valid_element); + mutable_element->setValue(value); + + // Parse configuration specified above. + SharedNetwork6Parser parser; + SharedNetwork6Ptr network; + + ASSERT_NO_THROW(network = parser.parse(config_element)); + ASSERT_TRUE(network); +} + +// This test verifies that error is returned when trying to configure a +// shared network with both interface and interface id. +TEST_F(SharedNetwork6ParserTest, mutuallyExclusiveInterfaceId) { + IfaceMgrTestConfig ifmgr(true); + + // Use the configuration with interface-id instead of interface parameter. + use_iface_id_ = true; + std::string config = getWorkingConfig(); + ElementPtr config_element = Element::fromJSON(config); + + // Add interface which is mutually exclusive with interface-id + config_element->set("interface", Element::create("eth1")); + + // Parse configuration specified above. + SharedNetwork6Parser parser; + EXPECT_THROW(parser.parse(config_element), DhcpConfigError); +} + +// This test verifies that it's possible to specify client-class +// on shared-network level. +TEST_F(SharedNetwork6ParserTest, clientClass) { + IfaceMgrTestConfig ifmgr(true); + + std::string config = getWorkingConfig(); + ElementPtr config_element = Element::fromJSON(config); + + config_element->set("client-class", Element::create("alpha")); + + // Parse configuration specified above. + SharedNetwork6Parser parser; + SharedNetwork6Ptr network; + network = parser.parse(config_element); + ASSERT_TRUE(network); + + EXPECT_EQ("alpha", network->getClientClass().get()); +} + +// This test verifies that it's possible to specify require-client-classes +// on shared-network level. +TEST_F(SharedNetwork6ParserTest, evalClientClasses) { + IfaceMgrTestConfig ifmgr(true); + + std::string config = getWorkingConfig(); + ElementPtr config_element = Element::fromJSON(config); + + ElementPtr class_list = Element::createList(); + class_list->add(Element::create("alpha")); + class_list->add(Element::create("beta")); + config_element->set("require-client-classes", class_list); + + // Parse configuration specified above. + SharedNetwork6Parser parser; + SharedNetwork6Ptr network; + network = parser.parse(config_element); + ASSERT_TRUE(network); + + const ClientClasses& classes = network->getRequiredClasses(); + EXPECT_EQ(2, classes.size()); + EXPECT_EQ("alpha, beta", classes.toText()); +} + +// This test verifies that bad require-client-classes configs raise +// expected errors. +TEST_F(SharedNetwork6ParserTest, badEvalClientClasses) { + IfaceMgrTestConfig ifmgr(true); + + std::string config = getWorkingConfig(); + ElementPtr config_element = Element::fromJSON(config); + + // Element of the list must be strings. + ElementPtr class_list = Element::createList(); + class_list->add(Element::create("alpha")); + class_list->add(Element::create(1234)); + config_element->set("require-client-classes", class_list); + + // Parse configuration specified above. + SharedNetwork6Parser parser; + SharedNetwork6Ptr network; + EXPECT_THROW(network = parser.parse(config_element), DhcpConfigError); + + // Empty class name is forbidden. + class_list = Element::createList(); + class_list->add(Element::create("alpha")); + class_list->add(Element::create("")); + EXPECT_THROW(network = parser.parse(config_element), DhcpConfigError); + + // And of course the list must be a list even the parser can only + // trigger the previous error case... + class_list = Element::createMap(); + EXPECT_THROW(network = parser.parse(config_element), DhcpConfigError); +} + +// This test verifies that v6 parsing of the "relay" element. +// It checks both valid and invalid scenarios. +TEST_F(SharedNetwork6ParserTest, relayInfoTests) { + IfaceMgrTestConfig ifmgr(true); + + + // Create the vector of test scenarios. + std::vector<RelayTest> tests = { + { + "valid ip-address #1", + "{ \"ip-address\": \"2001:db8::1\" }", + true, + { asiolink::IOAddress("2001:db8::1") } + }, + { + "invalid ip-address #1", + "{ \"ip-address\": \"not an address\" }", + false, + { } + }, + { + "invalid ip-address #2", + "{ \"ip-address\": \"192.168.2.1\" }", + false, + { } + }, + { + "valid ip-addresses #1", + "{ \"ip-addresses\": [ ] }", + true, + {} + }, + { + "valid ip-addresses #2", + "{ \"ip-addresses\": [ \"2001:db8::1\" ] }", + true, + { asiolink::IOAddress("2001:db8::1") } + }, + { + "valid ip-addresses #3", + "{ \"ip-addresses\": [ \"2001:db8::1\", \"2001:db8::2\" ] }", + true, + { asiolink::IOAddress("2001:db8::1"), asiolink::IOAddress("2001:db8::2") } + }, + { + "invalid ip-addresses #1", + "{ \"ip-addresses\": [ \"not an address\" ] }", + false, + { } + }, + { + "invalid ip-addresses #2", + "{ \"ip-addresses\": [ \"192.168.1.1\" ] }", + false, + { } + }, + { + "invalid both ip-address and ip-addresses", + "{" + " \"ip-address\": \"2001:db8::1\", " + " \"ip-addresses\": [ \"2001:db8::1\", \"2001:db8::2\" ]" + " }", + false, + { } + }, + { + "invalid neither ip-address nor ip-addresses", + "{}", + false, + { } + } + }; + + // Iterate over the test scenarios, verifying each prescribed + // outcome. + for (auto test = tests.begin(); test != tests.end(); ++test) { + { + SCOPED_TRACE((*test).description_); + relayTest(*test); + } + } +} + +// This test verifies that the optional interface check works as expected. +TEST_F(SharedNetwork6ParserTest, iface) { + // Basic configuration for shared network. + std::string config = getWorkingConfig(); + ElementPtr config_element = Element::fromJSON(config); + + // Parse configuration specified above. + + // The interface check can be disabled. + SharedNetwork6Parser parser_no_check(false); + SharedNetwork6Ptr network; + EXPECT_NO_THROW(network = parser_no_check.parse(config_element)); + ASSERT_TRUE(network); + EXPECT_FALSE(network->getIface().unspecified()); + EXPECT_EQ("eth1961", network->getIface().get()); + + // Retry with the interface check enabled. + SharedNetwork6Parser parser; + EXPECT_THROW(parser.parse(config_element), DhcpConfigError); + + // Configure default test interfaces. + IfaceMgrTestConfig ifmgr(true); + + EXPECT_NO_THROW(network = parser_no_check.parse(config_element)); + ASSERT_TRUE(network); + EXPECT_FALSE(network->getIface().unspecified()); + EXPECT_EQ("eth1961", network->getIface().get()); + + EXPECT_NO_THROW(network = parser.parse(config_element)); + ASSERT_TRUE(network); + EXPECT_FALSE(network->getIface().unspecified()); + EXPECT_EQ("eth1961", network->getIface().get()); +} + +} // end of anonymous namespace |