diff options
Diffstat (limited to 'src/lib/dhcpsrv/tests/client_class_def_unittest.cc')
-rw-r--r-- | src/lib/dhcpsrv/tests/client_class_def_unittest.cc | 806 |
1 files changed, 806 insertions, 0 deletions
diff --git a/src/lib/dhcpsrv/tests/client_class_def_unittest.cc b/src/lib/dhcpsrv/tests/client_class_def_unittest.cc new file mode 100644 index 0000000..21c2e07 --- /dev/null +++ b/src/lib/dhcpsrv/tests/client_class_def_unittest.cc @@ -0,0 +1,806 @@ +// Copyright (C) 2015-2022 Internet Systems Consortium, Inc. ("ISC") +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +#include <config.h> +#include <cc/data.h> +#include <dhcpsrv/client_class_def.h> +#include <dhcpsrv/cfgmgr.h> +#include <dhcp/libdhcp++.h> +#include <dhcp/option_space.h> +#include <testutils/test_to_element.h> +#include <exceptions/exceptions.h> +#include <boost/scoped_ptr.hpp> +#include <asiolink/io_address.h> + +#include <boost/make_shared.hpp> +#include <gtest/gtest.h> + +/// @file client_class_def_unittest.cc Unit tests for client class storage +/// classes. + +using namespace std; +using namespace isc::data; +using namespace isc::dhcp; +using namespace isc::util; +using namespace isc::asiolink; +using namespace isc::test; +using namespace isc; + +namespace { + +// Tests basic construction of ClientClassDef +TEST(ClientClassDef, construction) { + boost::scoped_ptr<ClientClassDef> cclass; + + std::string name = "class1"; + ExpressionPtr expr; + CfgOptionPtr cfg_option; + + // Classes cannot have blank names + ASSERT_THROW(cclass.reset(new ClientClassDef("", expr, cfg_option)), + BadValue); + + // Verify we can create a class with a name, expression, and no cfg_option + ASSERT_NO_THROW(cclass.reset(new ClientClassDef(name, expr))); + EXPECT_EQ(name, cclass->getName()); + ASSERT_FALSE(cclass->getMatchExpr()); + EXPECT_FALSE(cclass->getCfgOptionDef()); + + // Verify we get an empty collection of cfg_option + cfg_option = cclass->getCfgOption(); + ASSERT_TRUE(cfg_option); + EXPECT_TRUE(cfg_option->empty()); + + // Verify we don't depend on something. + EXPECT_FALSE(cclass->dependOnClass("foobar")); + EXPECT_FALSE(cclass->dependOnClass("")); +} + +// Test that client class is copied using the copy constructor. +TEST(ClientClassDef, copyConstruction) { + auto expr = boost::make_shared<Expression>(); + + auto cfg_option = boost::make_shared<CfgOption>(); + auto option = boost::make_shared<Option>(Option::V6, 1024); + cfg_option->add(option, false, DHCP6_OPTION_SPACE); + + auto option_def = boost::make_shared<OptionDefinition>("foo", 1024, "dhcp6", "empty"); + CfgOptionDefPtr cfg_option_def = boost::make_shared<CfgOptionDef>(); + cfg_option_def->add(option_def); + + boost::scoped_ptr<ClientClassDef> cclass; + ASSERT_NO_THROW(cclass.reset(new ClientClassDef("class1", expr, cfg_option))); + cclass->setId(123); + cclass->setContext(data::Element::create("my-context")); + cclass->setCfgOptionDef(cfg_option_def); + cclass->setTest("member('KNOWN')"); + cclass->setRequired(true); + cclass->setDependOnKnown(true); + cclass->setNextServer(IOAddress("1.2.3.4")); + cclass->setSname("ufo"); + cclass->setFilename("ufo.efi"); + cclass->setValid(Triplet<uint32_t>(10, 20, 30)); + cclass->setPreferred(Triplet<uint32_t>(11, 21, 31)); + + // Copy the client class. + boost::scoped_ptr<ClientClassDef> cclass_copy; + ASSERT_NO_THROW(cclass_copy.reset(new ClientClassDef(*cclass))); + + // Ensure that class data was copied. + EXPECT_EQ(cclass->getName(), cclass_copy->getName()); + EXPECT_EQ(cclass->getId(), cclass_copy->getId()); + ASSERT_TRUE(cclass_copy->getContext()); + ASSERT_EQ(data::Element::string, cclass_copy->getContext()->getType()); + EXPECT_EQ("my-context", cclass_copy->getContext()->stringValue()); + ASSERT_TRUE(cclass->getMatchExpr()); + EXPECT_NE(cclass_copy->getMatchExpr(), cclass->getMatchExpr()); + EXPECT_EQ(cclass->getTest(), cclass_copy->getTest()); + EXPECT_EQ(cclass->getRequired(), cclass_copy->getRequired()); + EXPECT_EQ(cclass->getDependOnKnown(), cclass_copy->getDependOnKnown()); + EXPECT_EQ(cclass->getNextServer().toText(), cclass_copy->getNextServer().toText()); + EXPECT_EQ(cclass->getSname(), cclass_copy->getSname()); + EXPECT_EQ(cclass->getFilename(), cclass_copy->getFilename()); + EXPECT_EQ(cclass->getValid().get(), cclass_copy->getValid().get()); + EXPECT_EQ(cclass->getValid().getMin(), cclass_copy->getValid().getMin()); + EXPECT_EQ(cclass->getValid().getMax(), cclass_copy->getValid().getMax()); + EXPECT_EQ(cclass->getPreferred().get(), cclass_copy->getPreferred().get()); + EXPECT_EQ(cclass->getPreferred().getMin(), cclass_copy->getPreferred().getMin()); + EXPECT_EQ(cclass->getPreferred().getMax(), cclass_copy->getPreferred().getMax()); + + // Ensure that the option was copied into a new structure. + ASSERT_TRUE(cclass_copy->getCfgOption()); + EXPECT_NE(cclass_copy->getCfgOption(), cclass->getCfgOption()); + EXPECT_TRUE(cclass_copy->getCfgOption()->get("dhcp6", 1024).option_); + + // Ensure that the option definition was copied into a new structure. + ASSERT_TRUE(cclass_copy->getCfgOptionDef()); + EXPECT_NE(cclass_copy->getCfgOptionDef(), cclass->getCfgOptionDef()); + EXPECT_TRUE(cclass_copy->getCfgOptionDef()->get("dhcp6", 1024)); +} + +// Tests options operations. Note we just do the basics +// as CfgOption is heavily tested elsewhere. +TEST(ClientClassDef, cfgOptionBasics) { + boost::scoped_ptr<ClientClassDef> cclass; + + std::string name = "class1"; + ExpressionPtr expr; + CfgOptionPtr test_options; + CfgOptionPtr class_options; + OptionPtr opt; + + // First construct the class with empty option pointer + ASSERT_NO_THROW(cclass.reset(new ClientClassDef(name, expr, test_options))); + + // We should get back a collection with no entries, + // not an empty collection pointer + class_options = cclass->getCfgOption(); + ASSERT_TRUE(class_options); + + // Create an option container and add some options + OptionPtr option; + test_options.reset(new CfgOption()); + option.reset(new Option(Option::V4, 17, OptionBuffer(10, 0xFF))); + ASSERT_NO_THROW(test_options->add(option, false, DHCP4_OPTION_SPACE)); + + option.reset(new Option(Option::V6, 101, OptionBuffer(10, 0xFF))); + ASSERT_NO_THROW(test_options->add(option, false, "isc")); + + option.reset(new Option(Option::V6, 100, OptionBuffer(10, 0xFF))); + ASSERT_NO_THROW(test_options->add(option, false, DHCP6_OPTION_SPACE)); + + // Now remake the client class with cfg_option + ASSERT_NO_THROW(cclass.reset(new ClientClassDef(name, expr, test_options))); + class_options = cclass->getCfgOption(); + ASSERT_TRUE(class_options); + + // Now make sure we can find all the options + OptionDescriptor opt_desc = class_options->get(DHCP4_OPTION_SPACE,17); + ASSERT_TRUE(opt_desc.option_); + EXPECT_EQ(17, opt_desc.option_->getType()); + + opt_desc = class_options->get("isc",101); + ASSERT_TRUE(opt_desc.option_); + EXPECT_EQ(101, opt_desc.option_->getType()); + + opt_desc = class_options->get(DHCP6_OPTION_SPACE,100); + ASSERT_TRUE(opt_desc.option_); + EXPECT_EQ(100, opt_desc.option_->getType()); +} + +// Verifies copy constructor and equality tools (methods/operators) +TEST(ClientClassDef, copyAndEquality) { + + boost::scoped_ptr<ClientClassDef> cclass; + ExpressionPtr expr; + CfgOptionPtr test_options; + OptionPtr opt; + + // Make an expression + expr.reset(new Expression()); + TokenPtr token(new TokenString("boo")); + expr->push_back(token); + + // Create an option container with an option + OptionPtr option; + test_options.reset(new CfgOption()); + option.reset(new Option(Option::V4, 17, OptionBuffer(10, 0xFF))); + ASSERT_NO_THROW(test_options->add(option, false, DHCP4_OPTION_SPACE)); + + // Now remake the client class with cfg_option + ASSERT_NO_THROW(cclass.reset(new ClientClassDef("class_one", expr, + test_options))); + + // Now lets make a copy of it. + boost::scoped_ptr<ClientClassDef> cclass2; + ASSERT_NO_THROW(cclass2.reset(new ClientClassDef(*cclass))); + + // The allocated Expression pointers should not match + EXPECT_TRUE(cclass->getMatchExpr().get() != + cclass2->getMatchExpr().get()); + + // The allocated CfgOption pointers should not match + EXPECT_TRUE(cclass->getCfgOption().get() != + cclass2->getCfgOption().get()); + + // Verify the equality tools reflect that the classes are equal. + EXPECT_TRUE(cclass->equals(*cclass2)); + EXPECT_TRUE(*cclass == *cclass2); + EXPECT_FALSE(*cclass != *cclass2); + + // Verify the required flag is enough to make classes not equal. + EXPECT_FALSE(cclass->getRequired()); + cclass2->setRequired(true); + EXPECT_TRUE(cclass2->getRequired()); + EXPECT_FALSE(*cclass == *cclass2); + EXPECT_TRUE(*cclass != *cclass2); + cclass2->setRequired(false); + EXPECT_TRUE(*cclass == *cclass2); + + // Verify the depend on known flag is enough to make classes not equal. + EXPECT_FALSE(cclass->getDependOnKnown()); + cclass2->setDependOnKnown(true); + EXPECT_TRUE(cclass2->getDependOnKnown()); + EXPECT_FALSE(*cclass == *cclass2); + EXPECT_TRUE(*cclass != *cclass2); + + // Make a class that differs from the first class only by name and + // verify that the equality tools reflect that the classes are not equal. + ASSERT_NO_THROW(cclass2.reset(new ClientClassDef("class_two", expr, + test_options))); + EXPECT_FALSE(cclass->equals(*cclass2)); + EXPECT_FALSE(*cclass == *cclass2); + EXPECT_TRUE(*cclass != *cclass2); + + // Make a class with the same name and options, but no expression + // verify that the equality tools reflect that the classes are not equal. + expr.reset(); + ASSERT_NO_THROW(cclass2.reset(new ClientClassDef("class_one", expr, + test_options))); + EXPECT_FALSE(cclass->equals(*cclass2)); + EXPECT_FALSE(*cclass == *cclass2); + EXPECT_TRUE(*cclass != *cclass2); + + // Make a class with the same name and options, but different expression, + // verify that the equality tools reflect that the classes are not equal. + expr.reset(new Expression()); + token.reset(new TokenString("yah")); + expr->push_back(token); + ASSERT_NO_THROW(cclass2.reset(new ClientClassDef("class_one", expr, + test_options))); + EXPECT_FALSE(cclass->equals(*cclass2)); + EXPECT_FALSE(*cclass == *cclass2); + EXPECT_TRUE(*cclass != *cclass2); + + // Make a class that with same name, expression and options, but + // different option definitions, verify that the equality tools reflect + // that the equality tools reflect that the classes are not equal. + ASSERT_NO_THROW(cclass2.reset(new ClientClassDef(*cclass))); + EXPECT_TRUE(cclass->equals(*cclass2)); + OptionDefinitionPtr def = LibDHCP::getOptionDef(DHCP4_OPTION_SPACE, + DHO_VENDOR_ENCAPSULATED_OPTIONS); + EXPECT_FALSE(def); + def = LibDHCP::getLastResortOptionDef(DHCP4_OPTION_SPACE, DHO_VENDOR_ENCAPSULATED_OPTIONS); + EXPECT_TRUE(def); + CfgOptionDefPtr cfg(new CfgOptionDef()); + ASSERT_NO_THROW(cfg->add(def)); + cclass2->setCfgOptionDef(cfg); + EXPECT_FALSE(cclass->equals(*cclass2)); + EXPECT_FALSE(*cclass == *cclass2); + EXPECT_TRUE(*cclass != *cclass2); + + // Make a class with same name and expression, but no options + // verify that the equality tools reflect that the classes are not equal. + test_options.reset(new CfgOption()); + ASSERT_NO_THROW(cclass2.reset(new ClientClassDef("class_one", expr, + test_options))); + EXPECT_FALSE(cclass->equals(*cclass2)); + EXPECT_FALSE(*cclass == *cclass2); + EXPECT_TRUE(*cclass != *cclass2); + + // Make a class that with same name and expression, but different options + // verify that the equality tools reflect that the classes are not equal. + option.reset(new Option(Option::V4, 20, OptionBuffer(10, 0xFF))); + ASSERT_NO_THROW(test_options->add(option, false, DHCP4_OPTION_SPACE)); + ASSERT_NO_THROW(cclass2.reset(new ClientClassDef("class_one", expr, + test_options))); + EXPECT_FALSE(cclass->equals(*cclass2)); + EXPECT_FALSE(*cclass == *cclass2); + EXPECT_TRUE(*cclass != *cclass2); +} + +// Tests dependency. +TEST(ClientClassDef, dependency) { + boost::scoped_ptr<ClientClassDef> cclass; + + ExpressionPtr expr; + + // Make an expression + expr.reset(new Expression()); + TokenPtr token(new TokenMember("foo")); + expr->push_back(token); + + ASSERT_NO_THROW(cclass.reset(new ClientClassDef("class", expr))); + EXPECT_TRUE(cclass->dependOnClass("foo")); + EXPECT_FALSE(cclass->dependOnClass("bar")); +} + + +// Tests the basic operation of ClientClassDictionary +// This includes adding, finding, and removing classes +TEST(ClientClassDictionary, basics) { + ClientClassDictionaryPtr dictionary; + ClientClassDefPtr cclass; + ExpressionPtr expr; + CfgOptionPtr cfg_option; + + // Verify constructor doesn't throw + ASSERT_NO_THROW(dictionary.reset(new ClientClassDictionary())); + + // Verify we can fetch a pointer the list of classes and + // that we start with no classes defined + const ClientClassDefListPtr classes = dictionary->getClasses(); + ASSERT_TRUE(classes); + EXPECT_EQ(0, classes->size()); + EXPECT_TRUE(classes->empty()); + + // Verify that we can add classes with both addClass variants + // First addClass(name, expression, cfg_option) + ASSERT_NO_THROW(dictionary->addClass("cc1", expr, "", false, + false, cfg_option)); + ASSERT_NO_THROW(dictionary->addClass("cc2", expr, "", false, + false, cfg_option)); + + // Verify duplicate add attempt throws + ASSERT_THROW(dictionary->addClass("cc2", expr, "", false, + false, cfg_option), + DuplicateClientClassDef); + + // Verify that you cannot add a class with no name. + ASSERT_THROW(dictionary->addClass("", expr, "", false, + false, cfg_option), + BadValue); + + // Now with addClass(class pointer) + ASSERT_NO_THROW(cclass.reset(new ClientClassDef("cc3", expr, cfg_option))); + ASSERT_NO_THROW(dictionary->addClass(cclass)); + + // Verify duplicate add attempt throws + ASSERT_THROW(dictionary->addClass(cclass), DuplicateClientClassDef); + + // Verify that you cannot add empty class pointer + cclass.reset(); + ASSERT_THROW(dictionary->addClass(cclass), BadValue); + + // Map should show 3 entries. + EXPECT_EQ(3, classes->size()); + EXPECT_FALSE(classes->empty()); + + // Removing client class by id of 0 should be no-op. + ASSERT_NO_THROW(dictionary->removeClass(0)); + EXPECT_EQ(3, classes->size()); + EXPECT_FALSE(classes->empty()); + + // Verify we can find them all. + ASSERT_NO_THROW(cclass = dictionary->findClass("cc1")); + ASSERT_TRUE(cclass); + EXPECT_EQ("cc1", cclass->getName()); + cclass->setId(1); + + ASSERT_NO_THROW(cclass = dictionary->findClass("cc2")); + ASSERT_TRUE(cclass); + EXPECT_EQ("cc2", cclass->getName()); + cclass->setId(2); + + ASSERT_NO_THROW(cclass = dictionary->findClass("cc3")); + ASSERT_TRUE(cclass); + EXPECT_EQ("cc3", cclass->getName()); + cclass->setId(3); + + // Verify the looking for non-existing returns empty pointer + ASSERT_NO_THROW(cclass = dictionary->findClass("bogus")); + EXPECT_FALSE(cclass); + + // Verify that we can remove a class + ASSERT_NO_THROW(dictionary->removeClass("cc3")); + EXPECT_EQ(2, classes->size()); + EXPECT_FALSE(classes->empty()); + + // Shouldn't be able to find anymore + ASSERT_NO_THROW(cclass = dictionary->findClass("cc3")); + EXPECT_FALSE(cclass); + + // Verify that we can attempt to remove a non-existing class + // without harm. + ASSERT_NO_THROW(dictionary->removeClass("cc3")); + EXPECT_EQ(2, classes->size()); + EXPECT_FALSE(classes->empty()); + + // Verify that we can remove client class by id. + ASSERT_NO_THROW(dictionary->removeClass(2)); + EXPECT_EQ(1, classes->size()); + EXPECT_FALSE(classes->empty()); + ASSERT_NO_THROW(cclass = dictionary->findClass("cc2")); + EXPECT_FALSE(cclass); +} + +// Verifies copy constructor and equality tools (methods/operators) +TEST(ClientClassDictionary, copyAndEquality) { + ClientClassDictionaryPtr dictionary; + ClientClassDictionaryPtr dictionary2; + ClientClassDefPtr cclass; + ExpressionPtr expr; + CfgOptionPtr options; + + dictionary.reset(new ClientClassDictionary()); + ASSERT_NO_THROW(dictionary->addClass("one", expr, "", false, + false, options)); + ASSERT_NO_THROW(dictionary->addClass("two", expr, "", false, + false, options)); + ASSERT_NO_THROW(dictionary->addClass("three", expr, "", false, + false, options)); + + // Copy constructor should succeed. + ASSERT_NO_THROW(dictionary2.reset(new ClientClassDictionary(*dictionary))); + + // Allocated class list pointers should not be equal + EXPECT_NE(dictionary->getClasses().get(), dictionary2->getClasses().get()); + + // Equality tools should reflect that the dictionaries are equal. + EXPECT_TRUE(dictionary->equals(*dictionary2)); + EXPECT_TRUE(*dictionary == *dictionary2); + EXPECT_FALSE(*dictionary != *dictionary2); + + // Remove a class from dictionary2. + ASSERT_NO_THROW(dictionary2->removeClass("two")); + + // Equality tools should reflect that the dictionaries are not equal. + EXPECT_FALSE(dictionary->equals(*dictionary2)); + EXPECT_FALSE(*dictionary == *dictionary2); + EXPECT_TRUE(*dictionary != *dictionary2); + + // Create an empty dictionary. + dictionary2.reset(new ClientClassDictionary()); + + // Equality tools should reflect that the dictionaries are not equal. + EXPECT_FALSE(dictionary->equals(*dictionary2)); + EXPECT_FALSE(*dictionary == *dictionary2); + EXPECT_TRUE(*dictionary != *dictionary2); +} + +// Verify that client class dictionaries are deep-copied. +TEST(ClientClassDictionary, copy) { + ClientClassDictionary dictionary; + ExpressionPtr expr; + CfgOptionPtr options; + + // Get a client class dictionary and fill it. + ASSERT_NO_THROW(dictionary.addClass("one", expr, "", false, + false, options)); + ASSERT_NO_THROW(dictionary.addClass("two", expr, "", false, + false, options)); + ASSERT_NO_THROW(dictionary.addClass("three", expr, "", false, + false, options)); + + // Make a copy with a copy constructor. Expect it to be a deep copy. + ClientClassDictionary dictionary_copy(dictionary); + ASSERT_NO_THROW(dictionary.removeClass("one")); + ASSERT_NO_THROW(dictionary.removeClass("two")); + ASSERT_NO_THROW(dictionary.removeClass("three")); + EXPECT_TRUE(dictionary.empty()); + EXPECT_FALSE(dictionary_copy.empty()); + + // Refill the client class dictionary. + ASSERT_NO_THROW(dictionary.addClass("one", expr, "", false, + false, options)); + ASSERT_NO_THROW(dictionary.addClass("two", expr, "", false, + false, options)); + ASSERT_NO_THROW(dictionary.addClass("three", expr, "", false, + false, options)); + + // Make a copy with operator=. Expect it to be a deep copy. + dictionary_copy = dictionary; + ASSERT_NO_THROW(dictionary.removeClass("one")); + ASSERT_NO_THROW(dictionary.removeClass("two")); + ASSERT_NO_THROW(dictionary.removeClass("three")); + EXPECT_TRUE(dictionary.empty()); + EXPECT_FALSE(dictionary_copy.empty()); +} + +// Tests dependency. +TEST(ClientClassDictionary, dependency) { + ClientClassDictionaryPtr dictionary(new ClientClassDictionary()); + ExpressionPtr expr; + CfgOptionPtr cfg_option; + + // Make an expression depending on forward class. + ExpressionPtr expr1; + expr1.reset(new Expression()); + TokenPtr token1(new TokenMember("cc2")); + expr1->push_back(token1); + + ASSERT_NO_THROW(dictionary->addClass("cc1", expr1, "", false, + false, cfg_option)); + + // Make an expression depending on first class. + ExpressionPtr expr2; + expr2.reset(new Expression()); + TokenPtr token2(new TokenMember("cc1")); + expr2->push_back(token2); + + ASSERT_NO_THROW(dictionary->addClass("cc2", expr2, "", false, + false, cfg_option)); + + // Make expression with dependency. + ASSERT_NO_THROW(dictionary->addClass("cc3", expr, "", false, + false, cfg_option)); + + ExpressionPtr expr3; + expr3.reset(new Expression()); + TokenPtr token3(new TokenMember("cc3")); + expr3->push_back(token3); + + ASSERT_NO_THROW(dictionary->addClass("cc4", expr3, "", false, + false, cfg_option)); + + // Not matching dependency does not match. + string depend; + EXPECT_FALSE(dictionary->dependOnClass("foobar", depend)); + EXPECT_TRUE(depend.empty()); + + // Forward dependency is ignored. + depend = ""; + EXPECT_FALSE(dictionary->dependOnClass("cc2", depend)); + EXPECT_TRUE(depend.empty()); + + // Backward dependency is detected. + depend = ""; + EXPECT_TRUE(dictionary->dependOnClass("cc3", depend)); + EXPECT_EQ("cc4", depend); +} + +// Tests that match expressions are set for all client classes in the +// dictionary. +TEST(ClientClassDictionary, initMatchExpr) { + ClientClassDictionaryPtr dictionary(new ClientClassDictionary()); + ExpressionPtr expr; + CfgOptionPtr cfg_option; + + // Add several classes. + ASSERT_NO_THROW(dictionary->addClass("foo", expr, "", false, + false, cfg_option)); + ASSERT_NO_THROW(dictionary->addClass("bar", expr, "member('KNOWN') or member('foo')", false, + false, cfg_option)); + ASSERT_NO_THROW(dictionary->addClass("baz", expr, "substring(option[61].hex,0,3) == 'foo'", false, + false, cfg_option)); + + // Create match expressions for all of them. + ASSERT_NO_THROW(dictionary->initMatchExpr(AF_INET)); + + // Ensure that the expressions were created only if 'test' is not empty. + auto classes = *(dictionary->getClasses()); + EXPECT_FALSE(classes[0]->getMatchExpr()); + + EXPECT_TRUE(classes[1]->getMatchExpr()); + EXPECT_EQ(3, classes[1]->getMatchExpr()->size()); + + EXPECT_TRUE(classes[2]->getMatchExpr()); + EXPECT_EQ(6, classes[2]->getMatchExpr()->size()); +} + +// Tests that an error is returned when any of the test expressions is +// invalid, and that no expressions are initialized if there is an error +// for a single expression. +TEST(ClientClassDictionary, initMatchExprError) { + ClientClassDictionaryPtr dictionary(new ClientClassDictionary()); + ExpressionPtr expr; + CfgOptionPtr cfg_option; + + // Add several classes. One of them has invalid test expression. + ASSERT_NO_THROW(dictionary->addClass("foo", expr, "member('KNOWN')", false, + false, cfg_option)); + ASSERT_NO_THROW(dictionary->addClass("bar", expr, "wrong expression", false, + false, cfg_option)); + ASSERT_NO_THROW(dictionary->addClass("baz", expr, "substring(option[61].hex,0,3) == 'foo'", false, + false, cfg_option)); + + // An attempt to initialize match expressions should fail because the + // test expression for the second class is invalid. + ASSERT_THROW(dictionary->initMatchExpr(AF_INET), std::exception); + + // Ensure that no classes have their match expressions modified. + for (auto c : (*dictionary->getClasses())) { + EXPECT_FALSE(c->getMatchExpr()); + } +} + +// Tests the default constructor regarding fixed fields +TEST(ClientClassDef, fixedFieldsDefaults) { + boost::scoped_ptr<ClientClassDef> cclass; + + std::string name = "class1"; + ExpressionPtr expr; + CfgOptionPtr cfg_option; + + // Classes cannot have blank names + ASSERT_THROW(cclass.reset(new ClientClassDef("", expr, cfg_option)), + BadValue); + + // Verify we can create a class with a name, expression, and no cfg_option + ASSERT_NO_THROW(cclass.reset(new ClientClassDef(name, expr))); + + // Let's checks that it doesn't return any nonsense + EXPECT_FALSE(cclass->getRequired()); + EXPECT_FALSE(cclass->getDependOnKnown()); + EXPECT_FALSE(cclass->getCfgOptionDef()); + string empty; + ASSERT_EQ(IOAddress("0.0.0.0"), cclass->getNextServer()); + EXPECT_EQ(empty, cclass->getSname()); + EXPECT_EQ(empty, cclass->getFilename()); +} + +// Tests basic operations of fixed fields +TEST(ClientClassDef, fixedFieldsBasics) { + boost::scoped_ptr<ClientClassDef> cclass; + + std::string name = "class1"; + ExpressionPtr expr; + CfgOptionPtr cfg_option; + + // Classes cannot have blank names + ASSERT_THROW(cclass.reset(new ClientClassDef("", expr, cfg_option)), + BadValue); + + // Verify we can create a class with a name, expression, and no cfg_option + ASSERT_NO_THROW(cclass.reset(new ClientClassDef(name, expr))); + + cclass->setRequired(true); + cclass->setDependOnKnown(true); + + string sname = "This is a very long string that can be a server name"; + string filename = "this-is-a-slightly-longish-name-of-a-file.txt"; + + cclass->setNextServer(IOAddress("1.2.3.4")); + cclass->setSname(sname); + cclass->setFilename(filename); + + // Let's checks that it doesn't return any nonsense + EXPECT_TRUE(cclass->getRequired()); + EXPECT_TRUE(cclass->getDependOnKnown()); + EXPECT_EQ(IOAddress("1.2.3.4"), cclass->getNextServer()); + EXPECT_EQ(sname, cclass->getSname()); + EXPECT_EQ(filename, cclass->getFilename()); +} + + +// Verifies the unparse method of option class definitions +TEST(ClientClassDef, unparseDef) { + CfgMgr::instance().setFamily(AF_INET); + boost::scoped_ptr<ClientClassDef> cclass; + + // Get a client class definition and fill it + std::string name = "class1"; + ExpressionPtr expr; + ASSERT_NO_THROW(cclass.reset(new ClientClassDef(name, expr))); + std::string test = "option[12].text == 'foo'"; + cclass->setTest(test); + std::string comment = "bar"; + std::string user_context = "{ \"comment\": \"" + comment + "\", "; + user_context += "\"bar\": 1 }"; + cclass->setContext(isc::data::Element::fromJSON(user_context)); + cclass->setRequired(true); + // The depend on known flag in not visible + cclass->setDependOnKnown(true); + std::string next_server = "1.2.3.4"; + cclass->setNextServer(IOAddress(next_server)); + std::string sname = "my-server.example.com"; + cclass->setSname(sname); + std::string filename = "/boot/kernel"; + cclass->setFilename(filename); + + // Unparse it + std::string expected = "{\n" + "\"name\": \"" + name + "\",\n" + "\"test\": \"" + test + "\",\n" + "\"only-if-required\": true,\n" + "\"next-server\": \"" + next_server + "\",\n" + "\"server-hostname\": \"" + sname + "\",\n" + "\"boot-file-name\": \"" + filename + "\",\n" + "\"option-data\": [ ],\n" + "\"user-context\": { \"bar\": 1,\n" + "\"comment\": \"" + comment + "\" } }\n"; + runToElementTest<ClientClassDef>(expected, *cclass); +} + +// Verifies the unparse method of client class dictionaries +TEST(ClientClassDictionary, unparseDict) { + CfgMgr::instance().setFamily(AF_INET); + ClientClassDictionaryPtr dictionary; + ExpressionPtr expr; + CfgOptionPtr options; + + // Get a client class dictionary and fill it + dictionary.reset(new ClientClassDictionary()); + ASSERT_NO_THROW(dictionary->addClass("one", expr, "", false, + false, options)); + ASSERT_NO_THROW(dictionary->addClass("two", expr, "", false, + false, options)); + ASSERT_NO_THROW(dictionary->addClass("three", expr, "", false, + false, options)); + + // Unparse it + auto add_defaults = + [](std::string name) { + return ("{\n" + "\"name\": \"" + name + "\",\n" + "\"next-server\": \"0.0.0.0\",\n" + "\"server-hostname\": \"\",\n" + "\"boot-file-name\": \"\",\n" + "\"option-data\": [ ] }"); + }; + + std::string expected = "[\n" + + add_defaults("one") + ",\n" + + add_defaults("two") + ",\n" + + add_defaults("three") + "]\n"; + + runToElementTest<ClientClassDictionary>(expected, *dictionary); +} + +// Tests that options have been created for all client classes in the +// dictionary. +TEST(ClientClassDictionary, createOptions) { + ClientClassDictionaryPtr dictionary(new ClientClassDictionary()); + ExpressionPtr expr; + CfgOptionPtr cfg_option; + + // First class has no options. + ASSERT_NO_THROW(dictionary->addClass("foo", expr, "", false, + false, cfg_option)); + + // Make some options for the second class. + cfg_option.reset(new CfgOption()); + OptionPtr option = Option::create(Option::V4, DHO_BOOT_FILE_NAME); + OptionDescriptorPtr desc = OptionDescriptor::create(option, true, "bogus-file.txt"); + desc->space_name_ = DHCP4_OPTION_SPACE; + cfg_option->add(*desc, desc->space_name_); + + option = Option::create(Option::V4, DHO_TFTP_SERVER_NAME); + desc = OptionDescriptor::create(option, true, "bogus-tftp-server"); + desc->space_name_ = DHCP4_OPTION_SPACE; + cfg_option->add(*desc, desc->space_name_); + + // Add the second class with options. + ASSERT_NO_THROW(dictionary->addClass("bar", expr, "", false, + false, cfg_option)); + + // Make sure first class has no options. + ASSERT_TRUE(dictionary->getClasses()); + auto classes = *(dictionary->getClasses()); + auto options = classes[0]->getCfgOption(); + ASSERT_TRUE(options->empty()); + + // Make sure second class has both options but their + // data buffers are empty. + options = classes[1]->getCfgOption(); + ASSERT_FALSE(options->empty()); + + auto option_desc = options->get("dhcp4", DHO_BOOT_FILE_NAME); + ASSERT_TRUE(option_desc.option_); + ASSERT_TRUE(option_desc.option_->getData().empty()); + + option_desc = options->get("dhcp4", DHO_TFTP_SERVER_NAME); + ASSERT_TRUE(option_desc.option_); + ASSERT_TRUE(option_desc.option_->getData().empty()); + + // Now create match expressions for all of them. + auto cfg_def = CfgMgr::instance().getCurrentCfg()->getCfgOptionDef(); + ASSERT_NO_THROW(dictionary->createOptions(cfg_def)); + + // Make sure first class still has no options. + classes = *(dictionary->getClasses()); + options = classes[0]->getCfgOption(); + ASSERT_TRUE(options->empty()); + + // Make sure second class has both options and that their + // data buffers are now correctly populated. + options = classes[1]->getCfgOption(); + ASSERT_FALSE(options->empty()); + + option_desc = options->get("dhcp4", DHO_BOOT_FILE_NAME); + option = option_desc.option_; + ASSERT_TRUE(option); + EXPECT_EQ(OptionBuffer(option_desc.formatted_value_.begin(), option_desc.formatted_value_.end()), + option->getData()); + + option_desc = options->get("dhcp4", DHO_TFTP_SERVER_NAME); + option = option_desc.option_; + ASSERT_TRUE(option); + EXPECT_EQ(OptionBuffer(option_desc.formatted_value_.begin(), option_desc.formatted_value_.end()), + option->getData()); +} + +} // end of anonymous namespace |