diff options
Diffstat (limited to 'src/bin/d2/tests/d2_cfg_mgr_unittests.cc')
-rw-r--r-- | src/bin/d2/tests/d2_cfg_mgr_unittests.cc | 1080 |
1 files changed, 1080 insertions, 0 deletions
diff --git a/src/bin/d2/tests/d2_cfg_mgr_unittests.cc b/src/bin/d2/tests/d2_cfg_mgr_unittests.cc new file mode 100644 index 0000000..5e27ff8 --- /dev/null +++ b/src/bin/d2/tests/d2_cfg_mgr_unittests.cc @@ -0,0 +1,1080 @@ +// Copyright (C) 2013-2021 Internet Systems Consortium, Inc. ("ISC") +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +#include <config.h> + +#include <d2/parser_context.h> +#include <d2/tests/parser_unittest.h> +#include <d2/tests/test_callout_libraries.h> +#include <d2srv/d2_cfg_mgr.h> +#include <d2srv/d2_config.h> +#include <d2srv/d2_simple_parser.h> +#include <dhcpsrv/testutils/config_result_check.h> +#include <process/testutils/d_test_stubs.h> +#include <test_data_files_config.h> +#include <util/encode/base64.h> + +#include <boost/foreach.hpp> +#include <boost/scoped_ptr.hpp> +#include <gtest/gtest.h> + +using namespace std; +using namespace isc; +using namespace isc::d2; +using namespace isc::hooks; +using namespace isc::process; + +namespace { + +/// @brief Function to create full path to test data file +/// +/// The full path is dependent upon the value of D2_TEST_DATA_DIR which +/// whose value is generated from test_data_files_config.h.in +/// +/// @param name file name to which the path should be prepended +std::string testDataFile(const std::string& name) { + return (std::string(D2_TEST_DATA_DIR) + "/" + name); +} + +/// @brief Test fixture class for testing D2CfgMgr class. +/// It maintains an member instance of D2CfgMgr and provides methods for +/// converting JSON strings to configuration element sets, checking parse +/// results, and accessing the configuration context. +class D2CfgMgrTest : public ConfigParseTest { +public: + + /// @brief Constructor + D2CfgMgrTest():cfg_mgr_(new D2CfgMgr()), d2_params_() { + } + + /// @brief Destructor + ~D2CfgMgrTest() { + } + + /// @brief Configuration manager instance. + D2CfgMgrPtr cfg_mgr_; + + /// @brief Build JSON configuration string for a D2Params element + /// + /// Constructs a JSON string for "params" element using replaceable + /// parameters. + /// + /// @param ip_address string to insert as ip_address value + /// @param port integer to insert as port value + /// @param dns_server_timeout integer to insert as dns_server_timeout value + /// @param ncr_protocol string to insert as ncr_protocol value + /// @param ncr_format string to insert as ncr_format value + /// + /// @return std::string containing the JSON configuration text + std::string makeParamsConfigString(const std::string& ip_address, + const int port, + const int dns_server_timeout, + const std::string& ncr_protocol, + const std::string& ncr_format) { + std::ostringstream config; + config << + "{" + " \"ip-address\": \"" << ip_address << "\" , " + " \"port\": " << port << " , " + " \"dns-server-timeout\": " << dns_server_timeout << " , " + " \"ncr-protocol\": \"" << ncr_protocol << "\" , " + " \"ncr-format\": \"" << ncr_format << "\", " + " \"tsig-keys\": [], " + " \"forward-ddns\" : {}, " + " \"reverse-ddns\" : {} " + "}"; + + return (config.str()); + } + + /// @brief Enumeration to select between expected configuration outcomes + enum RunConfigMode { + NO_ERROR, + SYNTAX_ERROR, + LOGIC_ERROR + }; + + /// @brief Parses a configuration string and tests against a given outcome + /// + /// Convenience method which accepts JSON text and an expected pass or fail + /// outcome. It uses the D2ParserContext to parse the text under the + /// PARSE_SUB_DHCPDDNS context, then adds the D2 defaults to the resultant + /// element tree. Assuming that's successful the element tree is passed + /// to D2CfgMgr::parseConfig() method. + /// + /// @param config_str the JSON configuration text to parse + /// @param error_type indicates the type error expected, NONE, SYNTAX, + /// or LOGIC. SYNTAX errors are emitted by JSON parser, logic errors + /// are emitted by element parser(s). + /// @param exp_error exact text of the error message expected + /// defaults to SHOULD_PASS. + /// + /// @return AssertionSuccess if test passes, AssertionFailure otherwise + ::testing::AssertionResult runConfigOrFail(const std::string& json, + const RunConfigMode mode, + const std::string& exp_error) { + + try { + // Invoke the JSON parser, casting the returned element tree + // into mutable form. + D2ParserContext parser_context; + data::ElementPtr elem = + boost::const_pointer_cast<Element> + (parser_context.parseString(json, D2ParserContext:: + PARSER_SUB_DHCPDDNS)); + + // If parsing succeeded when we expected a syntax error, then fail. + if (mode == SYNTAX_ERROR) { + return ::testing::AssertionFailure() + << "Unexpected JSON parsing success" + << "\njson: [" << json << " ]"; + } + + // JSON parsed ok, so add the defaults to the element tree it produced. + D2SimpleParser::setAllDefaults(elem); + config_set_ = elem; + } catch (const std::exception& ex) { + // JSON Parsing failed + if (exp_error.empty()) { + // We did not expect an error, so fail. + return ::testing::AssertionFailure() + << "Unexpected syntax error:" << ex.what() + << "\njson: [" << json << " ]"; + } + + if (ex.what() != exp_error) { + // Expected an error not the one we got, so fail + return ::testing::AssertionFailure() + << "Wrong syntax error detected, expected: " + << exp_error << ", got: " << ex.what() + << "\njson: [" << json << " ]"; + } + + // We go the syntax error we expected, so return success + return ::testing::AssertionSuccess(); + } + + // The JSON parsed ok and we've added the defaults, pass the config + // into the Element parser and check for the expected outcome. + data::ConstElementPtr answer; + answer = cfg_mgr_->simpleParseConfig(config_set_, false); + + // Extract the result and error text from the answer. + int rcode = 0; + isc::data::ConstElementPtr comment; + comment = isc::config::parseAnswer(rcode, answer); + + if (rcode != 0) { + // Element Parsing failed. + if (exp_error.empty()) { + // We didn't expect it to, fail the test. + return ::testing::AssertionFailure() + << "Unexpected logic error: " << *comment + << "\njson: [" << json << " ]"; + } + + if (comment->stringValue() != exp_error) { + // We 't expect a different error, fail the test. + return ::testing::AssertionFailure() + << "Wrong logic error detected, expected: " + << exp_error << ", got: " << *comment + << "\njson: [" << json << " ]"; + } + } else { + // Element parsing succeeded. + if (!exp_error.empty()) { + // It was supposed to fail, so fail the test. + return ::testing::AssertionFailure() + << "Unexpected logic success, expected error:" + << exp_error + << "\njson: [" << json << " ]"; + } + } + + // Verify that the D2 context can be retrieved and is not null. + D2CfgContextPtr context; + context = cfg_mgr_->getD2CfgContext(); + if (!context) { + return ::testing::AssertionFailure() << "D2CfgContext is null"; + } + + // Verify that the global scalar container has been created. + d2_params_ = context->getD2Params(); + if (!d2_params_) { + return ::testing::AssertionFailure() << "D2Params is null"; + } + + return ::testing::AssertionSuccess(); + } + + /// @brief Replaces %LIBRARY% with specified library name + /// + /// @param config input config text (should contain "%LIBRARY%" string) + /// @param lib_name %LIBRARY% will be replaced with that name + /// @return configuration text with library name replaced + std::string pathReplacer(const char* config, const char* lib_name) { + string txt(config); + txt.replace(txt.find("%LIBRARY%"), strlen("%LIBRARY%"), string(lib_name)); + return (txt); + } + + /// @brief Pointer the D2Params most recently parsed. + D2ParamsPtr d2_params_; +}; + +/// @brief Convenience macros for invoking runOrConfig() +#define RUN_CONFIG_OK(a) (runConfigOrFail(a, NO_ERROR, "")) +#define SYNTAX_ERROR(a,b) ASSERT_TRUE(runConfigOrFail(a,SYNTAX_ERROR,b)) +#define LOGIC_ERROR(a,b) ASSERT_TRUE(runConfigOrFail(a,LOGIC_ERROR,b)) + +/// @brief Tests a basic valid configuration for D2Param. +TEST_F(D2CfgMgrTest, validParamsEntry) { + // Verify that ip_address can be valid v4 address. + std::string config = makeParamsConfigString ("192.0.0.1", 777, 333, + "UDP", "JSON"); + RUN_CONFIG_OK(config); + + EXPECT_EQ(isc::asiolink::IOAddress("192.0.0.1"), + d2_params_->getIpAddress()); + EXPECT_EQ(777, d2_params_->getPort()); + EXPECT_EQ(333, d2_params_->getDnsServerTimeout()); + EXPECT_EQ(dhcp_ddns::NCR_UDP, d2_params_->getNcrProtocol()); + EXPECT_EQ(dhcp_ddns::FMT_JSON, d2_params_->getNcrFormat()); + + // Verify that ip_address can be valid v6 address. + config = makeParamsConfigString ("3001::5", 777, 333, "UDP", "JSON"); + RUN_CONFIG_OK(config); + + // Verify that the global scalars have the proper values. + EXPECT_EQ(isc::asiolink::IOAddress("3001::5"), + d2_params_->getIpAddress()); + + // Verify the configuration summary. + EXPECT_EQ("listening on 3001::5, port 777, using UDP", + d2_params_->getConfigSummary()); +} + +/// @brief Tests default values for D2Params. +/// It verifies that D2Params is populated with default value for optional +/// parameter if not supplied in the configuration. +/// Currently they are all optional. +TEST_F(D2CfgMgrTest, defaultValues) { + + ElementPtr defaults = isc::d2::test::parseJSON("{ }"); + ASSERT_NO_THROW(D2SimpleParser::setAllDefaults(defaults)); + + // Check that omitting ip_address gets you its default + std::string config = + "{" + " \"port\": 777 , " + " \"dns-server-timeout\": 333 , " + " \"ncr-protocol\": \"UDP\" , " + " \"ncr-format\": \"JSON\", " + " \"tsig-keys\": [], " + " \"forward-ddns\" : {}, " + " \"reverse-ddns\" : {} " + "}"; + + RUN_CONFIG_OK(config); + ConstElementPtr deflt; + ASSERT_NO_THROW(deflt = defaults->get("ip-address")); + ASSERT_TRUE(deflt); + EXPECT_EQ(deflt->stringValue(), d2_params_->getIpAddress().toText()); + + // Check that omitting port gets you its default + config = + "{" + " \"ip-address\": \"192.0.0.1\" , " + " \"dns-server-timeout\": 333 , " + " \"ncr-protocol\": \"UDP\" , " + " \"ncr-format\": \"JSON\", " + " \"tsig-keys\": [], " + " \"forward-ddns\" : {}, " + " \"reverse-ddns\" : {} " + "}"; + + RUN_CONFIG_OK(config); + ASSERT_NO_THROW(deflt = defaults->get("port")); + ASSERT_TRUE(deflt); + EXPECT_EQ(deflt->intValue(), d2_params_->getPort()); + + // Check that omitting timeout gets you its default + config = + "{" + " \"ip-address\": \"192.0.0.1\" , " + " \"port\": 777 , " + " \"ncr-protocol\": \"UDP\" , " + " \"ncr-format\": \"JSON\", " + " \"tsig-keys\": [], " + " \"forward-ddns\" : {}, " + " \"reverse-ddns\" : {} " + "}"; + + RUN_CONFIG_OK(config); + ASSERT_NO_THROW(deflt = defaults->get("dns-server-timeout")); + ASSERT_TRUE(deflt); + EXPECT_EQ(deflt->intValue(), d2_params_->getDnsServerTimeout()); + + // Check that omitting protocol gets you its default + config = + "{" + " \"ip-address\": \"192.0.0.1\" , " + " \"port\": 777 , " + " \"dns-server-timeout\": 333 , " + " \"ncr-format\": \"JSON\", " + " \"tsig-keys\": [], " + " \"forward-ddns\" : {}, " + " \"reverse-ddns\" : {} " + "}"; + + RUN_CONFIG_OK(config); + ASSERT_NO_THROW(deflt = defaults->get("ncr-protocol")); + ASSERT_TRUE(deflt); + EXPECT_EQ(dhcp_ddns::stringToNcrProtocol(deflt->stringValue()), + d2_params_->getNcrProtocol()); + + // Check that omitting format gets you its default + config = + "{" + " \"ip-address\": \"192.0.0.1\" , " + " \"port\": 777 , " + " \"dns-server-timeout\": 333 , " + " \"ncr-protocol\": \"UDP\", " + " \"tsig-keys\": [], " + " \"forward-ddns\" : {}, " + " \"reverse-ddns\" : {} " + "}"; + + RUN_CONFIG_OK(config); + ASSERT_NO_THROW(deflt = defaults->get("ncr-format")); + ASSERT_TRUE(deflt); + EXPECT_EQ(dhcp_ddns::stringToNcrFormat(deflt->stringValue()), + d2_params_->getNcrFormat()); +} + +/// @brief Tests the unsupported scalar parameters and objects are detected. +TEST_F(D2CfgMgrTest, unsupportedTopLevelItems) { + // Check that an unsupported top level parameter fails. + std::string config = + "{" + " \"ip-address\": \"127.0.0.1\", " + " \"port\": 777 , " + " \"dns-server-timeout\": 333 , " + " \"ncr-protocol\": \"UDP\" , " + " \"ncr-format\": \"JSON\", " + " \"tsig-keys\": [], " + " \"forward-ddns\" : {}, " + " \"reverse-ddns\" : {}, " + " \"bogus-param\" : true " + "}"; + + SYNTAX_ERROR(config, "<string>:1.185-197: got unexpected " + "keyword \"bogus-param\" in DhcpDdns map."); + + // Check that unsupported top level objects fails. For + // D2 these fail as they are not in the parse order. + config = + "{" + " \"ip-address\": \"127.0.0.1\", " + " \"port\": 777 , " + " \"dns-server-timeout\": 333 , " + " \"ncr-protocol\": \"UDP\" , " + " \"ncr-format\": \"JSON\", " + " \"tsig-keys\": [], " + " \"bogus-object-one\" : {}, " + " \"forward-ddns\" : {}, " + " \"reverse-ddns\" : {}, " + " \"bogus-object-two\" : {} " + "}"; + + SYNTAX_ERROR(config, "<string>:1.141-158: got unexpected" + " keyword \"bogus-object-one\" in DhcpDdns map."); +} + + +/// @brief Tests the enforcement of data validation when parsing D2Params. +/// It verifies that: +/// -# ip_address cannot be "0.0.0.0" +/// -# ip_address cannot be "::" +/// -# port cannot be 0 +/// -# dns_server_timeout cannot be 0 +/// -# ncr_protocol must be valid +/// -# ncr_format must be valid +TEST_F(D2CfgMgrTest, invalidEntry) { + // Cannot use IPv4 ANY address + std::string config = makeParamsConfigString ("0.0.0.0", 777, 333, + "UDP", "JSON"); + LOGIC_ERROR(config, "IP address cannot be \"0.0.0.0\" (<string>:1:17)"); + + // Cannot use IPv6 ANY address + config = makeParamsConfigString ("::", 777, 333, "UDP", "JSON"); + LOGIC_ERROR(config, "IP address cannot be \"::\" (<string>:1:17)"); + + // Cannot use port 0 + config = makeParamsConfigString ("127.0.0.1", 0, 333, "UDP", "JSON"); + SYNTAX_ERROR(config, "<string>:1.40: port must be greater than zero but less than 65536"); + + // Cannot use dns server timeout of 0 + config = makeParamsConfigString ("127.0.0.1", 777, 0, "UDP", "JSON"); + SYNTAX_ERROR(config, "<string>:1.69: dns-server-timeout" + " must be greater than zero"); + + // Invalid protocol + config = makeParamsConfigString ("127.0.0.1", 777, 333, "BOGUS", "JSON"); + SYNTAX_ERROR(config, "<string>:1.92-98: syntax error," + " unexpected constant string, expecting UDP or TCP"); + + // Unsupported protocol + config = makeParamsConfigString ("127.0.0.1", 777, 333, "TCP", "JSON"); + LOGIC_ERROR(config, "ncr-protocol : TCP is not yet supported" + " (<string>:1:92)"); + + // Invalid format + config = makeParamsConfigString ("127.0.0.1", 777, 333, "UDP", "BOGUS"); + SYNTAX_ERROR(config, "<string>:1.115-121: syntax error," + " unexpected constant string, expecting JSON"); +} + +// Control socket tests in d2_process_unittests.cc + +// DdnsDomainList and TSIGKey tests moved to d2_simple_parser_unittest.cc + +/// @brief Tests construction of D2CfgMgr +/// This test verifies that a D2CfgMgr constructs properly. +TEST(D2CfgMgr, construction) { + boost::scoped_ptr<D2CfgMgr> cfg_mgr; + + // Verify that configuration manager constructions without error. + ASSERT_NO_THROW(cfg_mgr.reset(new D2CfgMgr())); + + // Verify that the context can be retrieved and is not null. + D2CfgContextPtr context; + ASSERT_NO_THROW(context = cfg_mgr->getD2CfgContext()); + EXPECT_TRUE(context); + + // Verify that the forward manager can be retrieved and is not null. + EXPECT_TRUE(context->getForwardMgr()); + + // Verify that the reverse manager can be retrieved and is not null. + EXPECT_TRUE(context->getReverseMgr()); + + // Verify that the manager can be destructed without error. + EXPECT_NO_THROW(cfg_mgr.reset()); +} + +/// @brief Tests the parsing of a complete, valid DHCP-DDNS configuration. +/// This tests passes the configuration into an instance of D2CfgMgr just +/// as it would be done by d2_process in response to a configuration update +/// event. +TEST_F(D2CfgMgrTest, fullConfig) { + // Create a configuration with all of application level parameters, plus + // both the forward and reverse ddns managers. Both managers have two + // domains with three servers per domain. + std::string config = "{ " + "\"ip-address\" : \"192.168.1.33\" , " + "\"port\" : 88 , " + "\"dns-server-timeout\": 333 , " + "\"ncr-protocol\": \"UDP\" , " + "\"ncr-format\": \"JSON\", " + "\"control-socket\" : {" + " \"socket-type\" : \"unix\" ," + " \"socket-name\" : \"/tmp/d2-ctrl-channel\" " + "}," + "\"hooks-libraries\": [" + "{" + " \"library\": \"%LIBRARY%\" , " + " \"parameters\": " + " { \"param1\": \"foo\" } " + "}" + "]," + "\"tsig-keys\": [" + "{" + " \"name\": \"d2_key.example.com\" , " + " \"algorithm\": \"hmac-md5\" , " + " \"secret\": \"LSWXnfkKZjdPJI5QxlpnfQ==\" " + "}," + "{" + " \"name\": \"d2_key.billcat.net\" , " + " \"algorithm\": \"hmac-md5\" , " + " \"digest-bits\": 120 , " + " \"secret\": \"LSWXnfkKZjdPJI5QxlpnfQ==\" " + "}" + "]," + "\"forward-ddns\" : {" + "\"ddns-domains\": [ " + "{ \"name\": \"example.com\" , " + " \"key-name\": \"d2_key.example.com\" , " + " \"dns-servers\" : [ " + " { \"ip-address\": \"127.0.0.1\" } , " + " { \"ip-address\": \"127.0.0.2\" } , " + " { \"ip-address\": \"127.0.0.3\"} " + " ] } " + ", " + "{ \"name\": \"billcat.net\" , " + " \"key-name\": \"d2_key.billcat.net\" , " + " \"dns-servers\" : [ " + " { \"ip-address\": \"127.0.0.4\" } , " + " { \"ip-address\": \"127.0.0.5\" } , " + " { \"ip-address\": \"127.0.0.6\" } " + " ] } " + "] }," + "\"reverse-ddns\" : {" + "\"ddns-domains\": [ " + "{ \"name\": \" 0.168.192.in.addr.arpa.\" , " + " \"key-name\": \"d2_key.example.com\" , " + " \"dns-servers\" : [ " + " { \"ip-address\": \"127.0.1.1\" } , " + " { \"ip-address\": \"127.0.2.1\" } , " + " { \"ip-address\": \"127.0.3.1\" } " + " ] } " + ", " + "{ \"name\": \" 0.247.106.in.addr.arpa.\" , " + " \"key-name\": \"d2_key.billcat.net\" , " + " \"dns-servers\" : [ " + " { \"ip-address\": \"127.0.4.1\" }, " + " { \"ip-address\": \"127.0.5.1\" } , " + " { \"ip-address\": \"127.0.6.1\" } " + " ] } " + "] } }"; + + // Replace the library path. + std::string pr_config = pathReplacer(config.c_str(), CALLOUT_LIBRARY); + // Should parse without error. + RUN_CONFIG_OK(pr_config); + + // Verify that the D2 context can be retrieved and is not null. + D2CfgContextPtr context; + ASSERT_NO_THROW(context = cfg_mgr_->getD2CfgContext()); + + // Verify that the global scalars have the proper values. + D2ParamsPtr& d2_params = context->getD2Params(); + ASSERT_TRUE(d2_params); + + EXPECT_EQ(isc::asiolink::IOAddress("192.168.1.33"), + d2_params->getIpAddress()); + EXPECT_EQ(88, d2_params->getPort()); + EXPECT_EQ(333, d2_params->getDnsServerTimeout()); + EXPECT_EQ(dhcp_ddns::NCR_UDP, d2_params->getNcrProtocol()); + EXPECT_EQ(dhcp_ddns::FMT_JSON, d2_params->getNcrFormat()); + + // Verify that the control socket can be retrieved. + ConstElementPtr ctrl_sock = context->getControlSocketInfo(); + ASSERT_TRUE(ctrl_sock); + ASSERT_EQ(Element::map, ctrl_sock->getType()); + EXPECT_EQ(2, ctrl_sock->size()); + ASSERT_TRUE(ctrl_sock->get("socket-type")); + EXPECT_EQ("\"unix\"", ctrl_sock->get("socket-type")->str()); + ASSERT_TRUE(ctrl_sock->get("socket-name")); + EXPECT_EQ("\"/tmp/d2-ctrl-channel\"", ctrl_sock->get("socket-name")->str()); + + // Verify that the hooks libraries can be retrieved. + const HookLibsCollection libs = context->getHooksConfig().get(); + ASSERT_EQ(1, libs.size()); + EXPECT_EQ(string(CALLOUT_LIBRARY), libs[0].first); + ASSERT_TRUE(libs[0].second); + EXPECT_EQ("{ \"param1\": \"foo\" }", libs[0].second->str()); + + // Verify that the forward manager can be retrieved. + DdnsDomainListMgrPtr mgr = context->getForwardMgr(); + ASSERT_TRUE(mgr); + EXPECT_EQ("forward-ddns", mgr->getName()); + + // Verify that the forward manager has the correct number of domains. + DdnsDomainMapPtr domains = mgr->getDomains(); + ASSERT_TRUE(domains); + int count = domains->size(); + EXPECT_EQ(2, count); + + // Verify that the server count in each of the forward manager domains. + // NOTE that since prior tests have validated server parsing, we are are + // assuming that the servers did in fact parse correctly if the correct + // number of them are there. + DdnsDomainMapPair domain_pair; + BOOST_FOREACH(domain_pair, (*domains)) { + DdnsDomainPtr domain = domain_pair.second; + DnsServerInfoStoragePtr servers = domain->getServers(); + count = servers->size(); + EXPECT_TRUE(servers); + EXPECT_EQ(3, count); + } + + // Verify that the reverse manager can be retrieved. + mgr = context->getReverseMgr(); + ASSERT_TRUE(mgr); + EXPECT_EQ("reverse-ddns", mgr->getName()); + + // Verify that the reverse manager has the correct number of domains. + domains = mgr->getDomains(); + count = domains->size(); + EXPECT_EQ(2, count); + + // Verify that the server count in each of the reverse manager domains. + // NOTE that since prior tests have validated server parsing, we are are + // assuming that the servers did in fact parse correctly if the correct + // number of them are there. + BOOST_FOREACH(domain_pair, (*domains)) { + DdnsDomainPtr domain = domain_pair.second; + DnsServerInfoStoragePtr servers = domain->getServers(); + count = servers->size(); + EXPECT_TRUE(servers); + EXPECT_EQ(3, count); + } + + // Test directional update flags. + EXPECT_TRUE(cfg_mgr_->forwardUpdatesEnabled()); + EXPECT_TRUE(cfg_mgr_->reverseUpdatesEnabled()); + + // Verify that parsing the exact same configuration a second time + // does not cause a duplicate value errors. + answer_ = cfg_mgr_->simpleParseConfig(config_set_, false); + ASSERT_TRUE(checkAnswer(0)); +} + +/// @brief Tests the basics of the D2CfgMgr FQDN-domain matching +/// This test uses a valid configuration to exercise the D2CfgMgr +/// forward FQDN-to-domain matching. +/// It verifies that: +/// 1. Given an FQDN which exactly matches a domain's name, that domain is +/// returned as match. +/// 2. Given a FQDN for sub-domain in the list, returns the proper match. +/// 3. Given a FQDN that matches no domain name, returns the wild card domain +/// as a match. +TEST_F(D2CfgMgrTest, forwardMatch) { + // Create configuration with one domain, one sub domain, and the wild + // card. + std::string config = "{ " + "\"ip-address\" : \"192.168.1.33\" , " + "\"port\" : 88 , " + "\"tsig-keys\": [] ," + "\"forward-ddns\" : {" + "\"ddns-domains\": [ " + "{ \"name\": \"example.com\" , " + " \"dns-servers\" : [ " + " { \"ip-address\": \"127.0.0.1\" } " + " ] } " + ", " + "{ \"name\": \"one.example.com\" , " + " \"dns-servers\" : [ " + " { \"ip-address\": \"127.0.0.2\" } " + " ] } " + ", " + "{ \"name\": \"*\" , " + " \"dns-servers\" : [ " + " { \"ip-address\": \"127.0.0.3\" } " + " ] } " + "] }, " + "\"reverse-ddns\" : {} " + "}"; + + // Verify that we can parse the configuration. + RUN_CONFIG_OK(config); + + // Verify that the D2 context can be retrieved and is not null. + D2CfgContextPtr context; + ASSERT_NO_THROW(context = cfg_mgr_->getD2CfgContext()); + + // Test directional update flags. + EXPECT_TRUE(cfg_mgr_->forwardUpdatesEnabled()); + EXPECT_FALSE(cfg_mgr_->reverseUpdatesEnabled()); + + DdnsDomainPtr match; + // Verify that an exact match works. + EXPECT_TRUE(cfg_mgr_->matchForward("example.com", match)); + EXPECT_EQ("example.com", match->getName()); + + // Verify that search is case insensitive. + EXPECT_TRUE(cfg_mgr_->matchForward("EXAMPLE.COM", match)); + EXPECT_EQ("example.com", match->getName()); + + // Verify that an exact match works. + EXPECT_TRUE(cfg_mgr_->matchForward("one.example.com", match)); + EXPECT_EQ("one.example.com", match->getName()); + + // Verify that a FQDN for sub-domain matches. + EXPECT_TRUE(cfg_mgr_->matchForward("blue.example.com", match)); + EXPECT_EQ("example.com", match->getName()); + + // Verify that a FQDN for sub-domain matches. + EXPECT_TRUE(cfg_mgr_->matchForward("red.one.example.com", match)); + EXPECT_EQ("one.example.com", match->getName()); + + // Verify that an FQDN with no match, returns the wild card domain. + EXPECT_TRUE(cfg_mgr_->matchForward("shouldbe.wildcard", match)); + EXPECT_EQ("*", match->getName()); + + // Verify that an attempt to match an empty FQDN throws. + ASSERT_THROW(cfg_mgr_->matchForward("", match), D2CfgError); +} + +/// @brief Tests domain matching when there is no wild card domain. +/// This test verifies that matches are found only for FQDNs that match +/// some or all of a domain name. FQDNs without matches should not return +/// a match. +TEST_F(D2CfgMgrTest, matchNoWildcard) { + // Create a configuration with one domain, one sub-domain, and NO wild card. + std::string config = "{ " + "\"ip-address\" : \"192.168.1.33\" , " + "\"port\" : 88 , " + "\"tsig-keys\": [] ," + "\"forward-ddns\" : {" + "\"ddns-domains\": [ " + "{ \"name\": \"example.com\" , " + " \"dns-servers\" : [ " + " { \"ip-address\": \"127.0.0.1\" } " + " ] } " + ", " + "{ \"name\": \"one.example.com\" , " + " \"dns-servers\" : [ " + " { \"ip-address\": \"127.0.0.2\" } " + " ] } " + "] }, " + "\"reverse-ddns\" : {} " + " }"; + + // Verify that we can parse the configuration. + RUN_CONFIG_OK(config); + + // Verify that the D2 context can be retrieved and is not null. + D2CfgContextPtr context; + ASSERT_NO_THROW(context = cfg_mgr_->getD2CfgContext()); + + DdnsDomainPtr match; + // Verify that full or partial matches, still match. + EXPECT_TRUE(cfg_mgr_->matchForward("example.com", match)); + EXPECT_EQ("example.com", match->getName()); + + EXPECT_TRUE(cfg_mgr_->matchForward("blue.example.com", match)); + EXPECT_EQ("example.com", match->getName()); + + EXPECT_TRUE(cfg_mgr_->matchForward("red.one.example.com", match)); + EXPECT_EQ("one.example.com", match->getName()); + + // Verify that a FQDN with no match, fails to match. + EXPECT_FALSE(cfg_mgr_->matchForward("shouldbe.wildcard", match)); +} + +/// @brief Tests domain matching when there is ONLY a wild card domain. +/// This test verifies that any FQDN matches the wild card. +TEST_F(D2CfgMgrTest, matchAll) { + std::string config = "{ " + "\"ip-address\" : \"192.168.1.33\" , " + "\"port\" : 88 , " + "\"tsig-keys\": [] ," + "\"forward-ddns\" : {" + "\"ddns-domains\": [ " + "{ \"name\": \"*\" , " + " \"dns-servers\" : [ " + " { \"ip-address\": \"127.0.0.1\" } " + " ] } " + "] }, " + "\"reverse-ddns\" : {} " + "}"; + + // Verify that we can parse the configuration. + RUN_CONFIG_OK(config); + + // Verify that the D2 context can be retrieved and is not null. + D2CfgContextPtr context; + ASSERT_NO_THROW(context = cfg_mgr_->getD2CfgContext()); + + // Verify that wild card domain is returned for any FQDN. + DdnsDomainPtr match; + EXPECT_TRUE(cfg_mgr_->matchForward("example.com", match)); + EXPECT_EQ("*", match->getName()); + EXPECT_TRUE(cfg_mgr_->matchForward("shouldbe.wildcard", match)); + EXPECT_EQ("*", match->getName()); + + // Verify that an attempt to match an empty FQDN still throws. + ASSERT_THROW(cfg_mgr_->matchReverse("", match), D2CfgError); + +} + +/// @brief Tests the basics of the D2CfgMgr reverse FQDN-domain matching +/// This test uses a valid configuration to exercise the D2CfgMgr's +/// reverse FQDN-to-domain matching. +/// It verifies that: +/// 1. Given an FQDN which exactly matches a domain's name, that domain is +/// returned as match. +/// 2. Given a FQDN for sub-domain in the list, returns the proper match. +/// 3. Given a FQDN that matches no domain name, returns the wild card domain +/// as a match. +TEST_F(D2CfgMgrTest, matchReverse) { + std::string config = "{ " + "\"ip-address\" : \"192.168.1.33\" , " + "\"port\" : 88 , " + "\"tsig-keys\": [] ," + "\"forward-ddns\" : {}, " + "\"reverse-ddns\" : {" + "\"ddns-domains\": [ " + "{ \"name\": \"5.100.168.192.in-addr.arpa.\" , " + " \"dns-servers\" : [ " + " { \"ip-address\": \"127.0.0.1\" } " + " ] }, " + "{ \"name\": \"100.200.192.in-addr.arpa.\" , " + " \"dns-servers\" : [ " + " { \"ip-address\": \"127.0.0.1\" } " + " ] }, " + "{ \"name\": \"170.192.in-addr.arpa.\" , " + " \"dns-servers\" : [ " + " { \"ip-address\": \"127.0.0.1\" } " + " ] }, " + // Note mixed case to test case insensitivity. + "{ \"name\": \"2.0.3.0.8.b.d.0.1.0.0.2.IP6.ARPA.\" , " + " \"dns-servers\" : [ " + " { \"ip-address\": \"127.0.0.1\" } " + " ] }," + "{ \"name\": \"*\" , " + " \"dns-servers\" : [ " + " { \"ip-address\": \"127.0.0.1\" } " + " ] } " + "] } }"; + + // Verify that we can parse the configuration. + RUN_CONFIG_OK(config); + + // Verify that the D2 context can be retrieved and is not null. + D2CfgContextPtr context; + ASSERT_NO_THROW(context = cfg_mgr_->getD2CfgContext()); + + // Test directional update flags. + EXPECT_FALSE(cfg_mgr_->forwardUpdatesEnabled()); + EXPECT_TRUE(cfg_mgr_->reverseUpdatesEnabled()); + + DdnsDomainPtr match; + + // Verify an exact match. + EXPECT_TRUE(cfg_mgr_->matchReverse("192.168.100.5", match)); + EXPECT_EQ("5.100.168.192.in-addr.arpa.", match->getName()); + + // Verify a sub-domain match. + EXPECT_TRUE(cfg_mgr_->matchReverse("192.200.100.27", match)); + EXPECT_EQ("100.200.192.in-addr.arpa.", match->getName()); + + // Verify a sub-domain match. + EXPECT_TRUE(cfg_mgr_->matchReverse("192.170.50.30", match)); + EXPECT_EQ("170.192.in-addr.arpa.", match->getName()); + + // Verify a wild card match. + EXPECT_TRUE(cfg_mgr_->matchReverse("1.1.1.1", match)); + EXPECT_EQ("*", match->getName()); + + // Verify a IPv6 match. + EXPECT_TRUE(cfg_mgr_->matchReverse("2001:db8:302:99::",match)); + EXPECT_EQ("2.0.3.0.8.b.d.0.1.0.0.2.IP6.ARPA.", match->getName()); + + // Verify a IPv6 wild card match. + EXPECT_TRUE(cfg_mgr_->matchReverse("2001:db8:99:302::",match)); + EXPECT_EQ("*", match->getName()); + + // Verify that an attempt to match an invalid IP address throws. + ASSERT_THROW(cfg_mgr_->matchReverse("", match), D2CfgError); +} + +/// @brief Tests D2 config parsing against a wide range of config permutations. +/// +/// It tests for both syntax errors that the JSON parsing (D2ParserContext) +/// should detect as well as post-JSON parsing logic errors generated by +/// the Element parsers (i.e...SimpleParser/DhcpParser derivations) +/// +/// It iterates over all of the test configurations described in given file. +/// The file content is JSON specialized to this test. The format of the file +/// is: +/// +/// @code +/// # The file must open with a list. It's name is arbitrary. +/// +/// { "test_list" : +/// [ +/// +/// # Test one starts here: +/// { +/// +/// # Each test has: +/// # 1. description - optional text description +/// # 2. syntax-error - error JSON parser should emit (omit if none) +/// # 3. logic-error - error element parser(s) should emit (omit if none) +/// # 4. data - configuration text to parse +/// # +/// "description" : "<text describing test>", +/// "syntax-error" : "<exact text from JSON parser including position>" , +/// "logic-error" : "<exact text from element parser including position>" , +/// "data" : +/// { +/// # configuration elements here +/// "bool_val" : false, +/// "some_map" : {} +/// # : +/// } +/// } +/// +/// # Next test would start here +/// , +/// { +/// } +/// +/// ]} +/// +/// @endcode +/// +/// (The file supports comments per Element::fromJSONFile()) +/// +TEST_F(D2CfgMgrTest, configPermutations) { + std::string test_file = testDataFile("d2_cfg_tests.json"); + isc::data::ConstElementPtr tests; + + // Read contents of the file and parse it as JSON. Note it must contain + // all valid JSON, we aren't testing JSON parsing. + try { + tests = isc::data::Element::fromJSONFile(test_file, true); + } catch (const std::exception& ex) { + FAIL() << "ERROR parsing file : " << test_file << " : " << ex.what(); + } + + // Read in each test For each test, read: + // + // 1. description - optional text description + // 2. syntax-error or logic-error or neither + // 3. data - configuration text to parse + // 4. convert data into JSON text + // 5. submit JSON for parsing + isc::data::ConstElementPtr test; + ASSERT_TRUE(tests->get("test-list")); + BOOST_FOREACH(test, tests->get("test-list")->listValue()) { + // Grab the description. + std::string description = "<no desc>"; + isc::data::ConstElementPtr elem = test->get("description"); + if (elem) { + elem->getValue(description); + } + + // Grab the expected error message, if there is one. + std::string expected_error = ""; + RunConfigMode mode = NO_ERROR; + elem = test->get("syntax-error"); + if (elem) { + elem->getValue(expected_error); + mode = SYNTAX_ERROR; + } else { + elem = test->get("logic-error"); + if (elem) { + elem->getValue(expected_error); + mode = LOGIC_ERROR; + } + } + + // Grab the test's configuration data. + isc::data::ConstElementPtr data = test->get("data"); + ASSERT_TRUE(data) << "No data for test: " << test->getPosition(); + + // Convert the test data back to JSON text, then submit it for parsing. + stringstream os; + data->toJSON(os); + EXPECT_TRUE(runConfigOrFail(os.str(), mode, expected_error)) + << " failed for test: " << test->getPosition() << std::endl; + } +} + +/// @brief Tests comments. +TEST_F(D2CfgMgrTest, comments) { + std::string config = "{ " + "\"comment\": \"D2 config\" , " + "\"ip-address\" : \"192.168.1.33\" , " + "\"port\" : 88 , " + "\"control-socket\": {" + " \"comment\": \"Control channel\" , " + " \"socket-type\": \"unix\" ," + " \"socket-name\": \"/tmp/d2-ctrl-channel\" " + "}," + "\"tsig-keys\": [" + "{" + " \"user-context\": { " + " \"comment\": \"Indirect comment\" } , " + " \"name\": \"d2_key.example.com\" , " + " \"algorithm\": \"hmac-md5\" , " + " \"secret\": \"LSWXnfkKZjdPJI5QxlpnfQ==\" " + "}" + "]," + "\"forward-ddns\" : {" + "\"ddns-domains\": [ " + "{ \"comment\": \"A DDNS domain\" , " + " \"name\": \"example.com\" , " + " \"key-name\": \"d2_key.example.com\" , " + " \"dns-servers\" : [ " + " { \"ip-address\": \"127.0.0.1\" , " + " \"user-context\": { \"version\": 1 } } " + " ] } " + "] } }"; + + // Should parse without error. + RUN_CONFIG_OK(config); + + // Check the D2 context. + D2CfgContextPtr d2_context; + ASSERT_NO_THROW(d2_context = cfg_mgr_->getD2CfgContext()); + ASSERT_TRUE(d2_context); + + // Check global user context. + ConstElementPtr ctx = d2_context->getContext(); + ASSERT_TRUE(ctx); + ASSERT_EQ(1, ctx->size()); + ASSERT_TRUE(ctx->get("comment")); + EXPECT_EQ("\"D2 config\"", ctx->get("comment")->str()); + + // Check control socket. + ConstElementPtr ctrl_sock = d2_context->getControlSocketInfo(); + ASSERT_TRUE(ctrl_sock); + ASSERT_TRUE(ctrl_sock->get("user-context")); + EXPECT_EQ("{ \"comment\": \"Control channel\" }", + ctrl_sock->get("user-context")->str()); + + // Check TSIG keys. + TSIGKeyInfoMapPtr keys = d2_context->getKeys(); + ASSERT_TRUE(keys); + ASSERT_EQ(1, keys->size()); + + // Check the TSIG key. + TSIGKeyInfoMap::iterator gotkey = keys->find("d2_key.example.com"); + ASSERT_TRUE(gotkey != keys->end()); + TSIGKeyInfoPtr key = gotkey->second; + ASSERT_TRUE(key); + + // Check the TSIG key user context. + ConstElementPtr key_ctx = key->getContext(); + ASSERT_TRUE(key_ctx); + ASSERT_EQ(1, key_ctx->size()); + ASSERT_TRUE(key_ctx->get("comment")); + EXPECT_EQ("\"Indirect comment\"", key_ctx->get("comment")->str()); + + // Check the forward manager. + DdnsDomainListMgrPtr mgr = d2_context->getForwardMgr(); + ASSERT_TRUE(mgr); + EXPECT_EQ("forward-ddns", mgr->getName()); + DdnsDomainMapPtr domains = mgr->getDomains(); + ASSERT_TRUE(domains); + ASSERT_EQ(1, domains->size()); + + // Check the DDNS domain. + DdnsDomainMap::iterator gotdns = domains->find("example.com"); + ASSERT_TRUE(gotdns != domains->end()); + DdnsDomainPtr domain = gotdns->second; + ASSERT_TRUE(domain); + + // Check the DNS server. + DnsServerInfoStoragePtr servers = domain->getServers(); + ASSERT_TRUE(servers); + ASSERT_EQ(1, servers->size()); + DnsServerInfoPtr server = (*servers)[0]; + ASSERT_TRUE(server); + + // Check the DNS server user context. + ConstElementPtr srv_ctx = server->getContext(); + ASSERT_TRUE(srv_ctx); + ASSERT_EQ(1, srv_ctx->size()); + ASSERT_TRUE(srv_ctx->get("version")); + EXPECT_EQ("1", srv_ctx->get("version")->str()); +} + +} // end of anonymous namespace |