// Copyright (C) 2013-2020 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 #include #include #include #include #include #include #include #include #include #include #include #include using namespace std; using namespace isc; using namespace isc::d2; 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 (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 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, ":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, ":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\" (:1:17)"); // Cannot use IPv6 ANY address config = makeParamsConfigString ("::", 777, 333, "UDP", "JSON"); LOGIC_ERROR(config, "IP address cannot be \"::\" (:1:17)"); // Cannot use port 0 config = makeParamsConfigString ("127.0.0.1", 0, 333, "UDP", "JSON"); SYNTAX_ERROR(config, ":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, ":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, ":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" " (:1:92)"); // Invalid format config = makeParamsConfigString ("127.0.0.1", 777, 333, "UDP", "BOGUS"); SYNTAX_ERROR(config, ":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 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\" " "}," "\"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\" } " " ] } " "] } }"; // Should parse without error. 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 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 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" : "", /// "syntax-error" : "" , /// "logic-error" : "" , /// "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 = ""; 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