diff options
Diffstat (limited to 'src/lib/dhcpsrv/tests/dhcp_parsers_unittest.cc')
-rw-r--r-- | src/lib/dhcpsrv/tests/dhcp_parsers_unittest.cc | 3557 |
1 files changed, 3557 insertions, 0 deletions
diff --git a/src/lib/dhcpsrv/tests/dhcp_parsers_unittest.cc b/src/lib/dhcpsrv/tests/dhcp_parsers_unittest.cc new file mode 100644 index 0000000..de9f6a2 --- /dev/null +++ b/src/lib/dhcpsrv/tests/dhcp_parsers_unittest.cc @@ -0,0 +1,3557 @@ +// Copyright (C) 2012-2023 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 <cc/command_interpreter.h> +#include <cc/data.h> +#include <cc/simple_parser.h> +#include <dhcp/option.h> +#include <dhcp/option_custom.h> +#include <dhcp/option_int.h> +#include <dhcp/option_string.h> +#include <dhcp/option4_addrlst.h> +#include <dhcp/option6_addrlst.h> +#include <dhcp/tests/iface_mgr_test_config.h> +#include <dhcpsrv/cfgmgr.h> +#include <dhcpsrv/subnet.h> +#include <dhcpsrv/cfg_mac_source.h> +#include <dhcpsrv/flq_allocator.h> +#include <dhcpsrv/flq_allocation_state.h> +#include <dhcpsrv/iterative_allocator.h> +#include <dhcpsrv/iterative_allocation_state.h> +#include <dhcpsrv/parsers/dhcp_parsers.h> +#include <dhcpsrv/parsers/option_data_parser.h> +#include <dhcpsrv/parsers/shared_network_parser.h> +#include <dhcpsrv/parsers/shared_networks_list_parser.h> +#include <dhcpsrv/random_allocator.h> +#include <dhcpsrv/random_allocation_state.h> +#include <dhcpsrv/tests/test_libraries.h> +#include <dhcpsrv/testutils/config_result_check.h> +#include <exceptions/exceptions.h> +#include <hooks/hooks_parser.h> +#include <hooks/hooks_manager.h> +#include <testutils/test_to_element.h> + +#include <gtest/gtest.h> +#include <boost/foreach.hpp> +#include <boost/pointer_cast.hpp> + +#include <map> +#include <string> + +using namespace std; +using namespace isc; +using namespace isc::asiolink; +using namespace isc::config; +using namespace isc::data; +using namespace isc::dhcp; +using namespace isc::dhcp::test; +using namespace isc::hooks; +using namespace isc::test; + +namespace { + +/// @brief DHCP Parser test fixture class +class DhcpParserTest : public ::testing::Test { +public: + /// @brief Constructor + DhcpParserTest() { + resetIfaceCfg(); + } + + /// @brief Destructor. + virtual ~DhcpParserTest() { + resetIfaceCfg(); + } + + /// @brief Resets selection of the interfaces from previous tests. + void resetIfaceCfg() { + CfgMgr::instance().clear(); + } +}; + +/// Verifies the code that parses mac sources and adds them to CfgMgr +TEST_F(DhcpParserTest, MacSources) { + + // That's an equivalent of the following snippet: + // "mac-sources: [ \"duid\", \"ipv6\" ]"; + ElementPtr values = Element::createList(); + values->add(Element::create("duid")); + values->add(Element::create("ipv6-link-local")); + + // Let's grab server configuration from CfgMgr + SrvConfigPtr cfg = CfgMgr::instance().getStagingCfg(); + ASSERT_TRUE(cfg); + CfgMACSource& sources = cfg->getMACSources(); + + // This should parse the configuration and check that it doesn't throw. + MACSourcesListConfigParser parser; + EXPECT_NO_THROW(parser.parse(sources, values)); + + // Finally, check the sources that were configured + CfgMACSources configured_sources = cfg->getMACSources().get(); + ASSERT_EQ(2, configured_sources.size()); + EXPECT_EQ(HWAddr::HWADDR_SOURCE_DUID, configured_sources[0]); + EXPECT_EQ(HWAddr::HWADDR_SOURCE_IPV6_LINK_LOCAL, configured_sources[1]); +} + +/// @brief Check MACSourcesListConfigParser rejecting empty list +/// +/// Verifies that the code rejects an empty mac-sources list. +TEST_F(DhcpParserTest, MacSourcesEmpty) { + + // That's an equivalent of the following snippet: + // "mac-sources: [ \"duid\", \"ipv6\" ]"; + ElementPtr values = Element::createList(); + + // Let's grab server configuration from CfgMgr + SrvConfigPtr cfg = CfgMgr::instance().getStagingCfg(); + ASSERT_TRUE(cfg); + CfgMACSource& sources = cfg->getMACSources(); + + // This should throw, because if specified, at least one MAC source + // has to be specified. + MACSourcesListConfigParser parser; + EXPECT_THROW(parser.parse(sources, values), DhcpConfigError); +} + +/// @brief Check MACSourcesListConfigParser rejecting empty list +/// +/// Verifies that the code rejects fake mac source. +TEST_F(DhcpParserTest, MacSourcesBogus) { + + // That's an equivalent of the following snippet: + // "mac-sources: [ \"duid\", \"ipv6\" ]"; + ElementPtr values = Element::createList(); + values->add(Element::create("from-ebay")); + values->add(Element::create("just-guess-it")); + + // Let's grab server configuration from CfgMgr + SrvConfigPtr cfg = CfgMgr::instance().getStagingCfg(); + ASSERT_TRUE(cfg); + CfgMACSource& sources = cfg->getMACSources(); + + // This should throw, because these are not valid sources. + MACSourcesListConfigParser parser; + EXPECT_THROW(parser.parse(sources, values), DhcpConfigError); +} + +/// Verifies the code that properly catches duplicate entries +/// in mac-sources definition. +TEST_F(DhcpParserTest, MacSourcesDuplicate) { + + // That's an equivalent of the following snippet: + // "mac-sources: [ \"duid\", \"ipv6\" ]"; + ElementPtr values = Element::createList(); + values->add(Element::create("ipv6-link-local")); + values->add(Element::create("duid")); + values->add(Element::create("duid")); + values->add(Element::create("duid")); + + // Let's grab server configuration from CfgMgr + SrvConfigPtr cfg = CfgMgr::instance().getStagingCfg(); + ASSERT_TRUE(cfg); + CfgMACSource& sources = cfg->getMACSources(); + + // This should parse the configuration and check that it throws. + MACSourcesListConfigParser parser; + EXPECT_THROW(parser.parse(sources, values), DhcpConfigError); +} + + +/// @brief Test Fixture class which provides basic structure for testing +/// configuration parsing. This is essentially the same structure provided +/// by dhcp servers. +class ParseConfigTest : public ::testing::Test { +public: + /// @brief Constructor + ParseConfigTest() + :family_(AF_INET6) { + reset_context(); + } + + ~ParseConfigTest() { + reset_context(); + CfgMgr::instance().clear(); + } + + /// @brief Parses a configuration. + /// + /// Parse the given configuration, populating the context storage with + /// the parsed elements. + /// + /// @param config_set is the set of elements to parse. + /// @param v6 boolean flag indicating if this is a DHCPv6 configuration. + /// @return returns an ConstElementPtr containing the numeric result + /// code and outcome comment. + isc::data::ConstElementPtr + parseElementSet(isc::data::ConstElementPtr config_set, bool v6) { + // Answer will hold the result. + ConstElementPtr answer; + if (!config_set) { + answer = isc::config::createAnswer(CONTROL_RESULT_ERROR, + string("Can't parse NULL config")); + return (answer); + } + + ConfigPair config_pair; + try { + // Iterate over the config elements. + const std::map<std::string, ConstElementPtr>& values_map = + config_set->mapValue(); + BOOST_FOREACH(config_pair, values_map) { + + // These are the simple parsers. No need to go through + // the ParserPtr hooplas with them. + if ((config_pair.first == "option-data") || + (config_pair.first == "option-def") || + (config_pair.first == "dhcp-ddns")) { + continue; + } + + // We also don't care about the default values that may be been + // inserted + if ((config_pair.first == "preferred-lifetime") || + (config_pair.first == "valid-lifetime") || + (config_pair.first == "renew-timer") || + (config_pair.first == "rebind-timer")) { + continue; + } + + // Save global hostname-char-*. + if ((config_pair.first == "hostname-char-set") || + (config_pair.first == "hostname-char-replacement")) { + CfgMgr::instance().getStagingCfg()->addConfiguredGlobal(config_pair.first, + config_pair.second); + continue; + } + + if (config_pair.first == "hooks-libraries") { + HooksLibrariesParser hook_parser; + HooksConfig& libraries = + CfgMgr::instance().getStagingCfg()->getHooksConfig(); + hook_parser.parse(libraries, config_pair.second); + libraries.verifyLibraries(config_pair.second->getPosition(), false); + libraries.loadLibraries(false); + continue; + } + } + + // The option definition parser is the next one to be run. + std::map<std::string, ConstElementPtr>::const_iterator + def_config = values_map.find("option-def"); + if (def_config != values_map.end()) { + + CfgOptionDefPtr cfg_def = CfgMgr::instance().getStagingCfg()->getCfgOptionDef(); + OptionDefListParser def_list_parser(family_); + def_list_parser.parse(cfg_def, def_config->second); + } + + // The option values parser is the next one to be run. + std::map<std::string, ConstElementPtr>::const_iterator + option_config = values_map.find("option-data"); + if (option_config != values_map.end()) { + CfgOptionPtr cfg_option = CfgMgr::instance().getStagingCfg()->getCfgOption(); + + OptionDataListParser option_list_parser(family_); + option_list_parser.parse(cfg_option, option_config->second); + } + + // The dhcp-ddns parser is the next one to be run. + std::map<std::string, ConstElementPtr>::const_iterator + d2_client_config = values_map.find("dhcp-ddns"); + if (d2_client_config != values_map.end()) { + // Used to be done by parser commit + D2ClientConfigParser parser; + D2ClientConfigPtr cfg = parser.parse(d2_client_config->second); + cfg->validateContents(); + CfgMgr::instance().setD2ClientConfig(cfg); + } + + std::map<std::string, ConstElementPtr>::const_iterator + subnets4_config = values_map.find("subnet4"); + if (subnets4_config != values_map.end()) { + auto srv_config = CfgMgr::instance().getStagingCfg(); + Subnets4ListConfigParser parser; + parser.parse(srv_config, subnets4_config->second); + } + + std::map<std::string, ConstElementPtr>::const_iterator + subnets6_config = values_map.find("subnet6"); + if (subnets6_config != values_map.end()) { + auto srv_config = CfgMgr::instance().getStagingCfg(); + Subnets6ListConfigParser parser; + parser.parse(srv_config, subnets6_config->second); + } + + std::map<std::string, ConstElementPtr>::const_iterator + networks_config = values_map.find("shared-networks"); + if (networks_config != values_map.end()) { + if (v6) { + auto cfg_shared_networks = CfgMgr::instance().getStagingCfg()->getCfgSharedNetworks6(); + SharedNetworks6ListParser parser; + parser.parse(cfg_shared_networks, networks_config->second); + + } else { + auto cfg_shared_networks = CfgMgr::instance().getStagingCfg()->getCfgSharedNetworks4(); + SharedNetworks4ListParser parser; + parser.parse(cfg_shared_networks, networks_config->second); + } + } + + // Everything was fine. Configuration is successful. + answer = isc::config::createAnswer(CONTROL_RESULT_SUCCESS, "Configuration committed."); + } catch (const isc::Exception& ex) { + answer = isc::config::createAnswer(CONTROL_RESULT_ERROR, + string("Configuration parsing failed: ") + ex.what()); + + } catch (...) { + answer = isc::config::createAnswer(CONTROL_RESULT_ERROR, + string("Configuration parsing failed")); + } + + return (answer); + } + + /// @brief DHCP-specific method that sets global, and option specific defaults + /// + /// This method sets the defaults in the global scope, in option definitions, + /// and in option data. + /// + /// @param global pointer to the Element tree that holds configuration + /// @param global_defaults array with global default values + /// @param option_defaults array with option-data default values + /// @param option_def_defaults array with default values for option definitions + /// @return number of default values inserted. + size_t setAllDefaults(isc::data::ElementPtr global, + const SimpleDefaults& global_defaults, + const SimpleDefaults& option_defaults, + const SimpleDefaults& option_def_defaults) { + size_t cnt = 0; + // Set global defaults first. + cnt = SimpleParser::setDefaults(global, global_defaults); + + // Now set option definition defaults for each specified option definition + ConstElementPtr option_defs = global->get("option-def"); + if (option_defs) { + BOOST_FOREACH(ElementPtr single_def, option_defs->listValue()) { + cnt += SimpleParser::setDefaults(single_def, option_def_defaults); + } + } + + ConstElementPtr options = global->get("option-data"); + if (options) { + BOOST_FOREACH(ElementPtr single_option, options->listValue()) { + cnt += SimpleParser::setDefaults(single_option, option_defaults); + } + } + + return (cnt); + } + + /// This table defines default values for option definitions in DHCPv6 + static const SimpleDefaults OPTION6_DEF_DEFAULTS; + + /// This table defines default values for option definitions in DHCPv4 + static const SimpleDefaults OPTION4_DEF_DEFAULTS; + + /// This table defines default values for options in DHCPv6 + static const SimpleDefaults OPTION6_DEFAULTS; + + /// This table defines default values for options in DHCPv4 + static const SimpleDefaults OPTION4_DEFAULTS; + + /// This table defines default values for both DHCPv4 and DHCPv6 + static const SimpleDefaults GLOBAL6_DEFAULTS; + + /// @brief sets all default values for DHCPv4 and DHCPv6 + /// + /// This function largely duplicates what SimpleParser4 and SimpleParser6 classes + /// provide. However, since there are tons of unit-tests in dhcpsrv that need + /// this functionality and there are good reasons to keep those classes in + /// src/bin/dhcp{4,6}, the most straightforward way is to simply copy the + /// minimum code here. Hence this method. + /// + /// @todo - TKM, I think this is fairly hideous and we should figure out a + /// a way to not have to replicate in this fashion. It may be minimum code + /// now, but it won't be fairly soon. + /// + /// @param config configuration structure to be filled with default values + /// @param v6 true = DHCPv6, false = DHCPv4 + void setAllDefaults(ElementPtr config, bool v6) { + if (v6) { + setAllDefaults(config, GLOBAL6_DEFAULTS, OPTION6_DEFAULTS, + OPTION6_DEF_DEFAULTS); + } else { + setAllDefaults(config, GLOBAL6_DEFAULTS, OPTION4_DEFAULTS, + OPTION4_DEF_DEFAULTS); + } + + /// D2 client configuration code is in this library + ConstElementPtr d2_client = config->get("dhcp-ddns"); + if (d2_client) { + D2ClientConfigParser::setAllDefaults(d2_client); + } + } + + /// @brief Convenience method for parsing a configuration + /// + /// Given a configuration string, convert it into Elements + /// and parse them. + /// @param config is the configuration string to parse + /// @param v6 boolean value indicating if this is DHCPv6 configuration. + /// @param set_defaults boolean value indicating if the defaults should + /// be derived before parsing the configuration. + /// + /// @return returns 0 if the configuration parsed successfully, + /// non-zero otherwise failure. + int parseConfiguration(const std::string& config, bool v6 = false, + bool set_defaults = true) { + int rcode_ = 1; + // Turn config into elements. + // Test json just to make sure its valid. + ElementPtr json = Element::fromJSON(config); + EXPECT_TRUE(json); + if (json) { + if (set_defaults) { + setAllDefaults(json, v6); + } + + ConstElementPtr status = parseElementSet(json, v6); + ConstElementPtr comment = parseAnswer(rcode_, status); + error_text_ = comment->stringValue(); + // If error was reported, the error string should contain + // position of the data element which caused failure. + if (rcode_ != 0) { + EXPECT_TRUE(errorContainsPosition(status, "<string>")) + << "error text: " << error_text_; + } + } + + return (rcode_); + } + + /// @brief Find an option for a given space and code within the parser + /// context. + /// @param space is the space name of the desired option. + /// @param code is the numeric "type" of the desired option. + /// @return returns an OptionPtr which points to the found + /// option or is empty. + /// ASSERT_ tests don't work inside functions that return values + OptionPtr getOptionPtr(std::string space, uint32_t code) + { + OptionPtr option_ptr; + OptionContainerPtr options = CfgMgr::instance().getStagingCfg()-> + getCfgOption()->getAll(space); + // Should always be able to get options list even if it is empty. + EXPECT_TRUE(options); + if (options) { + // Attempt to find desired option. + const OptionContainerTypeIndex& idx = options->get<1>(); + const OptionContainerTypeRange& range = idx.equal_range(code); + int cnt = std::distance(range.first, range.second); + EXPECT_EQ(1, cnt); + if (cnt == 1) { + OptionDescriptor desc = *(idx.begin()); + option_ptr = desc.option_; + EXPECT_TRUE(option_ptr); + } + } + + return (option_ptr); + } + + /// @brief Wipes the contents of the context to allowing another parsing + /// during a given test if needed. + /// @param family protocol family to use during the test, defaults + /// to AF_INET6 + void reset_context(uint16_t family = AF_INET6){ + // Note set context universe to V6 as it has to be something. + CfgMgr::instance().clear(); + family_ = family; + + // Ensure no hooks libraries are loaded. + EXPECT_TRUE(HooksManager::unloadLibraries()); + + // Set it to minimal, disabled config + D2ClientConfigPtr tmp(new D2ClientConfig()); + CfgMgr::instance().setD2ClientConfig(tmp); + } + + /// Allows the tests to interrogate the state of the libraries (if required). + const isc::hooks::HookLibsCollection& getLibraries() { + return (CfgMgr::instance().getStagingCfg()->getHooksConfig().get()); + } + + /// @brief specifies IP protocol family (AF_INET or AF_INET6) + uint16_t family_; + + /// @brief Error string if the parsing failed + std::string error_text_; +}; + +/// This table defines default values for option definitions in DHCPv6 +const SimpleDefaults ParseConfigTest::OPTION6_DEF_DEFAULTS = { + { "record-types", Element::string, ""}, + { "space", Element::string, "dhcp6"}, + { "array", Element::boolean, "false"}, + { "encapsulate", Element::string, "" } +}; + +/// This table defines default values for option definitions in DHCPv4 +const SimpleDefaults ParseConfigTest::OPTION4_DEF_DEFAULTS = { + { "record-types", Element::string, ""}, + { "space", Element::string, "dhcp4"}, + { "array", Element::boolean, "false"}, + { "encapsulate", Element::string, "" } +}; + +/// This table defines default values for options in DHCPv6 +const SimpleDefaults ParseConfigTest::OPTION6_DEFAULTS = { + { "space", Element::string, "dhcp6"}, + { "csv-format", Element::boolean, "true"}, + { "always-send", Element::boolean, "false"}, + { "never-send", Element::boolean, "false"} +}; + +/// This table defines default values for options in DHCPv4 +const SimpleDefaults ParseConfigTest::OPTION4_DEFAULTS = { + { "space", Element::string, "dhcp4"}, + { "csv-format", Element::boolean, "true"}, + { "always-send", Element::boolean, "false"}, + { "never-send", Element::boolean, "false"} +}; + +/// This table defines default values for both DHCPv4 and DHCPv6 +const SimpleDefaults ParseConfigTest::GLOBAL6_DEFAULTS = { + { "renew-timer", Element::integer, "900" }, + { "rebind-timer", Element::integer, "1800" }, + { "preferred-lifetime", Element::integer, "3600" }, + { "valid-lifetime", Element::integer, "7200" } +}; + +/// @brief Option configuration class +/// +/// This class handles option-def and option-data which can be recovered +/// using the toElement() method +class CfgOptionsTest : public CfgToElement { +public: + /// @brief Constructor + /// + /// @param cfg the server configuration where to get option-{def,data} + CfgOptionsTest(SrvConfigPtr cfg) : + cfg_option_def_(cfg->getCfgOptionDef()), + cfg_option_(cfg->getCfgOption()) { } + + /// @brief Unparse a configuration object + /// + /// @return a pointer to unparsed configuration (a map with + /// not empty option-def and option-data lists) + ElementPtr toElement() const { + ElementPtr result = Element::createMap(); + // Set option-def + ConstElementPtr option_def = cfg_option_def_->toElement(); + if (!option_def->empty()) { + result->set("option-def", option_def); + } + // Set option-data + ConstElementPtr option_data = cfg_option_->toElement(); + if (!option_data->empty()) { + result->set("option-data", option_data); + } + return (result); + } + + /// @brief Run a toElement test (Element version) + /// + /// Use the runToElementTest template but add defaults to the config + /// + /// @param family the address family + /// @param config the expected result without defaults + void runCfgOptionsTest(uint16_t family, ConstElementPtr expected) { + ConstElementPtr option_def = expected->get("option-def"); + if (option_def) { + SimpleParser::setListDefaults(option_def, + family == AF_INET ? + ParseConfigTest::OPTION4_DEF_DEFAULTS : + ParseConfigTest::OPTION6_DEF_DEFAULTS); + } + ConstElementPtr option_data = expected->get("option-data"); + if (option_data) { + SimpleParser::setListDefaults(option_data, + family == AF_INET ? + ParseConfigTest::OPTION4_DEFAULTS : + ParseConfigTest::OPTION6_DEFAULTS); + } + runToElementTest<CfgOptionsTest>(expected, *this); + } + + /// @brief Run a toElement test + /// + /// Use the runToElementTest template but add defaults to the config + /// + /// @param family the address family + /// @param expected the expected result without defaults + void runCfgOptionsTest(uint16_t family, std::string config) { + ConstElementPtr json; + ASSERT_NO_THROW(json = Element::fromJSON(config)) << config; + runCfgOptionsTest(family, json); + } + +private: + /// @brief Pointer to option definitions configuration. + CfgOptionDefPtr cfg_option_def_; + + /// @brief Reference to options (data) configuration. + CfgOptionPtr cfg_option_; +}; + +/// @brief Check basic parsing of option definitions. +/// +/// Note that this tests basic operation of the OptionDefinitionListParser and +/// OptionDefinitionParser. It uses a simple configuration consisting of +/// one definition and verifies that it is parsed and committed to storage +/// correctly. +TEST_F(ParseConfigTest, basicOptionDefTest) { + + // Configuration string. + std::string config = + "{ \"option-def\": [ {" + " \"name\": \"foo\"," + " \"code\": 100," + " \"type\": \"ipv4-address\"," + " \"array\": false," + " \"record-types\": \"\"," + " \"space\": \"isc\"," + " \"encapsulate\": \"\"" + " } ]" + "}"; + + // Verify that the configuration string parses. + int rcode = parseConfiguration(config); + ASSERT_EQ(0, rcode); + + + // Verify that the option definition can be retrieved. + OptionDefinitionPtr def = + CfgMgr::instance().getStagingCfg()->getCfgOptionDef()->get("isc", 100); + ASSERT_TRUE(def); + + // Verify that the option definition is correct. + EXPECT_EQ("foo", def->getName()); + EXPECT_EQ(100, def->getCode()); + EXPECT_FALSE(def->getArrayType()); + EXPECT_EQ(OPT_IPV4_ADDRESS_TYPE, def->getType()); + EXPECT_TRUE(def->getEncapsulatedSpace().empty()); + + // Check if libdhcp++ runtime options have been updated. + OptionDefinitionPtr def_libdhcp = LibDHCP::getRuntimeOptionDef("isc", 100); + ASSERT_TRUE(def_libdhcp); + + // The LibDHCP should return a separate instance of the option definition + // but the values should be equal. + EXPECT_TRUE(def_libdhcp != def); + EXPECT_TRUE(*def_libdhcp == *def); + + // Check if it can be unparsed. + CfgOptionsTest cfg(CfgMgr::instance().getStagingCfg()); + cfg.runCfgOptionsTest(family_, config); +} + +/// @brief Check minimal parsing of option definitions. +/// +/// Same than basic but without optional parameters set to their default. +TEST_F(ParseConfigTest, minimalOptionDefTest) { + + // Configuration string. + std::string config = + "{ \"option-def\": [ {" + " \"name\": \"foo\"," + " \"code\": 100," + " \"type\": \"ipv4-address\"," + " \"space\": \"isc\"" + " } ]" + "}"; + + // Verify that the configuration string parses. + int rcode = parseConfiguration(config); + ASSERT_EQ(0, rcode); + + + // Verify that the option definition can be retrieved. + OptionDefinitionPtr def = + CfgMgr::instance().getStagingCfg()->getCfgOptionDef()->get("isc", 100); + ASSERT_TRUE(def); + + // Verify that the option definition is correct. + EXPECT_EQ("foo", def->getName()); + EXPECT_EQ(100, def->getCode()); + EXPECT_FALSE(def->getArrayType()); + EXPECT_EQ(OPT_IPV4_ADDRESS_TYPE, def->getType()); + EXPECT_TRUE(def->getEncapsulatedSpace().empty()); + + // Check if it can be unparsed. + CfgOptionsTest cfg(CfgMgr::instance().getStagingCfg()); + cfg.runCfgOptionsTest(family_, config); +} + +/// @brief Check parsing of option definitions using default dhcp6 space. +/// +/// Same than minimal but using the fact the default universe is V6 +/// so the default space is dhcp6 +TEST_F(ParseConfigTest, defaultSpaceOptionDefTest) { + + // Configuration string. + std::string config = + "{ \"option-def\": [ {" + " \"name\": \"foo\"," + " \"code\": 10000," + " \"type\": \"ipv6-address\"" + " } ]" + "}"; + + // Verify that the configuration string parses. + int rcode = parseConfiguration(config, true); + ASSERT_EQ(0, rcode); + + + // Verify that the option definition can be retrieved. + OptionDefinitionPtr def = + CfgMgr::instance().getStagingCfg()->getCfgOptionDef()->get(DHCP6_OPTION_SPACE, 10000); + ASSERT_TRUE(def); + + // Verify that the option definition is correct. + EXPECT_EQ("foo", def->getName()); + EXPECT_EQ(10000, def->getCode()); + EXPECT_FALSE(def->getArrayType()); + EXPECT_EQ(OPT_IPV6_ADDRESS_TYPE, def->getType()); + EXPECT_TRUE(def->getEncapsulatedSpace().empty()); + + // Check if it can be unparsed. + CfgOptionsTest cfg(CfgMgr::instance().getStagingCfg()); + cfg.runCfgOptionsTest(family_, config); +} + +/// @brief Check parsing of option definitions using invalid code fails. +TEST_F(ParseConfigTest, badCodeOptionDefTest) { + + { + SCOPED_TRACE("negative code"); + std::string config = + "{ \"option-def\": [ {" + " \"name\": \"negative\"," + " \"code\": -1," + " \"type\": \"ipv6-address\"," + " \"space\": \"isc\"" + " } ]" + "}"; + + int rcode = parseConfiguration(config, true); + ASSERT_NE(0, rcode); + } + + { + SCOPED_TRACE("out of range code (v6)"); + std::string config = + "{ \"option-def\": [ {" + " \"name\": \"hundred-thousands\"," + " \"code\": 100000," + " \"type\": \"ipv6-address\"," + " \"space\": \"isc\"" + " } ]" + "}"; + + int rcode = parseConfiguration(config, true); + ASSERT_NE(0, rcode); + } + + { + SCOPED_TRACE("out of range code (v4)"); + family_ = AF_INET; // Switch to DHCPv4. + + std::string config = + "{ \"option-def\": [ {" + " \"name\": \"thousand\"," + " \"code\": 1000," + " \"type\": \"ip-address\"," + " \"space\": \"isc\"" + " } ]" + "}"; + + int rcode = parseConfiguration(config, false); + ASSERT_NE(0, rcode); + } + + { + SCOPED_TRACE("conflict with PAD"); + family_ = AF_INET; // Switch to DHCPv4. + + std::string config = + "{ \"option-def\": [ {" + " \"name\": \"zero\"," + " \"code\": 0," + " \"type\": \"ip-address\"," + " \"space\": \"dhcp4\"" + " } ]" + "}"; + + int rcode = parseConfiguration(config, false); + ASSERT_NE(0, rcode); + } + + { + SCOPED_TRACE("conflict with END"); + family_ = AF_INET; // Switch to DHCPv4. + + std::string config = + "{ \"option-def\": [ {" + " \"name\": \"max\"," + " \"code\": 255," + " \"type\": \"ip-address\"," + " \"space\": \"dhcp4\"" + " } ]" + "}"; + + int rcode = parseConfiguration(config, false); + ASSERT_NE(0, rcode); + } + + { + SCOPED_TRACE("conflict with reserved"); + + std::string config = + "{ \"option-def\": [ {" + " \"name\": \"zero\"," + " \"code\": 0," + " \"type\": \"ipv6-address\"," + " \"space\": \"dhcp6\"" + " } ]" + "}"; + + int rcode = parseConfiguration(config, false); + ASSERT_NE(0, rcode); + } +} + +/// @brief Check parsing of option definitions using invalid space fails. +TEST_F(ParseConfigTest, badSpaceOptionDefTest) { + + // Configuration string. + std::string config = + "{ \"option-def\": [ {" + " \"name\": \"foo\"," + " \"code\": 100," + " \"type\": \"ipv6-address\"," + " \"space\": \"-1\"" + " } ]" + "}"; + + // Verify that the configuration string does not parse. + int rcode = parseConfiguration(config, true); + ASSERT_NE(0, rcode); +} + +/// @brief Check basic parsing of options. +/// +/// Note that this tests basic operation of the OptionDataListParser and +/// OptionDataParser. It uses a simple configuration consisting of one +/// one definition and matching option data. It verifies that the option +/// is parsed and committed to storage correctly. +TEST_F(ParseConfigTest, basicOptionDataTest) { + + // Configuration string. + std::string config = + "{ \"option-def\": [ {" + " \"name\": \"foo\"," + " \"code\": 100," + " \"type\": \"ipv4-address\"," + " \"space\": \"isc\"" + " } ], " + " \"option-data\": [ {" + " \"name\": \"foo\"," + " \"space\": \"isc\"," + " \"code\": 100," + " \"data\": \"192.0.2.0\"," + " \"csv-format\": true," + " \"always-send\": false" + " } ]" + "}"; + + // Verify that the configuration string parses. + int rcode = parseConfiguration(config); + ASSERT_EQ(0, rcode); + + // Verify that the option can be retrieved. + OptionPtr opt_ptr = getOptionPtr("isc", 100); + ASSERT_TRUE(opt_ptr); + + // Verify that the option data is correct. + std::string val = "type=00100, len=00004: 192.0.2.0 (ipv4-address)"; + + EXPECT_EQ(val, opt_ptr->toText()); + + // Check if it can be unparsed. + CfgOptionsTest cfg(CfgMgr::instance().getStagingCfg()); + cfg.runCfgOptionsTest(family_, config); +} + +/// @brief Check parsing of options with code 0. +TEST_F(ParseConfigTest, optionDataTest0) { + + // Configuration string. + std::string config = + "{ \"option-def\": [ {" + " \"name\": \"foo\"," + " \"code\": 0," + " \"type\": \"ipv4-address\"," + " \"space\": \"isc\"" + " } ], " + " \"option-data\": [ {" + " \"name\": \"foo\"," + " \"space\": \"isc\"," + " \"code\": 0," + " \"data\": \"192.0.2.0\"," + " \"csv-format\": true," + " \"always-send\": false" + " } ]" + "}"; + + // Verify that the configuration string parses. + int rcode = parseConfiguration(config); + ASSERT_EQ(0, rcode); + + // Verify that the option can be retrieved. + OptionPtr opt_ptr = getOptionPtr("isc", 0); + ASSERT_TRUE(opt_ptr); + + // Verify that the option data is correct. + std::string val = "type=00000, len=00004: 192.0.2.0 (ipv4-address)"; + + EXPECT_EQ(val, opt_ptr->toText()); + + // Check if it can be unparsed. + CfgOptionsTest cfg(CfgMgr::instance().getStagingCfg()); + cfg.runCfgOptionsTest(family_, config); +} + +/// @brief Check parsing of options with code 255. +TEST_F(ParseConfigTest, optionDataTest255) { + + // Configuration string. + std::string config = + "{ \"option-def\": [ {" + " \"name\": \"foo\"," + " \"code\": 255," + " \"type\": \"ipv4-address\"," + " \"space\": \"isc\"" + " } ], " + " \"option-data\": [ {" + " \"name\": \"foo\"," + " \"space\": \"isc\"," + " \"code\": 255," + " \"data\": \"192.0.2.0\"," + " \"csv-format\": true," + " \"always-send\": false" + " } ]" + "}"; + + // Verify that the configuration string parses. + int rcode = parseConfiguration(config); + ASSERT_EQ(0, rcode); + + // Verify that the option can be retrieved. + OptionPtr opt_ptr = getOptionPtr("isc", 255); + ASSERT_TRUE(opt_ptr); + + // Verify that the option data is correct. + std::string val = "type=00255, len=00004: 192.0.2.0 (ipv4-address)"; + + EXPECT_EQ(val, opt_ptr->toText()); + + // Check if it can be unparsed. + CfgOptionsTest cfg(CfgMgr::instance().getStagingCfg()); + cfg.runCfgOptionsTest(family_, config); +} + +/// @brief Check minimal parsing of options. +/// +/// Same than basic but without optional parameters set to their default. +TEST_F(ParseConfigTest, minimalOptionDataTest) { + + // Configuration string. + std::string config = + "{ \"option-def\": [ {" + " \"name\": \"foo\"," + " \"code\": 100," + " \"type\": \"ipv4-address\"," + " \"space\": \"isc\"" + " } ], " + " \"option-data\": [ {" + " \"name\": \"foo\"," + " \"space\": \"isc\"," + " \"data\": \"192.0.2.0\"" + " } ]" + "}"; + + // Verify that the configuration string parses. + int rcode = parseConfiguration(config); + ASSERT_EQ(0, rcode); + + // Verify that the option can be retrieved. + OptionPtr opt_ptr = getOptionPtr("isc", 100); + ASSERT_TRUE(opt_ptr); + + // Verify that the option data is correct. + std::string val = "type=00100, len=00004: 192.0.2.0 (ipv4-address)"; + + EXPECT_EQ(val, opt_ptr->toText()); + + ElementPtr expected = Element::fromJSON(config); + ElementPtr opt_data = expected->get("option-data")->getNonConst(0); + opt_data->set("code", Element::create(100)); + CfgOptionsTest cfg(CfgMgr::instance().getStagingCfg()); + cfg.runCfgOptionsTest(family_, expected); +} + +/// @brief Check parsing of unknown options fails. +TEST_F(ParseConfigTest, unknownOptionDataTest) { + + // Configuration string. + std::string config = + "{ \"option-data\": [ {" + " \"name\": \"foo\"," + " \"data\": \"01\"," + " \"space\": \"bar\"" + " } ]" + "}"; + + // Verify that the configuration string does not parse. + int rcode = parseConfiguration(config, true); + ASSERT_NE(0, rcode); +} + +/// @brief Check parsing of option data using invalid code fails. +TEST_F(ParseConfigTest, badCodeOptionDataTest) { + + { + SCOPED_TRACE("negative code"); + std::string config = + "{ \"option-data\": [ {" + " \"code\": -1," + " \"data\": \"01\"," + " \"space\": \"isc\"" + " } ]" + "}"; + + int rcode = parseConfiguration(config, true); + ASSERT_NE(0, rcode); + } + + { + SCOPED_TRACE("out of range code (v6)"); + std::string config = + "{ \"option-data\": [ {" + " \"code\": 100000," + " \"data\": \"01\"," + " \"space\": \"isc\"" + " } ]" + "}"; + + int rcode = parseConfiguration(config, true); + ASSERT_NE(0, rcode); + } + + { + SCOPED_TRACE("out of range code (v4)"); + family_ = AF_INET; // Switch to DHCPv4. + + std::string config = + "{ \"option-data\": [ {" + " \"code\": 1000," + " \"data\": \"01\"," + " \"space\": \"isc\"" + " } ]" + "}"; + + int rcode = parseConfiguration(config, false); + ASSERT_NE(0, rcode); + } + + { + SCOPED_TRACE("conflict with PAD"); + family_ = AF_INET; // Switch to DHCPv4. + + std::string config = + "{ \"option-data\": [ {" + " \"code\": 0," + " \"data\": \"01\"," + " \"csv-format\": false," + " \"space\": \"dhcp4\"" + " } ]" + "}"; + + int rcode = parseConfiguration(config, false); + ASSERT_NE(0, rcode); + } + + { + SCOPED_TRACE("conflict with END"); + family_ = AF_INET; // Switch to DHCPv4. + + std::string config = + "{ \"option-data\": [ {" + " \"code\": 255," + " \"data\": \"01\"," + " \"csv-format\": false," + " \"space\": \"dhcp4\"" + " } ]" + "}"; + + int rcode = parseConfiguration(config, false); + ASSERT_NE(0, rcode); + } + + { + SCOPED_TRACE("conflict with reserved"); + family_ = AF_INET6; // Switch to DHCPv6. + + std::string config = + "{ \"option-data\": [ {" + " \"code\": 0," + " \"data\": \"01\"," + " \"csv-format\": false," + " \"space\": \"dhcp6\"" + " } ]" + "}"; + + int rcode = parseConfiguration(config, false); + ASSERT_NE(0, rcode); + } +} + +/// @brief Check parsing of options with invalid space fails. +TEST_F(ParseConfigTest, badSpaceOptionDataTest) { + + // Configuration string. + std::string config = + "{ \"option-data\": [ {" + " \"code\": 100," + " \"data\": \"01\"," + " \"space\": \"-1\"" + " } ]" + "}"; + + // Verify that the configuration string does not parse. + int rcode = parseConfiguration(config, true); + ASSERT_NE(0, rcode); +} + +/// @brief Check parsing of options with escape characters. +/// +/// Note that this tests basic operation of the OptionDataListParser and +/// OptionDataParser. It uses a simple configuration consisting of one +/// one definition and matching option data. It verifies that the option +/// is parsed and committed to storage correctly and that its content +/// has the actual character (e.g. an actual backslash, not double backslash). +TEST_F(ParseConfigTest, escapedOptionDataTest) { + + family_ = AF_INET; + + // We need to use double escapes here. The first backslash will + // be consumed by C++ preprocessor, so the actual string will + // have two backslash characters: \\SMSBoot\\x64\\wdsnbp.com. + // + std::string config = + "{\"option-data\": [ {" + " \"name\": \"boot-file-name\"," + " \"data\": \"\\\\SMSBoot\\\\x64\\\\wdsnbp.com\"" + " } ]" + "}"; + + // Verify that the configuration string parses. + int rcode = parseConfiguration(config); + ASSERT_EQ(0, rcode); + + // Verify that the option can be retrieved. + OptionPtr opt = getOptionPtr(DHCP4_OPTION_SPACE, DHO_BOOT_FILE_NAME); + ASSERT_TRUE(opt); + + util::OutputBuffer buf(100); + + uint8_t exp[] = { DHO_BOOT_FILE_NAME, 23, '\\', 'S', 'M', 'S', 'B', 'o', 'o', + 't', '\\', 'x', '6', '4', '\\', 'w', 'd', 's', 'n', 'b', + 'p', '.', 'c', 'o', 'm' }; + ASSERT_EQ(25, sizeof(exp)); + + opt->pack(buf); + EXPECT_EQ(Option::OPTION4_HDR_LEN + 23, buf.getLength()); + + EXPECT_TRUE(0 == memcmp(buf.getData(), exp, 25)); + + ElementPtr expected = Element::fromJSON(config); + ElementPtr opt_data = expected->get("option-data")->getNonConst(0); + opt_data->set("code", Element::create(DHO_BOOT_FILE_NAME)); + CfgOptionsTest cfg(CfgMgr::instance().getStagingCfg()); + cfg.runCfgOptionsTest(family_, expected); +} + +// This test checks behavior of the configuration parser for option data +// for different values of csv-format parameter and when there is an option +// definition present. +TEST_F(ParseConfigTest, optionDataCSVFormatWithOptionDef) { + std::string config = + "{ \"option-data\": [ {" + " \"name\": \"swap-server\"," + " \"space\": \"dhcp4\"," + " \"code\": 16," + " \"data\": \"192.0.2.0\"" + " } ]" + "}"; + + // The default universe is V6. We need to change it to use dhcp4 option + // space. + family_ = AF_INET; + int rcode = 0; + ASSERT_NO_THROW(rcode = parseConfiguration(config)); + ASSERT_EQ(0, rcode); + + // Verify that the option data is correct. + OptionCustomPtr addr_opt = boost::dynamic_pointer_cast< + OptionCustom>(getOptionPtr(DHCP4_OPTION_SPACE, 16)); + ASSERT_TRUE(addr_opt); + EXPECT_EQ("192.0.2.0", addr_opt->readAddress().toText()); + + CfgOptionsTest cfg(CfgMgr::instance().getStagingCfg()); + cfg.runCfgOptionsTest(family_, config); + + // Explicitly enable csv-format. + CfgMgr::instance().clear(); + config = + "{ \"option-data\": [ {" + " \"name\": \"swap-server\"," + " \"space\": \"dhcp4\"," + " \"code\": 16," + " \"csv-format\": true," + " \"data\": \"192.0.2.0\"" + " } ]" + "}"; + ASSERT_NO_THROW(rcode = parseConfiguration(config)); + ASSERT_EQ(0, rcode); + + // Verify that the option data is correct. + addr_opt = boost::dynamic_pointer_cast< + OptionCustom>(getOptionPtr(DHCP4_OPTION_SPACE, 16)); + ASSERT_TRUE(addr_opt); + EXPECT_EQ("192.0.2.0", addr_opt->readAddress().toText()); + + // To make runToElementTest to work the csv-format must be removed... + + // Explicitly disable csv-format and use hex instead. + CfgMgr::instance().clear(); + config = + "{ \"option-data\": [ {" + " \"name\": \"swap-server\"," + " \"space\": \"dhcp4\"," + " \"code\": 16," + " \"csv-format\": false," + " \"data\": \"C0000200\"" + " } ]" + "}"; + ASSERT_NO_THROW(rcode = parseConfiguration(config)); + ASSERT_EQ(0, rcode); + + // Verify that the option data is correct. + addr_opt = boost::dynamic_pointer_cast< + OptionCustom>(getOptionPtr(DHCP4_OPTION_SPACE, 16)); + ASSERT_TRUE(addr_opt); + EXPECT_EQ("192.0.2.0", addr_opt->readAddress().toText()); + + CfgOptionsTest cfg2(CfgMgr::instance().getStagingCfg()); + cfg2.runCfgOptionsTest(family_, config); +} + +// This test verifies that definitions of standard encapsulated +// options can be used. +TEST_F(ParseConfigTest, encapsulatedOptionData) { + std::string config = + "{ \"option-data\": [ {" + " \"space\": \"s46-cont-mape-options\"," + " \"name\": \"s46-rule\"," + " \"data\": \"1, 0, 24, 192.0.2.0, 2001:db8:1::/64\"" + " } ]" + "}"; + + // Make sure that we're using correct universe. + family_ = AF_INET6; + int rcode = 0; + ASSERT_NO_THROW(rcode = parseConfiguration(config)); + ASSERT_EQ(0, rcode); + + // Verify that the option data is correct. + OptionCustomPtr s46_rule = boost::dynamic_pointer_cast<OptionCustom> + (getOptionPtr(MAPE_V6_OPTION_SPACE, D6O_S46_RULE)); + ASSERT_TRUE(s46_rule); + + uint8_t flags = 0; + uint8_t ea_len = 0; + uint8_t prefix4_len = 0; + IOAddress ipv4_prefix(IOAddress::IPV4_ZERO_ADDRESS()); + PrefixTuple ipv6_prefix(PrefixLen(0), IOAddress::IPV6_ZERO_ADDRESS());; + + ASSERT_NO_THROW({ + flags = s46_rule->readInteger<uint8_t>(0); + ea_len = s46_rule->readInteger<uint8_t>(1); + prefix4_len = s46_rule->readInteger<uint8_t>(2); + ipv4_prefix = s46_rule->readAddress(3); + ipv6_prefix = s46_rule->readPrefix(4); + }); + + EXPECT_EQ(1, flags); + EXPECT_EQ(0, ea_len); + EXPECT_EQ(24, prefix4_len); + EXPECT_EQ("192.0.2.0", ipv4_prefix.toText()); + EXPECT_EQ(64, ipv6_prefix.first.asUnsigned()); + EXPECT_EQ("2001:db8:1::", ipv6_prefix.second.toText()); + + ElementPtr expected = Element::fromJSON(config); + ElementPtr opt_data = expected->get("option-data")->getNonConst(0); + opt_data->set("code", Element::create(D6O_S46_RULE)); + CfgOptionsTest cfg(CfgMgr::instance().getStagingCfg()); + cfg.runCfgOptionsTest(family_, expected); +} + +// This test checks behavior of the configuration parser for option data +// for different values of csv-format parameter and when there is no +// option definition. +TEST_F(ParseConfigTest, optionDataCSVFormatNoOptionDef) { + // This option doesn't have any definition. It is ok to use such + // an option but the data should be specified in hex, not as CSV. + // Note that the parser will by default use the CSV format for the + // data but only in case there is a suitable option definition. + std::string config = + "{ \"option-data\": [ {" + " \"name\": \"foo-name\"," + " \"space\": \"dhcp6\"," + " \"code\": 25000," + " \"data\": \"1, 2, 5\"" + " } ]" + "}"; + int rcode = 0; + ASSERT_NO_THROW(rcode = parseConfiguration(config, true)); + EXPECT_NE(0, rcode); + + CfgMgr::instance().clear(); + // The data specified here will work both for CSV format and hex format. + // What we want to test here is that when the csv-format is enforced, the + // parser will fail because of lack of an option definition. + config = + "{ \"option-data\": [ {" + " \"name\": \"foo-name\"," + " \"space\": \"dhcp6\"," + " \"code\": 25000," + " \"csv-format\": true," + " \"data\": \"0\"" + " } ]" + "}"; + ASSERT_NO_THROW(rcode = parseConfiguration(config, true)); + EXPECT_NE(0, rcode); + + CfgMgr::instance().clear(); + // The same test case as above, but for the data specified in hex should + // be successful. + config = + "{ \"option-data\": [ {" + " \"name\": \"foo-name\"," + " \"space\": \"dhcp6\"," + " \"code\": 25000," + " \"csv-format\": false," + " \"data\": \"0\"" + " } ]" + "}"; + ASSERT_NO_THROW(rcode = parseConfiguration(config, true)); + ASSERT_EQ(0, rcode); + OptionPtr opt = getOptionPtr(DHCP6_OPTION_SPACE, 25000); + ASSERT_TRUE(opt); + ASSERT_EQ(1, opt->getData().size()); + EXPECT_EQ(0, opt->getData()[0]); + + ElementPtr expected = Element::fromJSON(config); + ElementPtr opt_data = expected->get("option-data")->getNonConst(0); + opt_data->remove("name"); + opt_data->set("data", Element::create(std::string("00"))); + CfgOptionsTest cfg(CfgMgr::instance().getStagingCfg()); + cfg.runCfgOptionsTest(family_, expected); + + CfgMgr::instance().clear(); + // When csv-format is not specified, the parser will check if the definition + // exists or not. Since there is no definition, the parser will accept the + // data in hex. + config = + "{ \"option-data\": [ {" + " \"name\": \"foo-name\"," + " \"space\": \"dhcp6\"," + " \"code\": 25000," + " \"csv-format\": false," + " \"data\": \"123456\"" + " } ]" + "}"; + ASSERT_NO_THROW(rcode = parseConfiguration(config, true)); + EXPECT_EQ(0, rcode); + opt = getOptionPtr(DHCP6_OPTION_SPACE, 25000); + ASSERT_TRUE(opt); + ASSERT_EQ(3, opt->getData().size()); + EXPECT_EQ(0x12, opt->getData()[0]); + EXPECT_EQ(0x34, opt->getData()[1]); + EXPECT_EQ(0x56, opt->getData()[2]); + + expected = Element::fromJSON(config); + opt_data = expected->get("option-data")->getNonConst(0); + opt_data->remove("name"); + CfgOptionsTest cfg2(CfgMgr::instance().getStagingCfg()); + cfg2.runCfgOptionsTest(family_, expected); +} + +// This test verifies that the option name is not mandatory, if the option +// code has been specified. +TEST_F(ParseConfigTest, optionDataNoName) { + std::string config = + "{ \"option-data\": [ {" + " \"space\": \"dhcp6\"," + " \"code\": 23," + " \"data\": \"2001:db8:1::1\"" + " } ]" + "}"; + int rcode = 0; + ASSERT_NO_THROW(rcode = parseConfiguration(config, true)); + EXPECT_EQ(0, rcode); + Option6AddrLstPtr opt = boost::dynamic_pointer_cast< + Option6AddrLst>(getOptionPtr(DHCP6_OPTION_SPACE, 23)); + ASSERT_TRUE(opt); + ASSERT_EQ(1, opt->getAddresses().size()); + EXPECT_EQ( "2001:db8:1::1", opt->getAddresses()[0].toText()); + + ElementPtr expected = Element::fromJSON(config); + ElementPtr opt_data = expected->get("option-data")->getNonConst(0); + opt_data->set("name", Element::create(std::string("dns-servers"))); + CfgOptionsTest cfg(CfgMgr::instance().getStagingCfg()); + cfg.runCfgOptionsTest(family_, expected); +} + +// This test verifies that the option code is not mandatory, if the option +// name has been specified. +TEST_F(ParseConfigTest, optionDataNoCode) { + std::string config = + "{ \"option-data\": [ {" + " \"space\": \"dhcp6\"," + " \"name\": \"dns-servers\"," + " \"data\": \"2001:db8:1::1\"" + " } ]" + "}"; + int rcode = 0; + ASSERT_NO_THROW(rcode = parseConfiguration(config, true)); + EXPECT_EQ(0, rcode); + Option6AddrLstPtr opt = boost::dynamic_pointer_cast< + Option6AddrLst>(getOptionPtr(DHCP6_OPTION_SPACE, 23)); + ASSERT_TRUE(opt); + ASSERT_EQ(1, opt->getAddresses().size()); + EXPECT_EQ( "2001:db8:1::1", opt->getAddresses()[0].toText()); + + ElementPtr expected = Element::fromJSON(config); + ElementPtr opt_data = expected->get("option-data")->getNonConst(0); + opt_data->set("code", Element::create(D6O_NAME_SERVERS)); + CfgOptionsTest cfg(CfgMgr::instance().getStagingCfg()); + cfg.runCfgOptionsTest(family_, expected); +} + +// This test verifies that the option data configuration with a minimal +// set of parameters works as expected. +TEST_F(ParseConfigTest, optionDataMinimal) { + std::string config = + "{ \"option-data\": [ {" + " \"name\": \"dns-servers\"," + " \"data\": \"2001:db8:1::10\"" + " } ]" + "}"; + int rcode = 0; + ASSERT_NO_THROW(rcode = parseConfiguration(config, true)); + EXPECT_EQ(0, rcode); + Option6AddrLstPtr opt = boost::dynamic_pointer_cast< + Option6AddrLst>(getOptionPtr(DHCP6_OPTION_SPACE, 23)); + ASSERT_TRUE(opt); + ASSERT_EQ(1, opt->getAddresses().size()); + EXPECT_EQ( "2001:db8:1::10", opt->getAddresses()[0].toText()); + + ElementPtr expected = Element::fromJSON(config); + ElementPtr opt_data = expected->get("option-data")->getNonConst(0); + opt_data->set("code", Element::create(D6O_NAME_SERVERS)); + opt_data->set("space", Element::create(std::string(DHCP6_OPTION_SPACE))); + CfgOptionsTest cfg(CfgMgr::instance().getStagingCfg()); + cfg.runCfgOptionsTest(family_, expected); + + CfgMgr::instance().clear(); + // This time using an option code. + config = + "{ \"option-data\": [ {" + " \"code\": 23," + " \"data\": \"2001:db8:1::20\"" + " } ]" + "}"; + rcode = 0; + ASSERT_NO_THROW(rcode = parseConfiguration(config, true)); + EXPECT_EQ(0, rcode); + opt = boost::dynamic_pointer_cast<Option6AddrLst>(getOptionPtr(DHCP6_OPTION_SPACE, + 23)); + ASSERT_TRUE(opt); + ASSERT_EQ(1, opt->getAddresses().size()); + EXPECT_EQ( "2001:db8:1::20", opt->getAddresses()[0].toText()); + + expected = Element::fromJSON(config); + opt_data = expected->get("option-data")->getNonConst(0); + opt_data->set("name", Element::create(std::string("dns-servers"))); + opt_data->set("space", Element::create(std::string(DHCP6_OPTION_SPACE))); + CfgOptionsTest cfg2(CfgMgr::instance().getStagingCfg()); + cfg2.runCfgOptionsTest(family_, expected); +} + +// This test verifies that the option data configuration with a minimal +// set of parameters works as expected when option definition is +// created in the configuration file. +TEST_F(ParseConfigTest, optionDataMinimalWithOptionDef) { + // Configuration string. + std::string config = + "{ \"option-def\": [ {" + " \"name\": \"foo-name\"," + " \"code\": 2345," + " \"type\": \"ipv6-address\"," + " \"array\": true," + " \"space\": \"dhcp6\"" + " } ]," + " \"option-data\": [ {" + " \"name\": \"foo-name\"," + " \"data\": \"2001:db8:1::10, 2001:db8:1::123\"" + " } ]" + "}"; + + int rcode = 0; + ASSERT_NO_THROW(rcode = parseConfiguration(config, true)); + EXPECT_EQ(0, rcode); + Option6AddrLstPtr opt = boost::dynamic_pointer_cast< + Option6AddrLst>(getOptionPtr(DHCP6_OPTION_SPACE, 2345)); + ASSERT_TRUE(opt); + ASSERT_EQ(2, opt->getAddresses().size()); + EXPECT_EQ("2001:db8:1::10", opt->getAddresses()[0].toText()); + EXPECT_EQ("2001:db8:1::123", opt->getAddresses()[1].toText()); + + ElementPtr expected = Element::fromJSON(config); + ElementPtr opt_data = expected->get("option-data")->getNonConst(0); + opt_data->set("code", Element::create(2345)); + opt_data->set("space", Element::create(std::string(DHCP6_OPTION_SPACE))); + CfgOptionsTest cfg(CfgMgr::instance().getStagingCfg()); + cfg.runCfgOptionsTest(family_, expected); + + CfgMgr::instance().clear(); + // Do the same test but now use an option code. + config = + "{ \"option-def\": [ {" + " \"name\": \"foo-name\"," + " \"code\": 2345," + " \"type\": \"ipv6-address\"," + " \"array\": true," + " \"space\": \"dhcp6\"" + " } ]," + " \"option-data\": [ {" + " \"code\": 2345," + " \"data\": \"2001:db8:1::10, 2001:db8:1::123\"" + " } ]" + "}"; + + rcode = 0; + ASSERT_NO_THROW(rcode = parseConfiguration(config, true)); + EXPECT_EQ(0, rcode); + opt = boost::dynamic_pointer_cast<Option6AddrLst>(getOptionPtr(DHCP6_OPTION_SPACE, + 2345)); + ASSERT_TRUE(opt); + ASSERT_EQ(2, opt->getAddresses().size()); + EXPECT_EQ("2001:db8:1::10", opt->getAddresses()[0].toText()); + EXPECT_EQ("2001:db8:1::123", opt->getAddresses()[1].toText()); + + expected = Element::fromJSON(config); + opt_data = expected->get("option-data")->getNonConst(0); + opt_data->set("name", Element::create(std::string("foo-name"))); + opt_data->set("space", Element::create(std::string(DHCP6_OPTION_SPACE))); + CfgOptionsTest cfg2(CfgMgr::instance().getStagingCfg()); + cfg2.runCfgOptionsTest(family_, expected); +} + +// This test verifies an empty option data configuration is supported. +TEST_F(ParseConfigTest, emptyOptionData) { + // Configuration string. + const std::string config = + "{ \"option-data\": [ {" + " \"name\": \"dhcp4o6-server-addr\"" + " } ]" + "}"; + + int rcode = 0; + ASSERT_NO_THROW(rcode = parseConfiguration(config, true)); + EXPECT_EQ(0, rcode); + const Option6AddrLstPtr opt = boost::dynamic_pointer_cast< + Option6AddrLst>(getOptionPtr(DHCP6_OPTION_SPACE, D6O_DHCPV4_O_DHCPV6_SERVER)); + ASSERT_TRUE(opt); + ASSERT_EQ(0, opt->getAddresses().size()); + + ElementPtr expected = Element::fromJSON(config); + ElementPtr opt_data = expected->get("option-data")->getNonConst(0); + opt_data->set("code", Element::create(D6O_DHCPV4_O_DHCPV6_SERVER)); + opt_data->set("space", Element::create(std::string(DHCP6_OPTION_SPACE))); + opt_data->set("csv-format", Element::create(false)); + opt_data->set("data", Element::create(std::string(""))); + CfgOptionsTest cfg(CfgMgr::instance().getStagingCfg()); + cfg.runCfgOptionsTest(family_, expected); +} + +// This test verifies an option data without suboptions is supported +TEST_F(ParseConfigTest, optionDataNoSubOption) { + // Configuration string. A global definition for option 43 is needed. + const std::string config = + "{ \"option-def\": [ {" + " \"name\": \"vendor-encapsulated-options\"," + " \"code\": 43," + " \"type\": \"empty\"," + " \"space\": \"dhcp4\"," + " \"encapsulate\": \"vendor-encapsulated-options\"" + " } ]," + " \"option-data\": [ {" + " \"name\": \"vendor-encapsulated-options\"" + " } ]" + "}"; + + // The default universe is V6. We need to change it to use dhcp4 option + // space. + family_ = AF_INET; + int rcode = 0; + ASSERT_NO_THROW(rcode = parseConfiguration(config)); + EXPECT_EQ(0, rcode); + const OptionPtr opt = getOptionPtr(DHCP4_OPTION_SPACE, DHO_VENDOR_ENCAPSULATED_OPTIONS); + ASSERT_TRUE(opt); + ASSERT_EQ(0, opt->getOptions().size()); + + ElementPtr expected = Element::fromJSON(config); + ElementPtr opt_data = expected->get("option-data")->getNonConst(0); + opt_data->set("code", Element::create(DHO_VENDOR_ENCAPSULATED_OPTIONS)); + opt_data->set("space", Element::create(std::string(DHCP4_OPTION_SPACE))); + opt_data->set("csv-format", Element::create(false)); + opt_data->set("data", Element::create(std::string(""))); + CfgOptionsTest cfg(CfgMgr::instance().getStagingCfg()); + cfg.runCfgOptionsTest(family_, expected); +} + +// This tests option-data in CSV format and embedded commas. +TEST_F(ParseConfigTest, commaCSVFormatOptionData) { + + // Configuration string. + std::string config = + "{ \"option-data\": [ {" + " \"csv-format\": true," + " \"code\": 41," + " \"data\": \"EST5EDT4\\\\,M3.2.0/02:00\\\\,M11.1.0/02:00\"," + " \"space\": \"dhcp6\"" + " } ]" + "}"; + + // Verify that the configuration string parses. + int rcode = parseConfiguration(config, true); + ASSERT_EQ(0, rcode); + + // Verify that the option can be retrieved. + OptionPtr opt = getOptionPtr(DHCP6_OPTION_SPACE, 41); + ASSERT_TRUE(opt); + + // Get the option as an option string. + OptionStringPtr opt_str = boost::dynamic_pointer_cast<OptionString>(opt); + ASSERT_TRUE(opt_str); + + + // Verify that the option data is correct. + string val = "EST5EDT4,M3.2.0/02:00,M11.1.0/02:00"; + EXPECT_EQ(val, opt_str->getValue()); + + ElementPtr expected = Element::fromJSON(config); + ElementPtr opt_data = expected->get("option-data")->getNonConst(0); + opt_data->remove("csv-format"); + opt_data->set("name", Element::create(std::string("new-posix-timezone"))); + CfgOptionsTest cfg(CfgMgr::instance().getStagingCfg()); + cfg.runCfgOptionsTest(family_, expected); +} + +// Verifies that hex literals can support a variety of formats. +TEST_F(ParseConfigTest, hexOptionData) { + + // All of the following variants should parse correctly + // into the same two IPv4 addresses: 12.0.3.1 and 192.0.3.2 + std::vector<std::string> valid_hexes = { + "0C000301C0000302", // even number + "C000301C0000302", // odd number + "0C 00 03 01 C0 00 03 02", // spaces + "0C:00:03:01:C0:00:03:02", // colons + "0x0C000301C0000302", // 0x + "C 0 3 1 C0 0 3 02", // one or two digit octets + "0x0c000301C0000302" // upper or lower case digits + }; + + for (auto hex_str : valid_hexes) { + ostringstream os; + os << + "{ \n" + " \"option-data\": [ { \n" + " \"name\": \"domain-name-servers\", \n" + " \"code\": 6, \n" + " \"space\": \"dhcp4\", \n" + " \"csv-format\": false, \n" + " \"data\": \"" << hex_str << "\" \n" + " } ] \n" + "} \n"; + + reset_context(AF_INET); + int rcode = 0; + ASSERT_NO_THROW(rcode = parseConfiguration(os.str(), true)); + EXPECT_EQ(0, rcode); + + Option4AddrLstPtr opt = boost::dynamic_pointer_cast<Option4AddrLst> + (getOptionPtr(DHCP4_OPTION_SPACE, 6)); + ASSERT_TRUE(opt); + ASSERT_EQ(2, opt->getAddresses().size()); + EXPECT_EQ("12.0.3.1", opt->getAddresses()[0].toText()); + EXPECT_EQ("192.0.3.2", opt->getAddresses()[1].toText()); + } +} + +// Verifies that binary option data can be configured with either +// "'strings'" or hex literals. +TEST_F(ParseConfigTest, stringOrHexBinaryData) { + // Structure the defines a given test scenario + struct Scenario { + std::string description_; // describes the scenario for logging + std::string str_data_; // configured data value of the option + std::vector<uint8_t> exp_binary_; // expected parsed binary data + std::string exp_error_; // expected error test for invalid input + }; + + // Convenience value to use for initting valid scenarios + std::string no_error(""); + + // Valid and invalid scenarios we will test. + // Note we are not concerned with the varitions of valid or invalid + // hex literals those are tested elsewhere. + std::vector<Scenario> scenarios = { + { + "valid hex digits", + "0C:00:03:01:C0:00:03:02", + {0x0C,0x00,0x03,0x01,0xC0,0x00,0x03,0x02}, + no_error + }, + { + "valid string", + "'abcdefghijk'", + {0x61,0x62,0x63,0x64,0x65,0x66,0x67,0x68,0x69,0x6A,0x6B}, + no_error + }, + { + "valid empty", + "", + {}, + no_error + }, + { + "invalid empty", + "''", + {}, + "Configuration parsing failed: option data is not a valid string" + " of hexadecimal digits: '' (<string>:7:13)" + }, + { + "missing end quote", + "'abcdefghijk", + {}, + "Configuration parsing failed: option data is not a valid string" + " of hexadecimal digits: 'abcdefghijk (<string>:7:13)" + }, + { + "missing open quote", + "abcdefghijk'", + {}, + "Configuration parsing failed: option data is not a valid string" + " of hexadecimal digits: abcdefghijk' (<string>:7:13)" + }, + { + "no quotes", + "abcdefghijk", + {}, + "Configuration parsing failed: option data is not a valid string" + " of hexadecimal digits: abcdefghijk (<string>:7:13)" + } + }; + + // Iterate over our test scenarios + for (auto scenario : scenarios) { + SCOPED_TRACE(scenario.description_); + { + // Build the configuration text. + ostringstream os; + os << + "{ \n" + " \"option-data\": [ { \n" + " \"name\": \"user-class\", \n" + " \"code\": 77, \n" + " \"space\": \"dhcp4\", \n" + " \"csv-format\": false, \n" + " \"data\": \"" << scenario.str_data_ << "\" \n" + " } ] \n" + "} \n"; + + // Attempt to parse it. + reset_context(AF_INET); + int rcode = 0; + ASSERT_NO_THROW(rcode = parseConfiguration(os.str(), true)); + + if (!scenario.exp_error_.empty()) { + // We expected to fail, did we? + ASSERT_NE(0, rcode); + // Did we fail for the reason we think we should? + EXPECT_EQ(error_text_, scenario.exp_error_); + } else { + // We expected to succeed, did we? + ASSERT_EQ(0, rcode); + OptionPtr opt = getOptionPtr(DHCP4_OPTION_SPACE, 77); + ASSERT_TRUE(opt); + // Verify the parsed data is correct. + EXPECT_EQ(opt->getData(), scenario.exp_binary_); + } + } + } +} + + +/// The next set of tests check basic operation of the HooksLibrariesParser. +// +// Convenience function to set a configuration of zero or more hooks +// libraries: +// +// lib1 - No parameters +// lib2 - Empty parameters statement +// lib3 - Valid parameters +std::string +setHooksLibrariesConfig(const char* lib1 = NULL, const char* lib2 = NULL, + const char* lib3 = NULL) { + const string lbrace("{"); + const string rbrace("}"); + const string quote("\""); + const string comma_space(", "); + const string library("\"library\": "); + + string config = string("{ \"hooks-libraries\": ["); + if (lib1 != NULL) { + // Library 1 has no parameters + config += lbrace; + config += library + quote + std::string(lib1) + quote; + config += rbrace; + + if (lib2 != NULL) { + // Library 2 has an empty parameters statement + config += comma_space + lbrace; + config += library + quote + std::string(lib2) + quote + comma_space; + config += string("\"parameters\": {}"); + config += rbrace; + + if (lib3 != NULL) { + // Library 3 has valid parameters + config += comma_space + lbrace; + config += library + quote + std::string(lib3) + quote + comma_space; + config += string("\"parameters\": {"); + config += string(" \"svalue\": \"string value\", "); + config += string(" \"ivalue\": 42, "); // Integer value + config += string(" \"bvalue\": true"); // Boolean value + config += string("}"); + config += rbrace; + } + } + } + config += std::string("] }"); + + return (config); +} + +// hooks-libraries element that does not contain anything. +TEST_F(ParseConfigTest, noHooksLibraries) { + // Check that no libraries are currently loaded + vector<string> hooks_libraries = HooksManager::getLibraryNames(); + EXPECT_TRUE(hooks_libraries.empty()); + + // Create an empty hooks-libraries configuration element. + const string config = setHooksLibrariesConfig(); + + // Verify that the configuration string parses. + const int rcode = parseConfiguration(config); + ASSERT_TRUE(rcode == 0) << error_text_; + + // Verify that the configuration object unparses. + ConstElementPtr expected; + ASSERT_NO_THROW(expected = + Element::fromJSON(config)->get("hooks-libraries")); + ASSERT_TRUE(expected); + const HooksConfig& cfg = + CfgMgr::instance().getStagingCfg()->getHooksConfig(); + runToElementTest<HooksConfig>(expected, cfg); + + // Check that the parser recorded nothing. + isc::hooks::HookLibsCollection libraries = getLibraries(); + EXPECT_TRUE(libraries.empty()); + + // Check that there are still no libraries loaded. + hooks_libraries = HooksManager::getLibraryNames(); + EXPECT_TRUE(hooks_libraries.empty()); +} + +// hooks-libraries element that contains a single library. +TEST_F(ParseConfigTest, oneHooksLibrary) { + // Check that no libraries are currently loaded + vector<string> hooks_libraries = HooksManager::getLibraryNames(); + EXPECT_TRUE(hooks_libraries.empty()); + + // Configuration with hooks-libraries set to a single library. + const string config = setHooksLibrariesConfig(CALLOUT_LIBRARY_1); + + // Verify that the configuration string parses. + const int rcode = parseConfiguration(config); + ASSERT_TRUE(rcode == 0) << error_text_; + + // Verify that the configuration object unparses. + ConstElementPtr expected; + ASSERT_NO_THROW(expected = + Element::fromJSON(config)->get("hooks-libraries")); + ASSERT_TRUE(expected); + const HooksConfig& cfg = + CfgMgr::instance().getStagingCfg()->getHooksConfig(); + runToElementTest<HooksConfig>(expected, cfg); + + // Check that the parser recorded a single library. + isc::hooks::HookLibsCollection libraries = getLibraries(); + ASSERT_EQ(1, libraries.size()); + EXPECT_EQ(CALLOUT_LIBRARY_1, libraries[0].first); + + // Check that the change was propagated to the hooks manager. + hooks_libraries = HooksManager::getLibraryNames(); + ASSERT_EQ(1, hooks_libraries.size()); + EXPECT_EQ(CALLOUT_LIBRARY_1, hooks_libraries[0]); +} + +// hooks-libraries element that contains two libraries +TEST_F(ParseConfigTest, twoHooksLibraries) { + // Check that no libraries are currently loaded + vector<string> hooks_libraries = HooksManager::getLibraryNames(); + EXPECT_TRUE(hooks_libraries.empty()); + + // Configuration with hooks-libraries set to two libraries. + const string config = setHooksLibrariesConfig(CALLOUT_LIBRARY_1, + CALLOUT_LIBRARY_2); + + // Verify that the configuration string parses. + const int rcode = parseConfiguration(config); + ASSERT_TRUE(rcode == 0) << error_text_; + + // Verify that the configuration object unparses. + ConstElementPtr expected; + ASSERT_NO_THROW(expected = + Element::fromJSON(config)->get("hooks-libraries")); + ASSERT_TRUE(expected); + const HooksConfig& cfg = + CfgMgr::instance().getStagingCfg()->getHooksConfig(); + runToElementTest<HooksConfig>(expected, cfg); + + // Check that the parser recorded two libraries in the expected order. + isc::hooks::HookLibsCollection libraries = getLibraries(); + ASSERT_EQ(2, libraries.size()); + EXPECT_EQ(CALLOUT_LIBRARY_1, libraries[0].first); + EXPECT_EQ(CALLOUT_LIBRARY_2, libraries[1].first); + + // Verify that the change was propagated to the hooks manager. + hooks_libraries = HooksManager::getLibraryNames(); + ASSERT_EQ(2, hooks_libraries.size()); + EXPECT_EQ(CALLOUT_LIBRARY_1, hooks_libraries[0]); + EXPECT_EQ(CALLOUT_LIBRARY_2, hooks_libraries[1]); +} + +// Configure with two libraries, then reconfigure with the same libraries. +TEST_F(ParseConfigTest, reconfigureSameHooksLibraries) { + // Check that no libraries are currently loaded + vector<string> hooks_libraries = HooksManager::getLibraryNames(); + EXPECT_TRUE(hooks_libraries.empty()); + + // Configuration with hooks-libraries set to two libraries. + const std::string config = setHooksLibrariesConfig(CALLOUT_LIBRARY_1, + CALLOUT_LIBRARY_2); + + // Verify that the configuration string parses. The twoHooksLibraries + // test shows that the list will be as expected. + int rcode = parseConfiguration(config); + ASSERT_TRUE(rcode == 0) << error_text_; + + // Verify that the configuration object unparses. + ConstElementPtr expected; + ASSERT_NO_THROW(expected = + Element::fromJSON(config)->get("hooks-libraries")); + ASSERT_TRUE(expected); + const HooksConfig& cfg = + CfgMgr::instance().getStagingCfg()->getHooksConfig(); + runToElementTest<HooksConfig>(expected, cfg); + + // The previous test shows that the parser correctly recorded the two + // libraries and that they loaded correctly. + + // Parse the string again. + rcode = parseConfiguration(config); + ASSERT_TRUE(rcode == 0) << error_text_; + + // The list has not changed between the two parse operations. However, + // the parameters (or the files they could point to) could have + // changed, so the libraries are reloaded anyway. + const HooksConfig& cfg2 = + CfgMgr::instance().getStagingCfg()->getHooksConfig(); + runToElementTest<HooksConfig>(expected, cfg2); + isc::hooks::HookLibsCollection libraries = getLibraries(); + ASSERT_EQ(2, libraries.size()); + EXPECT_EQ(CALLOUT_LIBRARY_1, libraries[0].first); + EXPECT_EQ(CALLOUT_LIBRARY_2, libraries[1].first); + + // ... and check that the same two libraries are still loaded in the + // HooksManager. + hooks_libraries = HooksManager::getLibraryNames(); + ASSERT_EQ(2, hooks_libraries.size()); + EXPECT_EQ(CALLOUT_LIBRARY_1, hooks_libraries[0]); + EXPECT_EQ(CALLOUT_LIBRARY_2, hooks_libraries[1]); +} + +// Configure the hooks with two libraries, then reconfigure with the same +// libraries, but in reverse order. +TEST_F(ParseConfigTest, reconfigureReverseHooksLibraries) { + // Check that no libraries are currently loaded + vector<string> hooks_libraries = HooksManager::getLibraryNames(); + EXPECT_TRUE(hooks_libraries.empty()); + + // Configuration with hooks-libraries set to two libraries. + std::string config = setHooksLibrariesConfig(CALLOUT_LIBRARY_1, + CALLOUT_LIBRARY_2); + + // Verify that the configuration string parses. The twoHooksLibraries + // test shows that the list will be as expected. + int rcode = parseConfiguration(config); + ASSERT_TRUE(rcode == 0) << error_text_; + + // A previous test shows that the parser correctly recorded the two + // libraries and that they loaded correctly. + + // Parse the reversed set of libraries. + config = setHooksLibrariesConfig(CALLOUT_LIBRARY_2, CALLOUT_LIBRARY_1); + rcode = parseConfiguration(config); + ASSERT_TRUE(rcode == 0) << error_text_; + + // The list has changed, and this is what we should see. + isc::hooks::HookLibsCollection libraries = getLibraries(); + ASSERT_EQ(2, libraries.size()); + EXPECT_EQ(CALLOUT_LIBRARY_2, libraries[0].first); + EXPECT_EQ(CALLOUT_LIBRARY_1, libraries[1].first); + + // ... and check that this was propagated to the HooksManager. + hooks_libraries = HooksManager::getLibraryNames(); + ASSERT_EQ(2, hooks_libraries.size()); + EXPECT_EQ(CALLOUT_LIBRARY_2, hooks_libraries[0]); + EXPECT_EQ(CALLOUT_LIBRARY_1, hooks_libraries[1]); +} + +// Configure the hooks with two libraries, then reconfigure with +// no libraries. +TEST_F(ParseConfigTest, reconfigureZeroHooksLibraries) { + // Check that no libraries are currently loaded + vector<string> hooks_libraries = HooksManager::getLibraryNames(); + EXPECT_TRUE(hooks_libraries.empty()); + + // Configuration with hooks-libraries set to two libraries. + std::string config = setHooksLibrariesConfig(CALLOUT_LIBRARY_1, + CALLOUT_LIBRARY_2); + + // Verify that the configuration string parses. + int rcode = parseConfiguration(config); + ASSERT_TRUE(rcode == 0) << error_text_; + + // A previous test shows that the parser correctly recorded the two + // libraries and that they loaded correctly. + + // Parse the string again, this time without any libraries. + config = setHooksLibrariesConfig(); + rcode = parseConfiguration(config); + ASSERT_TRUE(rcode == 0) << error_text_; + + // Verify that the configuration object unparses. + ConstElementPtr expected; + ASSERT_NO_THROW(expected = + Element::fromJSON(config)->get("hooks-libraries")); + ASSERT_TRUE(expected); + const HooksConfig& cfg = + CfgMgr::instance().getStagingCfg()->getHooksConfig(); + runToElementTest<HooksConfig>(expected, cfg); + + // The list has changed, and this is what we should see. + isc::hooks::HookLibsCollection libraries = getLibraries(); + EXPECT_TRUE(libraries.empty()); + + // Check that no libraries are currently loaded + hooks_libraries = HooksManager::getLibraryNames(); + EXPECT_TRUE(hooks_libraries.empty()); +} + +// Check with a set of libraries, some of which are invalid. +TEST_F(ParseConfigTest, invalidHooksLibraries) { + // Check that no libraries are currently loaded + vector<string> hooks_libraries = HooksManager::getLibraryNames(); + EXPECT_TRUE(hooks_libraries.empty()); + + // Configuration string. This contains an invalid library which should + // trigger an error in the "build" stage. + const std::string config = setHooksLibrariesConfig(CALLOUT_LIBRARY_1, + NOT_PRESENT_LIBRARY, + CALLOUT_LIBRARY_2); + + // Verify that the configuration fails to parse. (Syntactically it's OK, + // but the library is invalid). + const int rcode = parseConfiguration(config); + ASSERT_FALSE(rcode == 0) << error_text_; + + // Check that the message contains the library in error. + EXPECT_FALSE(error_text_.find(NOT_PRESENT_LIBRARY) == string::npos) << + "Error text returned from parse failure is " << error_text_; + + // Check that the parser recorded the names but, as they were in error, + // does not flag them as changed. + isc::hooks::HookLibsCollection libraries = getLibraries(); + ASSERT_EQ(3, libraries.size()); + EXPECT_EQ(CALLOUT_LIBRARY_1, libraries[0].first); + EXPECT_EQ(NOT_PRESENT_LIBRARY, libraries[1].first); + EXPECT_EQ(CALLOUT_LIBRARY_2, libraries[2].first); + + // ...and check it did not alter the libraries in the hooks manager. + hooks_libraries = HooksManager::getLibraryNames(); + EXPECT_TRUE(hooks_libraries.empty()); +} + +// Check that trying to reconfigure with an invalid set of libraries fails. +TEST_F(ParseConfigTest, reconfigureInvalidHooksLibraries) { + // Check that no libraries are currently loaded + vector<string> hooks_libraries = HooksManager::getLibraryNames(); + EXPECT_TRUE(hooks_libraries.empty()); + + // Configure with a single library. + std::string config = setHooksLibrariesConfig(CALLOUT_LIBRARY_1); + int rcode = parseConfiguration(config); + ASSERT_TRUE(rcode == 0) << error_text_; + + // A previous test shows that the parser correctly recorded the two + // libraries and that they loaded correctly. + + // Configuration string. This contains an invalid library which should + // trigger an error in the "build" stage. + config = setHooksLibrariesConfig(CALLOUT_LIBRARY_1, NOT_PRESENT_LIBRARY, + CALLOUT_LIBRARY_2); + + // Verify that the configuration fails to parse. (Syntactically it's OK, + // but the library is invalid). + rcode = parseConfiguration(config); + EXPECT_FALSE(rcode == 0) << error_text_; + + // Check that the message contains the library in error. + EXPECT_FALSE(error_text_.find(NOT_PRESENT_LIBRARY) == string::npos) << + "Error text returned from parse failure is " << error_text_; + + // Check that the parser recorded the names but, as the library set was + // incorrect, did not mark the configuration as changed. + isc::hooks::HookLibsCollection libraries = getLibraries(); + ASSERT_EQ(3, libraries.size()); + EXPECT_EQ(CALLOUT_LIBRARY_1, libraries[0].first); + EXPECT_EQ(NOT_PRESENT_LIBRARY, libraries[1].first); + EXPECT_EQ(CALLOUT_LIBRARY_2, libraries[2].first); + + // ... but check that the hooks manager was not updated with the incorrect + // names. + hooks_libraries.clear(); + hooks_libraries = HooksManager::getLibraryNames(); + ASSERT_EQ(1, hooks_libraries.size()); + EXPECT_EQ(CALLOUT_LIBRARY_1, hooks_libraries[0]); +} + +// Check that if hooks-libraries contains invalid syntax, it is detected. +TEST_F(ParseConfigTest, invalidSyntaxHooksLibraries) { + + // Element holds a mixture of (valid) maps and non-maps. + string config1 = "{ \"hooks-libraries\": [ " + "{ \"library\": \"/opt/lib/lib1\" }, " + "\"/opt/lib/lib2\" " + "] }"; + string error1 = "one or more entries in the hooks-libraries list is not" + " a map"; + + int rcode = parseConfiguration(config1); + ASSERT_NE(0, rcode); + EXPECT_TRUE(error_text_.find(error1) != string::npos) << + "Error text returned from parse failure is " << error_text_; + + // Element holds valid maps, except one where the library element is not + // a string. + string config2 = "{ \"hooks-libraries\": [ " + "{ \"library\": \"/opt/lib/lib1\" }, " + "{ \"library\": 123 } " + "] }"; + string error2 = "value of 'library' element is not a string giving" + " the path to a hooks library"; + + rcode = parseConfiguration(config2); + ASSERT_NE(0, rcode); + EXPECT_TRUE(error_text_.find(error2) != string::npos) << + "Error text returned from parse failure is " << error_text_; + + // Element holds valid maps, except one where the library element is the + // empty string. + string config3 = "{ \"hooks-libraries\": [ " + "{ \"library\": \"/opt/lib/lib1\" }, " + "{ \"library\": \"\" } " + "] }"; + string error3 = "value of 'library' element must not be blank"; + + rcode = parseConfiguration(config3); + ASSERT_NE(0, rcode); + EXPECT_TRUE(error_text_.find(error3) != string::npos) << + "Error text returned from parse failure is " << error_text_; + + // Element holds valid maps, except one where the library element is all + // spaces. + string config4 = "{ \"hooks-libraries\": [ " + "{ \"library\": \"/opt/lib/lib1\" }, " + "{ \"library\": \" \" } " + "] }"; + string error4 = "value of 'library' element must not be blank"; + + rcode = parseConfiguration(config4); + ASSERT_NE(0, rcode); + EXPECT_TRUE(error_text_.find(error3) != string::npos) << + "Error text returned from parse failure is " << error_text_; + + // Element holds valid maps, except one that does not contain a + // 'library' element. + string config5 = "{ \"hooks-libraries\": [ " + "{ \"library\": \"/opt/lib/lib1\" }, " + "{ \"parameters\": { \"alpha\": 123 } }, " + "{ \"library\": \"/opt/lib/lib2\" } " + "] }"; + string error5 = "one or more hooks-libraries elements are missing the" + " name of the library"; + + rcode = parseConfiguration(config5); + ASSERT_NE(0, rcode); + EXPECT_TRUE(error_text_.find(error5) != string::npos) << + "Error text returned from parse failure is " << error_text_; +} + +// Check that some parameters may have configuration parameters configured. +TEST_F(ParseConfigTest, HooksLibrariesParameters) { + // Check that no libraries are currently loaded + vector<string> hooks_libraries = HooksManager::getLibraryNames(); + EXPECT_TRUE(hooks_libraries.empty()); + + // Configuration string. This contains an invalid library which should + // trigger an error in the "build" stage. + const std::string config = setHooksLibrariesConfig(CALLOUT_LIBRARY_1, + CALLOUT_LIBRARY_2, + CALLOUT_PARAMS_LIBRARY); + + // Verify that the configuration fails to parse. (Syntactically it's OK, + // but the library is invalid). + const int rcode = parseConfiguration(config); + ASSERT_EQ(0, rcode); + + // Verify that the configuration object unparses. + ConstElementPtr expected; + ASSERT_NO_THROW(expected = + Element::fromJSON(config)->get("hooks-libraries")); + ASSERT_TRUE(expected); + const HooksConfig& cfg = + CfgMgr::instance().getStagingCfg()->getHooksConfig(); + runToElementTest<HooksConfig>(expected, cfg); + + // Check that the parser recorded the names. + isc::hooks::HookLibsCollection libraries = getLibraries(); + ASSERT_EQ(3, libraries.size()); + EXPECT_EQ(CALLOUT_LIBRARY_1, libraries[0].first); + EXPECT_EQ(CALLOUT_LIBRARY_2, libraries[1].first); + EXPECT_EQ(CALLOUT_PARAMS_LIBRARY, libraries[2].first); + + // Also, check that the third library has its parameters specified. + // They were set by setHooksLibrariesConfig. The first has no + // parameters, the second one has an empty map and the third + // one has actual parameters. + EXPECT_FALSE(libraries[0].second); + EXPECT_TRUE(libraries[1].second); + ASSERT_TRUE(libraries[2].second); + + // Ok, get the parameter for the third library. + ConstElementPtr params = libraries[2].second; + + // It must be a map. + ASSERT_EQ(Element::map, params->getType()); + + // This map should have 3 parameters: + // - svalue (and will expect its value to be "string value") + // - ivalue (and will expect its value to be 42) + // - bvalue (and will expect its value to be true) + ConstElementPtr svalue = params->get("svalue"); + ConstElementPtr ivalue = params->get("ivalue"); + ConstElementPtr bvalue = params->get("bvalue"); + + // There should be no extra parameters. + EXPECT_FALSE(params->get("nonexistent")); + + ASSERT_TRUE(svalue); + ASSERT_TRUE(ivalue); + ASSERT_TRUE(bvalue); + + ASSERT_EQ(Element::string, svalue->getType()); + ASSERT_EQ(Element::integer, ivalue->getType()); + ASSERT_EQ(Element::boolean, bvalue->getType()); + + EXPECT_EQ("string value", svalue->stringValue()); + EXPECT_EQ(42, ivalue->intValue()); + EXPECT_EQ(true, bvalue->boolValue()); +} + +/// @brief Checks that a valid, enabled D2 client configuration works correctly. +TEST_F(ParseConfigTest, validD2Config) { + + // Configuration string containing valid values. + std::string config_str = + "{ \"dhcp-ddns\" :" + " {" + " \"enable-updates\" : true, " + " \"server-ip\" : \"192.0.2.0\", " + " \"server-port\" : 3432, " + " \"sender-ip\" : \"192.0.2.1\", " + " \"sender-port\" : 3433, " + " \"max-queue-size\" : 2048, " + " \"ncr-protocol\" : \"UDP\", " + " \"ncr-format\" : \"JSON\", " + " \"user-context\": { \"foo\": \"bar\" } " + " }" + "}"; + + // Verify that the configuration string parses. + int rcode = parseConfiguration(config_str); + ASSERT_TRUE(rcode == 0) << error_text_; + + // Verify that DHCP-DDNS is enabled and we can fetch the configuration. + EXPECT_TRUE(CfgMgr::instance().ddnsEnabled()); + D2ClientConfigPtr d2_client_config; + ASSERT_NO_THROW(d2_client_config = CfgMgr::instance().getD2ClientConfig()); + ASSERT_TRUE(d2_client_config); + + // Verify that the configuration values are as expected. + EXPECT_TRUE(d2_client_config->getEnableUpdates()); + EXPECT_EQ("192.0.2.0", d2_client_config->getServerIp().toText()); + EXPECT_EQ(3432, d2_client_config->getServerPort()); + EXPECT_EQ(dhcp_ddns::NCR_UDP, d2_client_config->getNcrProtocol()); + EXPECT_EQ(dhcp_ddns::FMT_JSON, d2_client_config->getNcrFormat()); + ASSERT_TRUE(d2_client_config->getContext()); + EXPECT_EQ("{ \"foo\": \"bar\" }", d2_client_config->getContext()->str()); + + // Verify that the configuration object unparses. + ConstElementPtr expected; + ASSERT_NO_THROW(expected = Element::fromJSON(config_str)->get("dhcp-ddns")); + ASSERT_TRUE(expected); + runToElementTest<D2ClientConfig>(expected, *d2_client_config); + + // Another valid Configuration string. + // This one is disabled, has IPV6 server ip, control flags false, + // empty prefix/suffix + std::string config_str2 = + "{ \"dhcp-ddns\" :" + " {" + " \"enable-updates\" : false, " + " \"server-ip\" : \"2001:db8::\", " + " \"server-port\" : 43567, " + " \"sender-ip\" : \"2001:db8::1\", " + " \"sender-port\" : 3433, " + " \"max-queue-size\" : 2048, " + " \"ncr-protocol\" : \"UDP\", " + " \"ncr-format\" : \"JSON\", " + " \"user-context\": { \"foo\": \"bar\" } " + " }" + "}"; + + // Verify that the configuration string parses. + rcode = parseConfiguration(config_str2); + ASSERT_TRUE(rcode == 0) << error_text_; + + // Verify that DHCP-DDNS is disabled and we can fetch the configuration. + EXPECT_FALSE(CfgMgr::instance().ddnsEnabled()); + ASSERT_NO_THROW(d2_client_config = CfgMgr::instance().getD2ClientConfig()); + ASSERT_TRUE(d2_client_config); + + // Verify that the configuration values are as expected. + EXPECT_FALSE(d2_client_config->getEnableUpdates()); + EXPECT_EQ("2001:db8::", d2_client_config->getServerIp().toText()); + EXPECT_EQ(43567, d2_client_config->getServerPort()); + EXPECT_EQ(dhcp_ddns::NCR_UDP, d2_client_config->getNcrProtocol()); + EXPECT_EQ(dhcp_ddns::FMT_JSON, d2_client_config->getNcrFormat()); + ASSERT_TRUE(d2_client_config->getContext()); + EXPECT_EQ("{ \"foo\": \"bar\" }", d2_client_config->getContext()->str()); + + ASSERT_NO_THROW(expected = Element::fromJSON(config_str2)->get("dhcp-ddns")); + ASSERT_TRUE(expected); + runToElementTest<D2ClientConfig>(expected, *d2_client_config); +} + +/// @brief Checks that D2 client can be configured with enable flag of +/// false only. +TEST_F(ParseConfigTest, validDisabledD2Config) { + + // Configuration string. This defines a disabled D2 client config. + std::string config_str = + "{ \"dhcp-ddns\" :" + " {" + " \"enable-updates\" : false" + " }" + "}"; + + // Verify that the configuration string parses. + int rcode = parseConfiguration(config_str); + ASSERT_TRUE(rcode == 0) << error_text_; + + // Verify that DHCP-DDNS is disabled. + EXPECT_FALSE(CfgMgr::instance().ddnsEnabled()); + + // Make sure fetched config agrees. + D2ClientConfigPtr d2_client_config; + ASSERT_NO_THROW(d2_client_config = CfgMgr::instance().getD2ClientConfig()); + EXPECT_TRUE(d2_client_config); + EXPECT_FALSE(d2_client_config->getEnableUpdates()); +} + +/// @brief Checks that given a partial configuration, parser supplies +/// default values +TEST_F(ParseConfigTest, parserDefaultsD2Config) { + + // Configuration string. This defines an enabled D2 client config + // with the mandatory parameter in such a case, all other parameters + // are optional and their default values will be used. + std::string config_str = + "{ \"dhcp-ddns\" :" + " {" + " \"enable-updates\" : true " + " }" + "}"; + + // Verify that the configuration string parses. + int rcode = parseConfiguration(config_str); + ASSERT_TRUE(rcode == 0) << error_text_; + + // Verify that DHCP-DDNS is enabled. + EXPECT_TRUE(CfgMgr::instance().ddnsEnabled()); + + // Make sure fetched config is correct. + D2ClientConfigPtr d2_client_config; + ASSERT_NO_THROW(d2_client_config = CfgMgr::instance().getD2ClientConfig()); + EXPECT_TRUE(d2_client_config); + EXPECT_TRUE(d2_client_config->getEnableUpdates()); + EXPECT_EQ(D2ClientConfig::DFT_SERVER_IP, + d2_client_config->getServerIp().toText()); + EXPECT_EQ(D2ClientConfig::DFT_SERVER_PORT, + d2_client_config->getServerPort()); + EXPECT_EQ(dhcp_ddns::stringToNcrProtocol(D2ClientConfig::DFT_NCR_PROTOCOL), + d2_client_config->getNcrProtocol()); + EXPECT_EQ(dhcp_ddns::stringToNcrFormat(D2ClientConfig::DFT_NCR_FORMAT), + d2_client_config->getNcrFormat()); +} + + +/// @brief Check various invalid D2 client configurations. +TEST_F(ParseConfigTest, invalidD2Config) { + std::string invalid_configs[] = { + // Invalid server ip value + "{ \"dhcp-ddns\" :" + " {" + " \"enable-updates\" : true, " + " \"server-ip\" : \"x192.0.2.0\", " + " \"server-port\" : 53001, " + " \"ncr-protocol\" : \"UDP\", " + " \"ncr-format\" : \"JSON\" " + " }" + "}", + // Unknown protocol + "{ \"dhcp-ddns\" :" + " {" + " \"enable-updates\" : true, " + " \"server-ip\" : \"192.0.2.0\", " + " \"server-port\" : 53001, " + " \"ncr-protocol\" : \"Bogus\", " + " \"ncr-format\" : \"JSON\" " + " }" + "}", + // Unsupported protocol + "{ \"dhcp-ddns\" :" + " {" + " \"enable-updates\" : true, " + " \"server-ip\" : \"192.0.2.0\", " + " \"server-port\" : 53001, " + " \"ncr-protocol\" : \"TCP\", " + " \"ncr-format\" : \"JSON\" " + " }" + "}", + // Unknown format + "{ \"dhcp-ddns\" :" + " {" + " \"enable-updates\" : true, " + " \"server-ip\" : \"192.0.2.0\", " + " \"server-port\" : 53001, " + " \"ncr-protocol\" : \"UDP\", " + " \"ncr-format\" : \"Bogus\" " + " }" + "}", + // Invalid Port + "{ \"dhcp-ddns\" :" + " {" + " \"enable-updates\" : true, " + " \"server-ip\" : \"192.0.2.0\", " + " \"server-port\" : \"bogus\", " + " \"ncr-protocol\" : \"UDP\", " + " \"ncr-format\" : \"JSON\" " + " }" + "}", + // Mismatched server and sender IPs + "{ \"dhcp-ddns\" :" + " {" + " \"enable-updates\" : true, " + " \"server-ip\" : \"192.0.2.0\", " + " \"server-port\" : 3432, " + " \"sender-ip\" : \"3001::5\", " + " \"sender-port\" : 3433, " + " \"max-queue-size\" : 2048, " + " \"ncr-protocol\" : \"UDP\", " + " \"ncr-format\" : \"JSON\" " + " }" + "}", + // Identical server and sender IP/port + "{ \"dhcp-ddns\" :" + " {" + " \"enable-updates\" : true, " + " \"server-ip\" : \"3001::5\", " + " \"server-port\" : 3433, " + " \"sender-ip\" : \"3001::5\", " + " \"sender-port\" : 3433, " + " \"max-queue-size\" : 2048, " + " \"ncr-protocol\" : \"UDP\", " + " \"ncr-format\" : \"JSON\" " + " }" + "}", + // stop + "" + }; + + // Fetch the original config. + D2ClientConfigPtr original_config; + ASSERT_NO_THROW(original_config = CfgMgr::instance().getD2ClientConfig()); + + // Iterate through the invalid configuration strings, attempting to + // parse each one. They should fail to parse, but fail gracefully. + D2ClientConfigPtr current_config; + int i = 0; + while (!invalid_configs[i].empty()) { + // Verify that the configuration string parses without throwing. + int rcode = parseConfiguration(invalid_configs[i]); + + // Verify that parse result indicates a parsing error. + ASSERT_TRUE(rcode != 0) << "Invalid config #: " << i + << " should not have passed!"; + + // Verify that the "official" config still matches the original config. + ASSERT_NO_THROW(current_config = + CfgMgr::instance().getD2ClientConfig()); + EXPECT_EQ(*original_config, *current_config); + ++i; + } +} + +/// @brief Checks that a valid relay info structure for IPv4 can be handled +TEST_F(ParseConfigTest, validRelayInfo4) { + + // Relay information structure. Very simple for now. + std::string config_str = + " {" + " \"ip-address\" : \"192.0.2.1\"" + " }"; + ElementPtr json = Element::fromJSON(config_str); + + // Create an "empty" RelayInfo to hold the parsed result. + Network::RelayInfoPtr result(new Network::RelayInfo()); + + RelayInfoParser parser(Option::V4); + + EXPECT_NO_THROW(parser.parse(result, json)); + EXPECT_TRUE(result->containsAddress(IOAddress("192.0.2.1"))); +} + +/// @brief Checks that a bogus relay info structure for IPv4 is rejected. +TEST_F(ParseConfigTest, bogusRelayInfo4) { + + // Invalid config (wrong family type of the ip-address field) + std::string config_str_bogus1 = + " {" + " \"ip-address\" : \"2001:db8::1\"" + " }"; + ElementPtr json_bogus1 = Element::fromJSON(config_str_bogus1); + + // Invalid config (that thing is not an IPv4 address) + std::string config_str_bogus2 = + " {" + " \"ip-address\" : \"256.345.123.456\"" + " }"; + ElementPtr json_bogus2 = Element::fromJSON(config_str_bogus2); + + // Invalid config (ip-address is mandatory) + std::string config_str_bogus3 = + " {" + " }"; + ElementPtr json_bogus3 = Element::fromJSON(config_str_bogus3); + + // Create an "empty" RelayInfo to hold the parsed result. + Network::RelayInfoPtr result(new Network::RelayInfo()); + + RelayInfoParser parser(Option::V4); + + // wrong family type + EXPECT_THROW(parser.parse(result, json_bogus1), DhcpConfigError); + + // Too large byte values in pseudo-IPv4 addr + EXPECT_THROW(parser.parse(result, json_bogus2), DhcpConfigError); + + // Mandatory ip-address is missing. What a pity. + EXPECT_THROW(parser.parse(result, json_bogus2), DhcpConfigError); +} + +/// @brief Checks that a valid relay info structure for IPv6 can be handled +TEST_F(ParseConfigTest, validRelayInfo6) { + + // Relay information structure. Very simple for now. + std::string config_str = + " {" + " \"ip-address\" : \"2001:db8::1\"" + " }"; + ElementPtr json = Element::fromJSON(config_str); + + // Create an "empty" RelayInfo to hold the parsed result. + Network::RelayInfoPtr result(new Network::RelayInfo()); + + RelayInfoParser parser(Option::V6); + + EXPECT_NO_THROW(parser.parse(result, json)); + EXPECT_TRUE(result->containsAddress(IOAddress("2001:db8::1"))); +} + +/// @brief Checks that a valid relay info structure for IPv6 can be handled +TEST_F(ParseConfigTest, bogusRelayInfo6) { + + // Invalid config (wrong family type of the ip-address field + std::string config_str_bogus1 = + " {" + " \"ip-address\" : \"192.0.2.1\"" + " }"; + ElementPtr json_bogus1 = Element::fromJSON(config_str_bogus1); + + // That IPv6 address doesn't look right + std::string config_str_bogus2 = + " {" + " \"ip-address\" : \"2001:db8:::4\"" + " }"; + ElementPtr json_bogus2 = Element::fromJSON(config_str_bogus2); + + // Missing mandatory ip-address field. + std::string config_str_bogus3 = + " {" + " }"; + ElementPtr json_bogus3 = Element::fromJSON(config_str_bogus3); + + // Create an "empty" RelayInfo to hold the parsed result. + Network::RelayInfoPtr result(new Network::RelayInfo()); + + RelayInfoParser parser(Option::V6); + + // Negative scenario (wrong family type) + EXPECT_THROW(parser.parse(result, json_bogus1), DhcpConfigError); + + // Looks like IPv6 address, but has too many colons + EXPECT_THROW(parser.parse(result, json_bogus2), DhcpConfigError); + + // Mandatory ip-address is missing. What a pity. + EXPECT_THROW(parser.parse(result, json_bogus3), DhcpConfigError); +} + +// This test verifies that it is possible to parse an IPv4 subnet for which +// only mandatory parameters are specified without setting the defaults. +TEST_F(ParseConfigTest, defaultSubnet4) { + std::string config = + "{" + " \"subnet4\": [ {" + " \"subnet\": \"192.0.2.0/24\"," + " \"id\": 123" + " } ]" + "}"; + + int rcode = parseConfiguration(config, false, false); + ASSERT_EQ(0, rcode); + + auto subnet = CfgMgr::instance().getStagingCfg()->getCfgSubnets4()->getBySubnetId(123); + ASSERT_TRUE(subnet); + + EXPECT_TRUE(subnet->hasFetchGlobalsFn()); + + EXPECT_TRUE(subnet->getIface().unspecified()); + EXPECT_TRUE(subnet->getIface().empty()); + + EXPECT_TRUE(subnet->getClientClass().unspecified()); + EXPECT_TRUE(subnet->getClientClass().empty()); + + EXPECT_TRUE(subnet->getValid().unspecified()); + EXPECT_EQ(0, subnet->getValid().get()); + + EXPECT_TRUE(subnet->getT1().unspecified()); + EXPECT_EQ(0, subnet->getT1().get()); + + EXPECT_TRUE(subnet->getT2().unspecified()); + EXPECT_EQ(0, subnet->getT2().get()); + + EXPECT_TRUE(subnet->getReservationsGlobal().unspecified()); + EXPECT_FALSE(subnet->getReservationsGlobal().get()); + + EXPECT_TRUE(subnet->getReservationsInSubnet().unspecified()); + EXPECT_TRUE(subnet->getReservationsInSubnet().get()); + + EXPECT_TRUE(subnet->getReservationsOutOfPool().unspecified()); + EXPECT_FALSE(subnet->getReservationsOutOfPool().get()); + + EXPECT_TRUE(subnet->getCalculateTeeTimes().unspecified()); + EXPECT_FALSE(subnet->getCalculateTeeTimes().get()); + + EXPECT_TRUE(subnet->getT1Percent().unspecified()); + EXPECT_EQ(0.0, subnet->getT1Percent().get()); + + EXPECT_TRUE(subnet->getT2Percent().unspecified()); + EXPECT_EQ(0.0, subnet->getT2Percent().get()); + + EXPECT_TRUE(subnet->getMatchClientId().unspecified()); + EXPECT_TRUE(subnet->getMatchClientId().get()); + + EXPECT_TRUE(subnet->getAuthoritative().unspecified()); + EXPECT_FALSE(subnet->getAuthoritative().get()); + + EXPECT_TRUE(subnet->getSiaddr().unspecified()); + EXPECT_TRUE(subnet->getSiaddr().get().isV4Zero()); + + EXPECT_TRUE(subnet->getSname().unspecified()); + EXPECT_TRUE(subnet->getSname().empty()); + + EXPECT_TRUE(subnet->getFilename().unspecified()); + EXPECT_TRUE(subnet->getFilename().empty()); + + EXPECT_FALSE(subnet->get4o6().enabled()); + + EXPECT_TRUE(subnet->get4o6().getIface4o6().unspecified()); + EXPECT_TRUE(subnet->get4o6().getIface4o6().empty()); + + EXPECT_TRUE(subnet->get4o6().getSubnet4o6().unspecified()); + EXPECT_TRUE(subnet->get4o6().getSubnet4o6().get().first.isV6Zero()); + EXPECT_EQ(128, subnet->get4o6().getSubnet4o6().get().second); + + EXPECT_TRUE(subnet->getDdnsSendUpdates().unspecified()); + EXPECT_FALSE(subnet->getDdnsSendUpdates().get()); + + EXPECT_TRUE(subnet->getDdnsOverrideNoUpdate().unspecified()); + EXPECT_FALSE(subnet->getDdnsOverrideNoUpdate().get()); + + EXPECT_TRUE(subnet->getDdnsOverrideClientUpdate().unspecified()); + EXPECT_FALSE(subnet->getDdnsOverrideClientUpdate().get()); + + EXPECT_TRUE(subnet->getDdnsReplaceClientNameMode().unspecified()); + EXPECT_EQ(D2ClientConfig::RCM_NEVER, subnet->getDdnsReplaceClientNameMode().get()); + + EXPECT_TRUE(subnet->getDdnsGeneratedPrefix().unspecified()); + EXPECT_TRUE(subnet->getDdnsGeneratedPrefix().empty()); + + EXPECT_TRUE(subnet->getDdnsQualifyingSuffix().unspecified()); + EXPECT_TRUE(subnet->getDdnsQualifyingSuffix().empty()); + + EXPECT_TRUE(subnet->getHostnameCharSet().unspecified()); + EXPECT_TRUE(subnet->getHostnameCharSet().empty()); + + EXPECT_TRUE(subnet->getHostnameCharReplacement().unspecified()); + EXPECT_TRUE(subnet->getHostnameCharReplacement().empty()); + + EXPECT_TRUE(subnet->getStoreExtendedInfo().unspecified()); + EXPECT_FALSE(subnet->getStoreExtendedInfo().get()); + + EXPECT_TRUE(subnet->getDdnsUpdateOnRenew().unspecified()); + EXPECT_FALSE(subnet->getDdnsUpdateOnRenew().get()); + + EXPECT_TRUE(subnet->getDdnsUseConflictResolution().unspecified()); + EXPECT_FALSE(subnet->getDdnsUseConflictResolution().get()); + + EXPECT_TRUE(subnet->getAllocationState(Lease::TYPE_V4)); + EXPECT_TRUE(subnet->getAllocator(Lease::TYPE_V4)); + + auto allocator = subnet->getAllocator(Lease::TYPE_V4); + EXPECT_TRUE(boost::dynamic_pointer_cast<IterativeAllocator>(allocator)); + + EXPECT_TRUE(subnet->getOfferLft().unspecified()); + EXPECT_EQ(0, subnet->getOfferLft().get()); +} + +// This test verifies that it is possible to parse an IPv6 subnet for which +// only mandatory parameters are specified without setting the defaults. +TEST_F(ParseConfigTest, defaultSubnet6) { + std::string config = + "{" + " \"subnet6\": [ {" + " \"subnet\": \"2001:db8:1::/64\"," + " \"id\": 123" + " } ]" + "}"; + + int rcode = parseConfiguration(config, true, false); + ASSERT_EQ(0, rcode); + + auto subnet = CfgMgr::instance().getStagingCfg()->getCfgSubnets6()->getBySubnetId(123); + ASSERT_TRUE(subnet); + + EXPECT_TRUE(subnet->hasFetchGlobalsFn()); + + EXPECT_TRUE(subnet->getIface().unspecified()); + EXPECT_TRUE(subnet->getIface().empty()); + + EXPECT_TRUE(subnet->getClientClass().unspecified()); + EXPECT_TRUE(subnet->getClientClass().empty()); + + EXPECT_TRUE(subnet->getValid().unspecified()); + EXPECT_EQ(0, subnet->getValid().get()); + + EXPECT_TRUE(subnet->getT1().unspecified()); + EXPECT_EQ(0, subnet->getT1().get()); + + EXPECT_TRUE(subnet->getT2().unspecified()); + EXPECT_EQ(0, subnet->getT2().get()); + + EXPECT_TRUE(subnet->getReservationsGlobal().unspecified()); + EXPECT_FALSE(subnet->getReservationsGlobal().get()); + + EXPECT_TRUE(subnet->getReservationsInSubnet().unspecified()); + EXPECT_TRUE(subnet->getReservationsInSubnet().get()); + + EXPECT_TRUE(subnet->getReservationsOutOfPool().unspecified()); + EXPECT_FALSE(subnet->getReservationsOutOfPool().get()); + + EXPECT_TRUE(subnet->getCalculateTeeTimes().unspecified()); + EXPECT_FALSE(subnet->getCalculateTeeTimes().get()); + + EXPECT_TRUE(subnet->getT1Percent().unspecified()); + EXPECT_EQ(0.0, subnet->getT1Percent().get()); + + EXPECT_TRUE(subnet->getT2Percent().unspecified()); + EXPECT_EQ(0.0, subnet->getT2Percent().get()); + + EXPECT_TRUE(subnet->getPreferred().unspecified()); + EXPECT_EQ(0, subnet->getPreferred().get()); + + EXPECT_TRUE(subnet->getRapidCommit().unspecified()); + EXPECT_FALSE(subnet->getRapidCommit().get()); + + EXPECT_TRUE(subnet->getDdnsSendUpdates().unspecified()); + EXPECT_FALSE(subnet->getDdnsSendUpdates().get()); + + EXPECT_TRUE(subnet->getDdnsOverrideNoUpdate().unspecified()); + EXPECT_FALSE(subnet->getDdnsOverrideNoUpdate().get()); + + EXPECT_TRUE(subnet->getDdnsOverrideClientUpdate().unspecified()); + EXPECT_FALSE(subnet->getDdnsOverrideClientUpdate().get()); + + EXPECT_TRUE(subnet->getDdnsReplaceClientNameMode().unspecified()); + EXPECT_EQ(D2ClientConfig::RCM_NEVER, subnet->getDdnsReplaceClientNameMode().get()); + + EXPECT_TRUE(subnet->getDdnsGeneratedPrefix().unspecified()); + EXPECT_TRUE(subnet->getDdnsGeneratedPrefix().empty()); + + EXPECT_TRUE(subnet->getDdnsQualifyingSuffix().unspecified()); + EXPECT_TRUE(subnet->getDdnsQualifyingSuffix().empty()); + + EXPECT_TRUE(subnet->getHostnameCharSet().unspecified()); + EXPECT_TRUE(subnet->getHostnameCharSet().empty()); + + EXPECT_TRUE(subnet->getHostnameCharReplacement().unspecified()); + EXPECT_TRUE(subnet->getHostnameCharReplacement().empty()); + + EXPECT_TRUE(subnet->getStoreExtendedInfo().unspecified()); + EXPECT_FALSE(subnet->getStoreExtendedInfo().get()); + + EXPECT_TRUE(subnet->getDdnsUpdateOnRenew().unspecified()); + EXPECT_FALSE(subnet->getDdnsUpdateOnRenew().get()); + + EXPECT_TRUE(subnet->getDdnsUseConflictResolution().unspecified()); + EXPECT_FALSE(subnet->getDdnsUseConflictResolution().get()); + + EXPECT_TRUE(subnet->getAllocationState(Lease::TYPE_NA)); + EXPECT_TRUE(subnet->getAllocationState(Lease::TYPE_TA)); + EXPECT_TRUE(subnet->getAllocationState(Lease::TYPE_PD)); + + auto allocator = subnet->getAllocator(Lease::TYPE_NA); + EXPECT_TRUE(boost::dynamic_pointer_cast<IterativeAllocator>(allocator)); + + allocator = subnet->getAllocator(Lease::TYPE_TA); + EXPECT_TRUE(boost::dynamic_pointer_cast<IterativeAllocator>(allocator)); + + allocator = subnet->getAllocator(Lease::TYPE_PD); + EXPECT_TRUE(boost::dynamic_pointer_cast<IterativeAllocator>(allocator)); +} + +// This test verifies that it is possible to parse an IPv4 shared network +// for which only mandatory parameter is specified without setting the +// defaults. +TEST_F(ParseConfigTest, defaultSharedNetwork4) { + std::string config = + "{" + " \"shared-networks\": [ {" + " \"name\": \"frog\"" + " } ]" + "}"; + + int rcode = parseConfiguration(config, false, false); + ASSERT_EQ(0, rcode); + + auto network = + CfgMgr::instance().getStagingCfg()->getCfgSharedNetworks4()->getByName("frog"); + ASSERT_TRUE(network); + + EXPECT_TRUE(network->hasFetchGlobalsFn()); + EXPECT_TRUE(network->getIface().unspecified()); + EXPECT_TRUE(network->getIface().empty()); + + EXPECT_TRUE(network->getClientClass().unspecified()); + EXPECT_TRUE(network->getClientClass().empty()); + + EXPECT_TRUE(network->getValid().unspecified()); + EXPECT_EQ(0, network->getValid().get()); + + EXPECT_TRUE(network->getT1().unspecified()); + EXPECT_EQ(0, network->getT1().get()); + + EXPECT_TRUE(network->getT2().unspecified()); + EXPECT_EQ(0, network->getT2().get()); + + EXPECT_TRUE(network->getReservationsGlobal().unspecified()); + EXPECT_FALSE(network->getReservationsGlobal().get()); + + EXPECT_TRUE(network->getReservationsInSubnet().unspecified()); + EXPECT_TRUE(network->getReservationsInSubnet().get()); + + EXPECT_TRUE(network->getReservationsOutOfPool().unspecified()); + EXPECT_FALSE(network->getReservationsOutOfPool().get()); + + EXPECT_TRUE(network->getCalculateTeeTimes().unspecified()); + EXPECT_FALSE(network->getCalculateTeeTimes().get()); + + EXPECT_TRUE(network->getT1Percent().unspecified()); + EXPECT_EQ(0.0, network->getT1Percent().get()); + + EXPECT_TRUE(network->getT2Percent().unspecified()); + EXPECT_EQ(0.0, network->getT2Percent().get()); + + EXPECT_TRUE(network->getMatchClientId().unspecified()); + EXPECT_TRUE(network->getMatchClientId().get()); + + EXPECT_TRUE(network->getAuthoritative().unspecified()); + EXPECT_FALSE(network->getAuthoritative().get()); + + EXPECT_TRUE(network->getDdnsSendUpdates().unspecified()); + EXPECT_FALSE(network->getDdnsSendUpdates().get()); + + EXPECT_TRUE(network->getDdnsOverrideNoUpdate().unspecified()); + EXPECT_FALSE(network->getDdnsOverrideNoUpdate().get()); + + EXPECT_TRUE(network->getDdnsOverrideClientUpdate().unspecified()); + EXPECT_FALSE(network->getDdnsOverrideClientUpdate().get()); + + EXPECT_TRUE(network->getDdnsReplaceClientNameMode().unspecified()); + EXPECT_EQ(D2ClientConfig::RCM_NEVER, network->getDdnsReplaceClientNameMode().get()); + + EXPECT_TRUE(network->getDdnsGeneratedPrefix().unspecified()); + EXPECT_TRUE(network->getDdnsGeneratedPrefix().empty()); + + EXPECT_TRUE(network->getDdnsQualifyingSuffix().unspecified()); + EXPECT_TRUE(network->getDdnsQualifyingSuffix().empty()); + + EXPECT_TRUE(network->getStoreExtendedInfo().unspecified()); + EXPECT_FALSE(network->getStoreExtendedInfo().get()); + + EXPECT_TRUE(network->getDdnsUpdateOnRenew().unspecified()); + EXPECT_FALSE(network->getDdnsUpdateOnRenew().get()); + + EXPECT_TRUE(network->getDdnsUseConflictResolution().unspecified()); + EXPECT_FALSE(network->getDdnsUseConflictResolution().get()); + + EXPECT_TRUE(network->getAllocatorType().unspecified()); + EXPECT_TRUE(network->getAllocatorType().get().empty()); + + EXPECT_TRUE(network->getOfferLft().unspecified()); + EXPECT_EQ(0, network->getOfferLft().get()); +} + +// This test verifies that it is possible to parse an IPv6 shared network +// for which only mandatory parameter is specified without setting the +// defaults. +TEST_F(ParseConfigTest, defaultSharedNetwork6) { + std::string config = + "{" + " \"shared-networks\": [ {" + " \"name\": \"frog\"" + " } ]" + "}"; + + int rcode = parseConfiguration(config, true, false); + ASSERT_EQ(0, rcode); + + auto network = + CfgMgr::instance().getStagingCfg()->getCfgSharedNetworks6()->getByName("frog"); + ASSERT_TRUE(network); + + EXPECT_TRUE(network->hasFetchGlobalsFn()); + + EXPECT_TRUE(network->getIface().unspecified()); + EXPECT_TRUE(network->getIface().empty()); + + EXPECT_TRUE(network->getClientClass().unspecified()); + EXPECT_TRUE(network->getClientClass().empty()); + + EXPECT_TRUE(network->getValid().unspecified()); + EXPECT_EQ(0, network->getValid().get()); + + EXPECT_TRUE(network->getT1().unspecified()); + EXPECT_EQ(0, network->getT1().get()); + + EXPECT_TRUE(network->getT2().unspecified()); + EXPECT_EQ(0, network->getT2().get()); + + EXPECT_TRUE(network->getReservationsGlobal().unspecified()); + EXPECT_FALSE(network->getReservationsGlobal().get()); + + EXPECT_TRUE(network->getReservationsInSubnet().unspecified()); + EXPECT_TRUE(network->getReservationsInSubnet().get()); + + EXPECT_TRUE(network->getReservationsOutOfPool().unspecified()); + EXPECT_FALSE(network->getReservationsOutOfPool().get()); + + EXPECT_TRUE(network->getCalculateTeeTimes().unspecified()); + EXPECT_FALSE(network->getCalculateTeeTimes().get()); + + EXPECT_TRUE(network->getT1Percent().unspecified()); + EXPECT_EQ(0.0, network->getT1Percent().get()); + + EXPECT_TRUE(network->getT2Percent().unspecified()); + EXPECT_EQ(0.0, network->getT2Percent().get()); + + EXPECT_TRUE(network->getPreferred().unspecified()); + EXPECT_EQ(0, network->getPreferred().get()); + + EXPECT_TRUE(network->getRapidCommit().unspecified()); + EXPECT_FALSE(network->getRapidCommit().get()); + + EXPECT_TRUE(network->getDdnsSendUpdates().unspecified()); + EXPECT_FALSE(network->getDdnsSendUpdates().get()); + + EXPECT_TRUE(network->getDdnsOverrideNoUpdate().unspecified()); + EXPECT_FALSE(network->getDdnsOverrideNoUpdate().get()); + + EXPECT_TRUE(network->getDdnsOverrideClientUpdate().unspecified()); + EXPECT_FALSE(network->getDdnsOverrideClientUpdate().get()); + + EXPECT_TRUE(network->getDdnsReplaceClientNameMode().unspecified()); + EXPECT_EQ(D2ClientConfig::RCM_NEVER, network->getDdnsReplaceClientNameMode().get()); + + EXPECT_TRUE(network->getDdnsGeneratedPrefix().unspecified()); + EXPECT_TRUE(network->getDdnsGeneratedPrefix().empty()); + + EXPECT_TRUE(network->getDdnsQualifyingSuffix().unspecified()); + EXPECT_TRUE(network->getDdnsQualifyingSuffix().empty()); + + EXPECT_TRUE(network->getStoreExtendedInfo().unspecified()); + EXPECT_FALSE(network->getStoreExtendedInfo().get()); + + EXPECT_TRUE(network->getDdnsUpdateOnRenew().unspecified()); + EXPECT_FALSE(network->getDdnsUpdateOnRenew().get()); + + EXPECT_TRUE(network->getDdnsUseConflictResolution().unspecified()); + EXPECT_FALSE(network->getDdnsUseConflictResolution().get()); + + EXPECT_TRUE(network->getAllocatorType().unspecified()); + EXPECT_TRUE(network->getAllocatorType().get().empty()); + + EXPECT_TRUE(network->getPdAllocatorType().unspecified()); + EXPECT_TRUE(network->getPdAllocatorType().get().empty()); +} + +// This test verifies a negative value for the subnet ID is rejected (v4). +TEST_F(ParseConfigTest, negativeSubnetId4) { + std::string config = + "{" + " \"subnet4\": [ {" + " \"subnet\": \"192.0.2.0/24\"," + " \"id\": -1" + " } ]" + "}"; + + ElementPtr json = Element::fromJSON(config); + EXPECT_TRUE(json); + ConstElementPtr status = parseElementSet(json, false); + int rcode = 0; + ConstElementPtr comment = parseAnswer(rcode, status); + ASSERT_TRUE(comment); + ASSERT_EQ(comment->getType(), Element::string); + EXPECT_EQ(1, rcode); + std::string expected = "Configuration parsing failed: "; + expected += "subnet configuration failed: "; + expected += "The 'id' value (-1) is not within expected range: "; + expected += "(0 - 4294967294)"; + EXPECT_EQ(expected, comment->stringValue()); +} + +// This test verifies a negative value for the subnet ID is rejected (v6). +TEST_F(ParseConfigTest, negativeSubnetId6) { + std::string config = + "{" + " \"subnet6\": [ {" + " \"subnet\": \"2001:db8:1::/64\"," + " \"id\": -1" + " } ]" + "}"; + + ElementPtr json = Element::fromJSON(config); + EXPECT_TRUE(json); + ConstElementPtr status = parseElementSet(json, true); + int rcode = 0; + ConstElementPtr comment = parseAnswer(rcode, status); + ASSERT_TRUE(comment); + ASSERT_EQ(comment->getType(), Element::string); + EXPECT_EQ(1, rcode); + std::string expected = "Configuration parsing failed: "; + expected += "subnet configuration failed: "; + expected += "The 'id' value (-1) is not within expected range: "; + expected += "(0 - 4294967294)"; + EXPECT_EQ(expected, comment->stringValue()); +} + +// This test verifies a too high value for the subnet ID is rejected (v4). +TEST_F(ParseConfigTest, reservedSubnetId4) { + std::string config = + "{" + " \"subnet4\": [ {" + " \"subnet\": \"192.0.2.0/24\"," + " \"id\": 4294967295" + " } ]" + "}"; + + ElementPtr json = Element::fromJSON(config); + EXPECT_TRUE(json); + ConstElementPtr status = parseElementSet(json, false); + int rcode = 0; + ConstElementPtr comment = parseAnswer(rcode, status); + ASSERT_TRUE(comment); + ASSERT_EQ(comment->getType(), Element::string); + EXPECT_EQ(1, rcode); + std::string expected = "Configuration parsing failed: "; + expected += "subnet configuration failed: "; + expected += "The 'id' value (4294967295) is not within expected range: "; + expected += "(0 - 4294967294)"; + EXPECT_EQ(expected, comment->stringValue()); +} + +// This test verifies a too high value for the subnet ID is rejected (v6). +TEST_F(ParseConfigTest, reservedSubnetId6) { + std::string config = + "{" + " \"subnet6\": [ {" + " \"subnet\": \"2001:db8:1::/64\"," + " \"id\": 4294967295" + " } ]" + "}"; + + ElementPtr json = Element::fromJSON(config); + EXPECT_TRUE(json); + ConstElementPtr status = parseElementSet(json, true); + int rcode = 0; + ConstElementPtr comment = parseAnswer(rcode, status); + ASSERT_TRUE(comment); + ASSERT_EQ(comment->getType(), Element::string); + EXPECT_EQ(1, rcode); + std::string expected = "Configuration parsing failed: "; + expected += "subnet configuration failed: "; + expected += "The 'id' value (4294967295) is not within expected range: "; + expected += "(0 - 4294967294)"; + EXPECT_EQ(expected, comment->stringValue()); +} + +// This test verifies that random allocator can be selected for +// a subnet. +TEST_F(ParseConfigTest, randomSubnetAllocator4) { + std::string config = + "{" + " \"subnet4\": [ {" + " \"subnet\": \"192.0.2.0/24\"," + " \"id\": 1," + " \"allocator\": \"random\"" + " } ]" + "}"; + + ElementPtr json = Element::fromJSON(config); + EXPECT_TRUE(json); + ConstElementPtr status = parseElementSet(json, false); + int rcode = 0; + ConstElementPtr comment = parseAnswer(rcode, status); + ASSERT_EQ(0, rcode); + + auto subnet = CfgMgr::instance().getStagingCfg()->getCfgSubnets4()->getBySubnetId(1); + ASSERT_TRUE(subnet); + + EXPECT_EQ("random", subnet->getAllocatorType().get()); + auto allocator = subnet->getAllocator(Lease::TYPE_V4); + ASSERT_TRUE(allocator); + EXPECT_TRUE(boost::dynamic_pointer_cast<RandomAllocator>(allocator)); +} + +// This test verifies that Free Lease Queue allocator can be selected for +// a subnet. +TEST_F(ParseConfigTest, flqSubnetAllocator4) { + std::string config = + "{" + " \"subnet4\": [ {" + " \"subnet\": \"192.0.2.0/24\"," + " \"id\": 1," + " \"allocator\": \"flq\"" + " } ]" + "}"; + + ElementPtr json = Element::fromJSON(config); + EXPECT_TRUE(json); + ConstElementPtr status = parseElementSet(json, false); + int rcode = 0; + ConstElementPtr comment = parseAnswer(rcode, status); + ASSERT_EQ(0, rcode); + + auto subnet = CfgMgr::instance().getStagingCfg()->getCfgSubnets4()->getBySubnetId(1); + ASSERT_TRUE(subnet); + + EXPECT_EQ("flq", subnet->getAllocatorType().get()); + auto allocator = subnet->getAllocator(Lease::TYPE_V4); + ASSERT_TRUE(allocator); + EXPECT_TRUE(boost::dynamic_pointer_cast<FreeLeaseQueueAllocator>(allocator)); +} + +// This test verifies that unknown allocator is rejected. +TEST_F(ParseConfigTest, invalidSubnetAllocator4) { + std::string config = + "{" + " \"subnet4\": [ {" + " \"subnet\": \"192.0.2.0/24\"," + " \"id\": 1," + " \"allocator\": \"unsupported\"" + " } ]" + "}"; + + ElementPtr json = Element::fromJSON(config); + EXPECT_TRUE(json); + ConstElementPtr status = parseElementSet(json, false); + int rcode = 0; + ConstElementPtr comment = parseAnswer(rcode, status); + ASSERT_TRUE(comment); + ASSERT_EQ(comment->getType(), Element::string); + EXPECT_EQ(1, rcode); + std::string expected = "Configuration parsing failed: "; + expected += "supported allocators are: iterative, random and flq"; + EXPECT_EQ(expected, comment->stringValue()); +} + +// This test verifies that random allocator can be selected for +// a subnet. +TEST_F(ParseConfigTest, randomSubnetAllocator6) { + std::string config = + "{" + " \"subnet6\": [ {" + " \"subnet\": \"2001:db8:1::/64\"," + " \"id\": 1," + " \"allocator\": \"random\"" + " } ]" + "}"; + + ElementPtr json = Element::fromJSON(config); + EXPECT_TRUE(json); + ConstElementPtr status = parseElementSet(json, false); + int rcode = 0; + ConstElementPtr comment = parseAnswer(rcode, status); + ASSERT_EQ(0, rcode); + + auto subnet = CfgMgr::instance().getStagingCfg()->getCfgSubnets6()->getBySubnetId(1); + ASSERT_TRUE(subnet); + + EXPECT_EQ("random", subnet->getAllocatorType().get()); + auto allocator = subnet->getAllocator(Lease::TYPE_NA); + ASSERT_TRUE(allocator); + EXPECT_TRUE(boost::dynamic_pointer_cast<RandomAllocator>(allocator)); + allocator = subnet->getAllocator(Lease::TYPE_TA); + ASSERT_TRUE(allocator); + EXPECT_TRUE(boost::dynamic_pointer_cast<RandomAllocator>(allocator)); + // PD allocator should be iterative. + allocator = subnet->getAllocator(Lease::TYPE_PD); + ASSERT_TRUE(allocator); + EXPECT_TRUE(boost::dynamic_pointer_cast<IterativeAllocator>(allocator)); +} + +// This test verifies that FLQ allocator is not supported for +// IPv6 address pools. +TEST_F(ParseConfigTest, flqSubnetAllocator6) { + std::string config = + "{" + " \"subnet6\": [ {" + " \"subnet\": \"2001:db8:1::/64\"," + " \"id\": 1," + " \"allocator\": \"flq\"" + " } ]" + "}"; + + ElementPtr json = Element::fromJSON(config); + EXPECT_TRUE(json); + ConstElementPtr status = parseElementSet(json, false); + int rcode = 0; + ConstElementPtr comment = parseAnswer(rcode, status); + ASSERT_TRUE(comment); + ASSERT_EQ(comment->getType(), Element::string); + EXPECT_EQ(1, rcode); + std::string expected = "Configuration parsing failed: "; + expected += "Free Lease Queue allocator is not supported for IPv6 address pools"; + EXPECT_EQ(expected, comment->stringValue()); +} + +// This test verifies that unknown allocator is rejected. +TEST_F(ParseConfigTest, invalidSubnetAllocator6) { + std::string config = + "{" + " \"subnet6\": [ {" + " \"subnet\": \"2001:db8:1::/64\"," + " \"id\": 1," + " \"allocator\": \"unsupported\"" + " } ]" + "}"; + + ElementPtr json = Element::fromJSON(config); + EXPECT_TRUE(json); + ConstElementPtr status = parseElementSet(json, false); + int rcode = 0; + ConstElementPtr comment = parseAnswer(rcode, status); + ASSERT_TRUE(comment); + ASSERT_EQ(comment->getType(), Element::string); + EXPECT_EQ(1, rcode); + std::string expected = "Configuration parsing failed: "; + expected += "supported allocators are: iterative, random and flq"; + EXPECT_EQ(expected, comment->stringValue()); +} + +// This test verifies that random allocator can be selected for +// a subnet. +TEST_F(ParseConfigTest, randomSubnetPdAllocator6) { + std::string config = + "{" + " \"subnet6\": [ {" + " \"subnet\": \"2001:db8:1::/64\"," + " \"id\": 1," + " \"pd-allocator\": \"random\"" + " } ]" + "}"; + + ElementPtr json = Element::fromJSON(config); + EXPECT_TRUE(json); + ConstElementPtr status = parseElementSet(json, false); + int rcode = 0; + ConstElementPtr comment = parseAnswer(rcode, status); + ASSERT_EQ(0, rcode); + + auto subnet = CfgMgr::instance().getStagingCfg()->getCfgSubnets6()->getBySubnetId(1); + ASSERT_TRUE(subnet); + + EXPECT_EQ("random", subnet->getPdAllocatorType().get()); + + // Address allocators should be iterative. + auto allocator = subnet->getAllocator(Lease::TYPE_NA); + ASSERT_TRUE(allocator); + EXPECT_TRUE(boost::dynamic_pointer_cast<IterativeAllocator>(allocator)); + allocator = subnet->getAllocator(Lease::TYPE_TA); + ASSERT_TRUE(allocator); + EXPECT_TRUE(boost::dynamic_pointer_cast<IterativeAllocator>(allocator)); + // PD allocator should be random. + allocator = subnet->getAllocator(Lease::TYPE_PD); + ASSERT_TRUE(allocator); + EXPECT_TRUE(boost::dynamic_pointer_cast<RandomAllocator>(allocator)); +} + +// This test verifies that the FLQ allocator can be selected for +// a v6 subnet's pd-allocator. +TEST_F(ParseConfigTest, flqSubnetPdAllocator6) { + std::string config = + "{" + " \"subnet6\": [ {" + " \"subnet\": \"2001:db8:1::/64\"," + " \"id\": 1," + " \"pd-allocator\": \"flq\"" + " } ]" + "}"; + + ElementPtr json = Element::fromJSON(config); + EXPECT_TRUE(json); + ConstElementPtr status = parseElementSet(json, false); + int rcode = 0; + ConstElementPtr comment = parseAnswer(rcode, status); + ASSERT_EQ(0, rcode); + + auto subnet = CfgMgr::instance().getStagingCfg()->getCfgSubnets6()->getBySubnetId(1); + ASSERT_TRUE(subnet); + + EXPECT_EQ("flq", subnet->getPdAllocatorType().get()); + + // Address allocators should be iterative. + auto allocator = subnet->getAllocator(Lease::TYPE_NA); + ASSERT_TRUE(allocator); + EXPECT_TRUE(boost::dynamic_pointer_cast<IterativeAllocator>(allocator)); + allocator = subnet->getAllocator(Lease::TYPE_TA); + ASSERT_TRUE(allocator); + EXPECT_TRUE(boost::dynamic_pointer_cast<IterativeAllocator>(allocator)); + // PD allocator should use FLQ. + allocator = subnet->getAllocator(Lease::TYPE_PD); + ASSERT_TRUE(allocator); + EXPECT_TRUE(boost::dynamic_pointer_cast<FreeLeaseQueueAllocator>(allocator)); +} + +// This test verifies that unknown prefix delegation allocator is rejected. +TEST_F(ParseConfigTest, invalidSubnetPdAllocator6) { + std::string config = + "{" + " \"subnet6\": [ {" + " \"subnet\": \"2001:db8:1::/64\"," + " \"id\": 1," + " \"pd-allocator\": \"unsupported\"" + " } ]" + "}"; + + ElementPtr json = Element::fromJSON(config); + EXPECT_TRUE(json); + ConstElementPtr status = parseElementSet(json, false); + int rcode = 0; + ConstElementPtr comment = parseAnswer(rcode, status); + ASSERT_TRUE(comment); + ASSERT_EQ(comment->getType(), Element::string); + EXPECT_EQ(1, rcode); + std::string expected = "Configuration parsing failed: "; + expected += "supported allocators are: iterative, random and flq"; + EXPECT_EQ(expected, comment->stringValue()); +} + +// There's no test for ControlSocketParser, as it is tested in the DHCPv4 code +// (see CtrlDhcpv4SrvTest.commandSocketBasic in +// src/bin/dhcp4/tests/ctrl_dhcp4_srv_unittest.cc). + + +// Verifies that parsing an option which encapsulates its own option space +// is detected. +TEST_F(ParseConfigTest, selfEncapsulationTest) { + // Verify that the option definition can be retrieved. + OptionDefinitionPtr def = LibDHCP::getOptionDef(DHCP6_OPTION_SPACE, 45); + ASSERT_TRUE(def); + + // Configuration string. + std::string config = + "{" + " \"option-data\": [" + "{" + " \"name\": \"client-data\"," + " \"code\": 45," + " \"csv-format\": false," + " \"space\": \"dhcp6\"," + " \"data\": \"0001000B0102020202030303030303\"" + "}" + "]}"; + + // Verify that the configuration string parses. + family_ = AF_INET6; + + int rcode = parseConfiguration(config, true, true); + ASSERT_EQ(0, rcode); + + // Verify that the option can be retrieved. + OptionCustomPtr opt = boost::dynamic_pointer_cast<OptionCustom> + (getOptionPtr(DHCP6_OPTION_SPACE, D6O_CLIENT_DATA)); + ASSERT_TRUE(opt); + + // Verify length is correct and doesn't infinitely recurse. + EXPECT_EQ(19, opt->len()); + + // Check if it can be unparsed. + CfgOptionsTest cfg(CfgMgr::instance().getStagingCfg()); + cfg.runCfgOptionsTest(family_, config); +} + +// This test verifies parsing offer-lifetime for Subnet4. +TEST_F(ParseConfigTest, subnet4OfferLft) { + std::string config = + "{" + " \"subnet4\": [ {" + " \"subnet\": \"192.0.2.0/24\"," + " \"id\": 123," + " \"offer-lifetime\": 888" + " } ]" + "}"; + + int rcode = parseConfiguration(config, false, false); + ASSERT_EQ(0, rcode); + + auto subnet = CfgMgr::instance().getStagingCfg()->getCfgSubnets4()->getBySubnetId(123); + ASSERT_TRUE(subnet); + + EXPECT_FALSE(subnet->getOfferLft().unspecified()); + EXPECT_EQ(888, subnet->getOfferLft().get()); +} + +// This test verifies parsing invalid offer-lifetime for Subnet4. +TEST_F(ParseConfigTest, subnet4InvalidOfferLft) { + std::string config = + "{" + " \"subnet4\": [ {" + " \"subnet\": \"192.0.2.0/24\"," + " \"id\": 123," + " \"offer-lifetime\": -77" + " } ]" + "}"; + + int rcode = parseConfiguration(config, false, false); + ASSERT_NE(0, rcode); +} + + +} // Anonymous namespace |