// Copyright (C) 2017-2022 Internet Systems Consortium, Inc. ("ISC") // // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at http://mozilla.org/MPL/2.0/. #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace isc::config; using namespace isc::data; using namespace isc::dhcp; using namespace isc::dhcp::test; using namespace isc::test; namespace { /// @name How to fill configurations /// /// Copy get_config_unittest.cc.skel into get_config_unittest.cc /// /// For the extracted configurations define the EXTRACT_CONFIG and /// recompile this file. Run dhcp6_unittests on Dhcp6ParserTest /// redirecting the standard error to a temporary file, e.g. by /// @code /// ./dhcp6_unittests --gtest_filter="Dhcp6Parser*" > /dev/null 2> x /// @endcode /// /// Update EXTRACTED_CONFIGS with the file content /// /// When configurations have been extracted the corresponding unparsed /// configurations must be generated. To do that define GENERATE_ACTION /// and recompile this file. Run dhcp6_unittests on Dhcp6GetConfigTest /// redirecting the standard error to a temporary file, e.g. by /// @code /// ./dhcp6_unittests --gtest_filter="Dhcp6GetConfig*" > /dev/null 2> u /// @endcode /// /// Update UNPARSED_CONFIGS with the file content, recompile this file /// without EXTRACT_CONFIG and GENERATE_ACTION. /// /// @note Check for failures at each step! /// @note The tests of this file do not check if configs returned /// by @ref isc::dhcp::CfgToElement::ToElement() are complete. /// This has to be done manually. /// ///@{ /// @brief extracted configurations const char* EXTRACTED_CONFIGS[] = { // "to be replaced" }; /// @brief unparsed configurations const char* UNPARSED_CONFIGS[] = { // "to be replaced" }; /// @brief the number of configurations const size_t max_config_counter = sizeof(EXTRACTED_CONFIGS) / sizeof(char*); ///@} /// @brief the extraction counter /// /// < 0 means do not extract, >= 0 means extract on extractConfig() calls /// and increment #ifdef EXTRACT_CONFIG int extract_count = 0; #else int extract_count = -1; #endif /// @brief the generate action /// false means do nothing, true means unparse extracted configurations #ifdef GENERATE_ACTION const bool generate_action = true; #else const bool generate_action = false; static_assert(max_config_counter == sizeof(UNPARSED_CONFIGS) / sizeof(char*), "unparsed configurations must be generated"); #endif /// @brief format and output a configuration void outputFormatted(const std::string& config) { // pretty print it ConstElementPtr json = parseDHCP6(config); std::string prettier = prettyPrint(json, 4, 4); // get it as a line array std::list lines; boost::split(lines, prettier, boost::is_any_of("\n")); // add escapes using again JSON std::list escapes; while (!lines.empty()) { const std::string& line = lines.front(); ConstElementPtr escaping = Element::create(line + "\n"); escapes.push_back(escaping->str()); lines.pop_front(); } // output them on std::cerr while (!escapes.empty()) { std::cerr << "\n" << escapes.front(); escapes.pop_front(); } } } // namespace namespace isc { namespace dhcp { namespace test { /// @ref isc::dhcp::test::extractConfig in the header void extractConfig(const std::string& config) { // skip when disable if (extract_count < 0) { return; } // mark beginning if (extract_count == 0) { // header (note there is no trailer) std::cerr << "/// put this after const char* EXTRACTED_CONFIGS[] = {\n"; } else { // end of previous configuration std::cerr << ",\n"; } std::cerr << " // CONFIGURATION " << extract_count; try { outputFormatted(config); } catch (...) { // mark error std::cerr << "\n//// got an error\n"; } ++extract_count; } } // namespace test } // namespace dhcp } // namespace isc namespace { /// Test fixture class (code from Dhcp6ParserTest) class Dhcp6GetConfigTest : public ::testing::TestWithParam { public: Dhcp6GetConfigTest() : rcode_(-1), srv_(0) { // srv_(0) means to not open any sockets. We don't want to // deal with sockets here, just check if configuration handling // is sane. // Reset configuration for each test. resetConfiguration(); } ~Dhcp6GetConfigTest() { // Reset configuration database after each test. resetConfiguration(); }; /// @brief Parse and Execute configuration /// /// Parses a configuration and executes a configuration of the server. /// If the operation fails, the current test will register a failure. /// /// @param config Configuration to parse /// @param operation Operation being performed. In the case of an error, /// the error text will include the string "unable to .". /// /// @return true if the configuration succeeded, false if not. bool executeConfiguration(const std::string& config, const char* operation) { // clear config manager CfgMgr::instance().clear(); // enable fake network interfaces IfaceMgrTestConfig test_config(true); // try JSON parser ConstElementPtr json; try { json = parseJSON(config); } catch (const std::exception& ex) { ADD_FAILURE() << "invalid JSON for " << operation << " failed with " << ex.what() << " on\n" << config << "\n"; return (false); } // try DHCP6 parser try { json = parseDHCP6(config, true); } catch (...) { ADD_FAILURE() << "parsing failed for " << operation << " on\n" << prettyPrint(json) << "\n"; return (false); } // try DHCP6 configure ConstElementPtr status; try { status = configureDhcp6Server(srv_, json); } catch (const std::exception& ex) { ADD_FAILURE() << "configure for " << operation << " failed with " << ex.what() << " on\n" << prettyPrint(json) << "\n"; return (false); } // The status object must not be NULL if (!status) { ADD_FAILURE() << "configure for " << operation << " returned null on\n" << prettyPrint(json) << "\n"; return (false); } // Returned value should be 0 (configuration success) comment_ = parseAnswer(rcode_, status); if (rcode_ != 0) { string reason = ""; if (comment_) { reason = string(" (") + comment_->stringValue() + string(")"); } ADD_FAILURE() << "configure for " << operation << " returned error code " << rcode_ << reason << " on\n" << prettyPrint(json) << "\n"; return (false); } return (true); } /// @brief Reset configuration database. /// /// This function resets configuration data base by /// removing all subnets and option-data. Reset must /// be performed after each test to make sure that /// contents of the database do not affect result of /// subsequent tests. void resetConfiguration() { string config = "{" "\"interfaces-config\": { \"interfaces\": [ \"*\" ] }," "\"preferred-lifetime\": 3000," "\"rebind-timer\": 2000, " "\"renew-timer\": 1000, " "\"valid-lifetime\": 4000, " "\"subnet6\": [ ], " "\"dhcp-ddns\": { \"enable-updates\" : false }, " "\"option-def\": [ ], " "\"option-data\": [ ] }"; EXPECT_TRUE(executeConfiguration(config, "reset configuration")); CfgMgr::instance().clear(); CfgMgr::instance().setFamily(AF_INET6); } int rcode_; ///< Return code (see @ref isc::config::parseAnswer) ControlledDhcpv6Srv srv_; ///< Instance of the ControlledDhcp6Srv used during tests ConstElementPtr comment_; ///< Comment (see @ref isc::config::parseAnswer) }; /// Test a configuration TEST_P(Dhcp6GetConfigTest, run) { // configurations have not been extracted yet if (max_config_counter == 0) { return; } // get the index of configurations to test size_t config_counter = GetParam(); // emit unparsed header if wanted if ((config_counter == 0) && generate_action) { std::cerr << "///put this after const char* UNPARSED_CONFIGS[] = {\n"; } // get the extracted configuration std::string config = EXTRACTED_CONFIGS[config_counter]; std::ostringstream ss; ss << "extracted config #" << config_counter; // execute the extracted configuration ASSERT_TRUE(executeConfiguration(config, ss.str().c_str())); // unparse it ConstSrvConfigPtr extracted = CfgMgr::instance().getStagingCfg(); ConstElementPtr unparsed; ASSERT_NO_THROW_LOG(unparsed = extracted->toElement()); ConstElementPtr dhcp; ASSERT_NO_THROW_LOG(dhcp = unparsed->get("Dhcp6")); ASSERT_TRUE(dhcp); // dump if wanted else check std::string expected; if (generate_action) { if (config_counter > 0) { std::cerr << ",\n"; } std::cerr << " // CONFIGURATION " << config_counter; ASSERT_NO_THROW_LOG(expected = prettyPrint(dhcp)); ASSERT_NO_THROW_LOG(outputFormatted(dhcp->str())); } else { expected = UNPARSED_CONFIGS[config_counter]; // get the expected config using the dhcpv6 syntax parser ElementPtr jsond; ASSERT_NO_THROW_LOG(jsond = parseDHCP6(expected, true)); // get the expected config using the generic JSON syntax parser ElementPtr jsonj; ASSERT_NO_THROW_LOG(jsonj = parseJSON(expected)); // the generic JSON parser does not handle comments EXPECT_TRUE(isEquivalent(jsond, moveComments(jsonj))); // check that unparsed and expected values match EXPECT_TRUE(isEquivalent(dhcp, jsonj)); // check on pretty prints too std::string current = prettyPrint(dhcp, 4, 4) + "\n"; EXPECT_EQ(expected, current); if (expected != current) { expected = current; } } // execute the dhcp configuration ss.str(""); ss << "unparsed config #" << config_counter; EXPECT_TRUE(executeConfiguration(expected, ss.str().c_str())); // is it a fixed point? ConstSrvConfigPtr extracted2 = CfgMgr::instance().getStagingCfg(); ConstElementPtr unparsed2; ASSERT_NO_THROW_LOG(unparsed2 = extracted2->toElement()); ASSERT_TRUE(unparsed2); EXPECT_TRUE(isEquivalent(unparsed, unparsed2)); } class IntToString { public: std::string operator()(const testing::TestParamInfo& n) { std::ostringstream ss; ss << static_cast(n.param); return (ss.str()); } }; /// Define the parameterized test loop. #ifdef INSTANTIATE_TEST_SUITE_P INSTANTIATE_TEST_SUITE_P(Dhcp6GetConfigTest, Dhcp6GetConfigTest, ::testing::Range(static_cast(0), max_config_counter), IntToString()); #else INSTANTIATE_TEST_CASE_P(Dhcp6GetConfigTest, Dhcp6GetConfigTest, ::testing::Range(static_cast(0), max_config_counter), IntToString()); #endif } // namespace