diff options
Diffstat (limited to 'src/lib/dhcpsrv/tests/client_class_def_parser_unittest.cc')
-rw-r--r-- | src/lib/dhcpsrv/tests/client_class_def_parser_unittest.cc | 1454 |
1 files changed, 1454 insertions, 0 deletions
diff --git a/src/lib/dhcpsrv/tests/client_class_def_parser_unittest.cc b/src/lib/dhcpsrv/tests/client_class_def_parser_unittest.cc new file mode 100644 index 0000000..7751591 --- /dev/null +++ b/src/lib/dhcpsrv/tests/client_class_def_parser_unittest.cc @@ -0,0 +1,1454 @@ +// 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 <dhcp/libdhcp++.h> +#include <dhcp/option.h> +#include <dhcp/option_string.h> +#include <dhcpsrv/cfgmgr.h> +#include <dhcpsrv/parsers/client_class_def_parser.h> +#include <dhcpsrv/parsers/dhcp_parsers.h> +#include <asiolink/io_address.h> +#include <eval/evaluate.h> +#include <testutils/gtest_utils.h> +#include <gtest/gtest.h> +#include <sstream> +#include <stdint.h> +#include <string> + +/// @file client_class_def_parser_unittest.cc Unit tests for client class +/// definition parsing. + +using namespace isc::data; +using namespace isc::dhcp; +using namespace isc::asiolink; +using namespace isc::util; + +namespace { + +/// @brief Test fixture class for @c ExpressionParser. +class ExpressionParserTest : public ::testing::Test { +protected: + + /// @brief Test that validate expression can be evaluated against v4 or + /// v6 packet. + /// + /// Verifies that given a valid expression, the ExpressionParser + /// produces an Expression which can be evaluated against a v4 or v6 + /// packet. + /// + /// @param family AF_INET or AF_INET6 + /// @param expression Textual representation of the expression to be + /// evaluated. + /// @param option_string String data to be placed in the hostname + /// option, being placed in the packet used for evaluation. + /// @tparam Type of the packet: @c Pkt4 or @c Pkt6. + template<typename PktType> + void testValidExpression(uint16_t family, + const std::string& expression, + const std::string& option_string) { + ExpressionPtr parsed_expr; + ExpressionParser parser; + + // Turn config into elements. This may emit exceptions. + ElementPtr config_element = Element::fromJSON(expression); + + // Expression should parse. + ASSERT_NO_THROW(parser.parse(parsed_expr, config_element, family)); + + // Parsed expression should exist. + ASSERT_TRUE(parsed_expr); + + // Build a packet that will fail evaluation. + uint8_t message_type; + if (family == AF_INET) { + message_type = DHCPDISCOVER; + } else { + message_type = DHCPV6_SOLICIT; + } + boost::shared_ptr<PktType> pkt(new PktType(message_type, 123)); + EXPECT_FALSE(evaluateBool(*parsed_expr, *pkt)); + + // Now add the option so it will pass. Use a standard option carrying a + // single string value, i.e. hostname for DHCPv4 and bootfile url for + // DHCPv6. + Option::Universe universe(family == AF_INET ? Option::V4 : Option::V6); + uint16_t option_type; + if (family == AF_INET) { + option_type = DHO_HOST_NAME; + } else { + option_type = D6O_BOOTFILE_URL; + } + OptionPtr opt(new OptionString(universe, option_type, option_string)); + pkt->addOption(opt); + EXPECT_TRUE(evaluateBool(*parsed_expr, *pkt)); + } +}; + +/// @brief Test fixture class for @c ClientClassDefParser. +class ClientClassDefParserTest : public ::testing::Test { +protected: + + /// @brief Convenience method for parsing a configuration + /// + /// Attempt to parse a given client class definition. + /// + /// @param config - JSON string containing the client class configuration + /// to parse. + /// @param family - the address family in which the parsing should + /// occur. + /// @return Returns a pointer to class instance created, or NULL if + /// for some unforeseen reason it wasn't created in the local dictionary + /// @throw indirectly, exceptions converting the JSON text to elements, + /// or by the parsing itself are not caught + ClientClassDefPtr parseClientClassDef(const std::string& config, + uint16_t family) { + // Create local dictionary to which the parser add the class. + ClientClassDictionaryPtr dictionary(new ClientClassDictionary()); + + // Turn config into elements. This may emit exceptions. + ElementPtr config_element = Element::fromJSON(config); + + // Parse the configuration. This may emit exceptions. + ClientClassDefParser parser; + parser.parse(dictionary, config_element, family); + + // If we didn't throw, then return the first and only class + ClientClassDefListPtr classes = dictionary->getClasses(); + ClientClassDefList::const_iterator it = classes->cbegin(); + if (it != classes->cend()) { + return (*it); + } + + // Return NULL if for some reason the class doesn't exist. + return (ClientClassDefPtr()); + } + + /// @brief Test that client class parser throws when unspported parameter + /// is specified. + /// + /// @param config JSON string containing the client class configuration. + /// @param family The address family indicating whether the DHCPv4 or + /// DHCPv6 client class is parsed. + void testClassParamsUnsupported(const std::string& config, + const uint16_t family) { + ElementPtr config_element = Element::fromJSON(config); + + ClientClassDefParser parser; + EXPECT_THROW(parser.checkParametersSupported(config_element, family), + DhcpConfigError); + } +}; + +/// @brief Test fixture class for @c ClientClassDefListParser. +class ClientClassDefListParserTest : public ::testing::Test { +protected: + + /// @brief Convenience method for parsing a list of client class + /// definitions. + /// + /// Attempt to parse a given list of client class definitions into a + /// ClientClassDictionary. + /// + /// @param config - JSON string containing the list of definitions to parse. + /// @param family - the address family in which the parsing should + /// occur. + /// @param check_dependencies - indicates if the parser should check whether + /// referenced classes exist. + /// @return Returns a pointer to class dictionary created + /// @throw indirectly, exceptions converting the JSON text to elements, + /// or by the parsing itself are not caught + ClientClassDictionaryPtr parseClientClassDefList(const std::string& config, + uint16_t family, + bool check_dependencies = true) + { + // Turn config into elements. This may emit exceptions. + ElementPtr config_element = Element::fromJSON(config); + + // Parse the configuration. This may emit exceptions. + ClientClassDefListParser parser; + return (parser.parse(config_element, family, check_dependencies)); + } +}; + +// Verifies that given a valid expression, the ExpressionParser +// produces an Expression which can be evaluated against a v4 packet. +TEST_F(ExpressionParserTest, validExpression4) { + testValidExpression<Pkt4>(AF_INET, + "\"option[12].text == 'hundred4'\"", + "hundred4"); +} + +// Verifies that the option name can be used in the evaluated expression. +TEST_F(ExpressionParserTest, validExpressionWithOptionName4) { + testValidExpression<Pkt4>(AF_INET, + "\"option[host-name].text == 'hundred4'\"", + "hundred4"); +} + +// Verifies that given a valid expression using .hex operator for option, the +// ExpressionParser produces an Expression which can be evaluated against +// a v4 packet. +TEST_F(ExpressionParserTest, validExpressionWithHex4) { + testValidExpression<Pkt4>(AF_INET, + "\"option[12].hex == 0x68756E6472656434\"", + "hundred4"); +} + +// Verifies that the option name can be used together with .hex operator in +// the evaluated expression. +TEST_F(ExpressionParserTest, validExpressionWithOptionNameAndHex4) { + testValidExpression<Pkt6>(AF_INET, + "\"option[host-name].text == 0x68756E6472656434\"", + "hundred4"); +} + +// Verifies that given a valid expression, the ExpressionParser +// produces an Expression which can be evaluated against a v6 packet. +TEST_F(ExpressionParserTest, validExpression6) { + testValidExpression<Pkt6>(AF_INET6, + "\"option[59].text == 'hundred6'\"", + "hundred6"); +} + +// Verifies that the option name can be used in the evaluated expression. +TEST_F(ExpressionParserTest, validExpressionWithOptionName6) { + testValidExpression<Pkt6>(AF_INET6, + "\"option[bootfile-url].text == 'hundred6'\"", + "hundred6"); +} + +// Verifies that given a valid expression using .hex operator for option, the +// ExpressionParser produces an Expression which can be evaluated against +// a v6 packet. +TEST_F(ExpressionParserTest, validExpressionWithHex6) { + testValidExpression<Pkt6>(AF_INET6, + "\"option[59].hex == 0x68756E6472656436\"", + "hundred6"); +} + +// Verifies that the option name can be used together with .hex operator in +// the evaluated expression. +TEST_F(ExpressionParserTest, validExpressionWithOptionNameAndHex6) { + testValidExpression<Pkt6>(AF_INET6, + "\"option[bootfile-url].text == 0x68756E6472656436\"", + "hundred6"); +} + +// Verifies that an the ExpressionParser only accepts StringElements. +TEST_F(ExpressionParserTest, invalidExpressionElement) { + // This will create an integer element should fail. + std::string cfg_txt = "777"; + ElementPtr config_element = Element::fromJSON(cfg_txt); + + // Create the parser. + ExpressionPtr parsed_expr; + ExpressionParser parser; + + // Expression parsing should fail. + ASSERT_THROW(parser.parse(parsed_expr, config_element, AF_INET6), + DhcpConfigError); +} + +// Verifies that given an invalid expression with a syntax error, +// the Expression parser will throw a DhdpConfigError. Note this +// is not intended to be an exhaustive test or expression syntax. +// It is simply to ensure that if the parser fails, it does so +// Properly. +TEST_F(ExpressionParserTest, expressionSyntaxError) { + // Turn config into elements. + std::string cfg_txt = "\"option 'bogus'\""; + ElementPtr config_element = Element::fromJSON(cfg_txt); + + // Create the parser. + ExpressionPtr parsed_expr; + ExpressionParser parser; + + // Expression parsing should fail. + ASSERT_THROW(parser.parse(parsed_expr, config_element, AF_INET), + DhcpConfigError); +} + +// Verifies that the name parameter is required and must not be empty +TEST_F(ExpressionParserTest, nameEmpty) { + std::string cfg_txt = "{ \"name\": \"\" }"; + ElementPtr config_element = Element::fromJSON(cfg_txt); + + // Create the parser. + ExpressionPtr parsed_expr; + ExpressionParser parser; + + // Expression parsing should fail. + ASSERT_THROW(parser.parse(parsed_expr, config_element, AF_INET6), + DhcpConfigError); +} + +// Verifies that the function checking if specified client class parameters +// are supported does not throw if all specified DHCPv4 client class +// parameters are recognized. +TEST_F(ClientClassDefParserTest, checkAllSupported4) { + std::string cfg_text = + "{\n" + " \"name\": \"foo\"," + " \"test\": \"member('ALL')\"," + " \"option-def\": [ ],\n" + " \"option-data\": [ ],\n" + " \"user-context\": { },\n" + " \"only-if-required\": false,\n" + " \"next-server\": \"192.0.2.3\",\n" + " \"server-hostname\": \"myhost\",\n" + " \"boot-file-name\": \"efi\"" + "}\n"; + + ElementPtr config_element = Element::fromJSON(cfg_text); + + ClientClassDefParser parser; + EXPECT_NO_THROW(parser.checkParametersSupported(config_element, AF_INET)); +} + +// Verifies that the function checking if specified client class parameters +// are supported does not throw if all specified DHCPv6 client class +// parameters are recognized. +TEST_F(ClientClassDefParserTest, checkAllSupported6) { + std::string cfg_text = + "{\n" + " \"name\": \"foo\"," + " \"test\": \"member('ALL')\"," + " \"option-data\": [ ],\n" + " \"user-context\": { },\n" + " \"only-if-required\": false\n" + "}\n"; + + ElementPtr config_element = Element::fromJSON(cfg_text); + + ClientClassDefParser parser; + EXPECT_NO_THROW(parser.checkParametersSupported(config_element, AF_INET6)); +} + +// Verifies that the function checking if specified client class parameters +// are supported throws if DHCPv4 specific parameters are specified for the +// DHCPv6 client class. +TEST_F(ClientClassDefParserTest, checkParams4Unsupported6) { + std::string cfg_text; + + { + SCOPED_TRACE("option-def"); + cfg_text = + "{\n" + " \"name\": \"foo\"," + " \"test\": \"member('ALL')\"," + " \"option-def\": [ ],\n" + " \"option-data\": [ ],\n" + " \"user-context\": { },\n" + " \"only-if-required\": false\n" + "}\n"; + + testClassParamsUnsupported(cfg_text, AF_INET6); + } + + { + SCOPED_TRACE("next-server"); + cfg_text = + "{\n" + " \"name\": \"foo\"," + " \"test\": \"member('ALL')\"," + " \"option-data\": [ ],\n" + " \"user-context\": { },\n" + " \"only-if-required\": false,\n" + " \"next-server\": \"192.0.2.3\"\n" + "}\n"; + + testClassParamsUnsupported(cfg_text, AF_INET6); + } + + { + SCOPED_TRACE("server-hostname"); + cfg_text = + "{\n" + " \"name\": \"foo\"," + " \"test\": \"member('ALL')\"," + " \"option-data\": [ ],\n" + " \"user-context\": { },\n" + " \"only-if-required\": false,\n" + " \"server-hostname\": \"myhost\"\n" + "}\n"; + + testClassParamsUnsupported(cfg_text, AF_INET6); + } + + { + SCOPED_TRACE("boot-file-name"); + cfg_text = + "{\n" + " \"name\": \"foo\"," + " \"test\": \"member('ALL')\"," + " \"option-data\": [ ],\n" + " \"user-context\": { },\n" + " \"only-if-required\": false,\n" + " \"boot-file-name\": \"efi\"" + "}\n"; + + testClassParamsUnsupported(cfg_text, AF_INET6); + } +} + +// Verifies that the function checking if specified DHCPv4 client class +// parameters are supported throws if one of the parameters is not recognized. +TEST_F(ClientClassDefParserTest, checkParams4Unsupported) { + std::string cfg_text = + "{\n" + " \"name\": \"foo\"," + " \"unsupported\": \"member('ALL')\"" + "}\n"; + + testClassParamsUnsupported(cfg_text, AF_INET); +} + +// Verifies that the function checking if specified DHCPv6 client class +// parameters are supported throws if one of the parameters is not recognized. +TEST_F(ClientClassDefParserTest, checkParams6Unsupported) { + std::string cfg_text = + "{\n" + " \"name\": \"foo\"," + " \"unsupported\": \"member('ALL')\"" + "}\n"; + + testClassParamsUnsupported(cfg_text, AF_INET6); +} + +// Verifies you can create a class with only a name +// Whether that's useful or not, remains to be seen. +// For now the class allows it. +TEST_F(ClientClassDefParserTest, nameOnlyValid) { + std::string cfg_text = + "{ \n" + " \"name\": \"MICROSOFT\" \n" + "} \n"; + + ClientClassDefPtr cclass; + ASSERT_NO_THROW(cclass = parseClientClassDef(cfg_text, AF_INET)); + + // We should find our class. + ASSERT_TRUE(cclass); + EXPECT_EQ("MICROSOFT", cclass->getName()); + + // CfgOption should be a non-null pointer but there + // should be no options. Currently there's no good + // way to test that there no options. + CfgOptionPtr cfg_option; + cfg_option = cclass->getCfgOption(); + ASSERT_TRUE(cfg_option); + OptionContainerPtr oc; + ASSERT_TRUE(oc = cclass->getCfgOption()->getAll(DHCP4_OPTION_SPACE)); + EXPECT_EQ(0, oc->size()); + + // Verify we have no expression. + ASSERT_FALSE(cclass->getMatchExpr()); +} + +// Verifies you can create a class with a name, expression, +// but no options. +// @todo same with AF_INET6 +TEST_F(ClientClassDefParserTest, nameAndExpressionClass) { + + std::string test = "option[100].text == 'works right'"; + std::string cfg_text = + "{ \n" + " \"name\": \"class_one\", \n" + " \"test\": \"" + test + "\" \n" + "} \n"; + + ClientClassDefPtr cclass; + ASSERT_NO_THROW(cclass = parseClientClassDef(cfg_text, AF_INET)); + + // We should find our class. + ASSERT_TRUE(cclass); + EXPECT_EQ("class_one", cclass->getName()); + + // CfgOption should be a non-null pointer but there + // should be no options. Currently there's no good + // way to test that there no options. + CfgOptionPtr cfg_option; + cfg_option = cclass->getCfgOption(); + ASSERT_TRUE(cfg_option); + OptionContainerPtr oc; + ASSERT_TRUE(oc = cclass->getCfgOption()->getAll(DHCP4_OPTION_SPACE)); + EXPECT_EQ(0, oc->size()); + + // Verify we can retrieve the expression + ExpressionPtr match_expr = cclass->getMatchExpr(); + ASSERT_TRUE(match_expr); + + // Verify the original expression was saved. + EXPECT_EQ(test, cclass->getTest()); + + // Build a packet that will fail evaluation. + Pkt4Ptr pkt4(new Pkt4(DHCPDISCOVER, 123)); + EXPECT_FALSE(evaluateBool(*match_expr, *pkt4)); + + // Now add the option so it will pass. + OptionPtr opt(new OptionString(Option::V4, 100, "works right")); + pkt4->addOption(opt); + EXPECT_TRUE(evaluateBool(*match_expr, *pkt4)); +} + +// Verifies you can create a class with a name and options, +// but no expression. +// @todo same with AF_INET6 +TEST_F(ClientClassDefParserTest, nameAndOptionsClass) { + + std::string cfg_text = + "{ \n" + " \"name\": \"MICROSOFT\", \n" + " \"option-data\": [ \n" + " { \n" + " \"name\": \"domain-name-servers\", \n" + " \"code\": 6, \n" + " \"space\": \"dhcp4\", \n" + " \"csv-format\": true, \n" + " \"data\": \"192.0.2.1, 192.0.2.2\" \n" + " } \n" + " ] \n" + "} \n"; + + ClientClassDefPtr cclass; + ASSERT_NO_THROW(cclass = parseClientClassDef(cfg_text, AF_INET)); + + // We should find our class. + ASSERT_TRUE(cclass); + EXPECT_EQ("MICROSOFT", cclass->getName()); + + // Our one option should exist. + OptionDescriptor od = cclass->getCfgOption()->get(DHCP4_OPTION_SPACE, 6); + ASSERT_TRUE(od.option_); + EXPECT_EQ(6, od.option_->getType()); + + // Verify we have no expression + ASSERT_FALSE(cclass->getMatchExpr()); +} + + +// Verifies you can create a class with a name, expression, +// and options. +// @todo same with AF_INET6 +TEST_F(ClientClassDefParserTest, basicValidClass) { + + std::string test = "option[100].text == 'booya'"; + std::string cfg_text = + "{ \n" + " \"name\": \"MICROSOFT\", \n" + " \"test\": \"" + test + "\", \n" + " \"option-data\": [ \n" + " { \n" + " \"name\": \"domain-name-servers\", \n" + " \"code\": 6, \n" + " \"space\": \"dhcp4\", \n" + " \"csv-format\": true, \n" + " \"data\": \"192.0.2.1, 192.0.2.2\" \n" + " } \n" + " ] \n" + "} \n"; + + ClientClassDefPtr cclass; + ASSERT_NO_THROW(cclass = parseClientClassDef(cfg_text, AF_INET)); + + // We should find our class. + ASSERT_TRUE(cclass); + EXPECT_EQ("MICROSOFT", cclass->getName()); + + // Our one option should exist. + OptionDescriptor od = cclass->getCfgOption()->get(DHCP4_OPTION_SPACE, 6); + ASSERT_TRUE(od.option_); + EXPECT_EQ(6, od.option_->getType()); + + // Verify we can retrieve the expression + ExpressionPtr match_expr = cclass->getMatchExpr(); + ASSERT_TRUE(match_expr); + + // Verify the original expression was saved. + EXPECT_EQ(test, cclass->getTest()); + + // Build a packet that will fail evaluation. + Pkt4Ptr pkt4(new Pkt4(DHCPDISCOVER, 123)); + EXPECT_FALSE(evaluateBool(*match_expr, *pkt4)); + + // Now add the option so it will pass. + OptionPtr opt(new OptionString(Option::V4, 100, "booya")); + pkt4->addOption(opt); + EXPECT_TRUE(evaluateBool(*match_expr, *pkt4)); +} + +// Verifies that a class with no name, fails to parse. +TEST_F(ClientClassDefParserTest, noClassName) { + + std::string cfg_text = + "{ \n" + " \"test\": \"option[123].text == 'abc'\", \n" + " \"option-data\": [ \n" + " { \n" + " \"name\": \"domain-name-servers\", \n" + " \"code\": 6, \n" + " \"space\": \"dhcp4\", \n" + " \"csv-format\": true, \n" + " \"data\": \"192.0.2.1, 192.0.2.2\" \n" + " } \n" + " ] \n" + "} \n"; + + ClientClassDefPtr cclass; + ASSERT_THROW(cclass = parseClientClassDef(cfg_text, AF_INET), + DhcpConfigError); +} + +// Verifies that a class with a blank name, fails to parse. +TEST_F(ClientClassDefParserTest, blankClassName) { + + std::string cfg_text = + "{ \n" + " \"name\": \"\", \n" + " \"test\": \"option[123].text == 'abc'\", \n" + " \"option-data\": [ \n" + " { \n" + " \"name\": \"domain-name-servers\", \n" + " \"code\": 6, \n" + " \"space\": \"dhcp4\", \n" + " \"csv-format\": true, \n" + " \"data\": \"192.0.2.1, 192.0.2.2\" \n" + " } \n" + " ] \n" + "} \n"; + + ClientClassDefPtr cclass; + ASSERT_THROW(cclass = parseClientClassDef(cfg_text, AF_INET), + DhcpConfigError); +} + +// Verifies that a class with an invalid expression, fails to parse. +TEST_F(ClientClassDefParserTest, invalidExpression) { + std::string cfg_text = + "{ \n" + " \"name\": \"one\", \n" + " \"test\": 777 \n" + "} \n"; + + ClientClassDefPtr cclass; + ASSERT_THROW(cclass = parseClientClassDef(cfg_text, AF_INET6), + DhcpConfigError); +} + +// Verifies that a class with invalid option-def, fails to parse. +TEST_F(ClientClassDefParserTest, invalidOptionDef) { + std::string cfg_text = + "{ \n" + " \"name\": \"one\", \n" + " \"option-def\": [ \n" + " { \"bogus\": \"bad\" } \n" + " ] \n" + "} \n"; + + ClientClassDefPtr cclass; + ASSERT_THROW(cclass = parseClientClassDef(cfg_text, AF_INET), + DhcpConfigError); +} + +// Verifies that a class with invalid option-data, fails to parse. +TEST_F(ClientClassDefParserTest, invalidOptionData) { + std::string cfg_text = + "{ \n" + " \"name\": \"one\", \n" + " \"option-data\": [ \n" + " { \"bogus\": \"bad\" } \n" + " ] \n" + "} \n"; + + ClientClassDefPtr cclass; + ASSERT_THROW(cclass = parseClientClassDef(cfg_text, AF_INET), + DhcpConfigError); +} + + +// Verifies that a valid list of client classes will parse. +TEST_F(ClientClassDefListParserTest, simpleValidList) { + std::string cfg_text = + "[ \n" + " { \n" + " \"name\": \"one\" \n" + " }, \n" + " { \n" + " \"name\": \"two\" \n" + " }, \n" + " { \n" + " \"name\": \"three\" \n" + " } \n" + "] \n"; + + // Parsing the list should succeed. + ClientClassDictionaryPtr dictionary; + ASSERT_NO_THROW(dictionary = parseClientClassDefList(cfg_text, AF_INET6)); + ASSERT_TRUE(dictionary); + + // We should have three classes in the dictionary. + EXPECT_EQ(3, dictionary->getClasses()->size()); + + // Make sure we can find all three. + ClientClassDefPtr cclass; + ASSERT_NO_THROW(cclass = dictionary->findClass("one")); + ASSERT_TRUE(cclass); + EXPECT_EQ("one", cclass->getName()); + + ASSERT_NO_THROW(cclass = dictionary->findClass("two")); + ASSERT_TRUE(cclass); + EXPECT_EQ("two", cclass->getName()); + + ASSERT_NO_THROW(cclass = dictionary->findClass("three")); + ASSERT_TRUE(cclass); + EXPECT_EQ("three", cclass->getName()); + + // For good measure, make sure we can't find a non-existent class. + ASSERT_NO_THROW(cclass = dictionary->findClass("bogus")); + EXPECT_FALSE(cclass); +} + +// Verifies that class list containing a duplicate class entries, fails +// to parse. +TEST_F(ClientClassDefListParserTest, duplicateClass) { + std::string cfg_text = + "[ \n" + " { \n" + " \"name\": \"one\" \n" + " }, \n" + " { \n" + " \"name\": \"two\" \n" + " }, \n" + " { \n" + " \"name\": \"two\" \n" + " } \n" + "] \n"; + + ClientClassDictionaryPtr dictionary; + ASSERT_THROW(dictionary = parseClientClassDefList(cfg_text, AF_INET), + DhcpConfigError); +} + +// Test verifies that without any class specified, the fixed fields have their +// default, empty value. +// @todo same with AF_INET6 +TEST_F(ClientClassDefParserTest, noFixedFields) { + + std::string cfg_text = + "{ \n" + " \"name\": \"MICROSOFT\", \n" + " \"option-data\": [ \n" + " { \n" + " \"name\": \"domain-name-servers\", \n" + " \"data\": \"192.0.2.1, 192.0.2.2\" \n" + " } \n" + " ] \n" + "} \n"; + + ClientClassDefPtr cclass; + ASSERT_NO_THROW(cclass = parseClientClassDef(cfg_text, AF_INET)); + + // We should find our class. + ASSERT_TRUE(cclass); + + // And it should not have any fixed fields set + EXPECT_EQ(IOAddress("0.0.0.0"), cclass->getNextServer()); + EXPECT_EQ(0, cclass->getSname().size()); + EXPECT_EQ(0, cclass->getFilename().size()); + + // Nor option definitions + CfgOptionDefPtr cfg = cclass->getCfgOptionDef(); + ASSERT_TRUE(cfg->getAll(DHCP4_OPTION_SPACE)->empty()); +} + +// Test verifies option-def for a bad option fails to parse. +TEST_F(ClientClassDefParserTest, badOptionDef) { + std::string cfg_text = + "{ \n" + " \"name\": \"MICROSOFT\", \n" + " \"option-def\": [ \n" + " { \n" + " \"name\": \"foo\", \n" + " \"code\": 222, \n" + " \"type\": \"uint32\" \n" + " } \n" + " ] \n" + "} \n"; + + ClientClassDefPtr cclass; + ASSERT_THROW(cclass = parseClientClassDef(cfg_text, AF_INET), + DhcpConfigError); +} + +// Test verifies option-def works for private options (224-254). +TEST_F(ClientClassDefParserTest, privateOptionDef) { + std::string cfg_text = + "{ \n" + " \"name\": \"MICROSOFT\", \n" + " \"option-def\": [ \n" + " { \n" + " \"name\": \"foo\", \n" + " \"code\": 232, \n" + " \"type\": \"uint32\" \n" + " } \n" + " ] \n" + "} \n"; + + ClientClassDefPtr cclass; + ASSERT_NO_THROW(cclass = parseClientClassDef(cfg_text, AF_INET)); + + // We should find our class. + ASSERT_TRUE(cclass); + + // And the option definition. + CfgOptionDefPtr cfg = cclass->getCfgOptionDef(); + ASSERT_TRUE(cfg); + EXPECT_TRUE(cfg->get(DHCP4_OPTION_SPACE, 232)); + EXPECT_FALSE(cfg->get(DHCP6_OPTION_SPACE, 232)); + EXPECT_FALSE(cfg->get(DHCP4_OPTION_SPACE, 233)); +} + +// Test verifies option-def works for option 43. +TEST_F(ClientClassDefParserTest, option43Def) { + std::string cfg_text = + "{ \n" + " \"name\": \"MICROSOFT\", \n" + " \"test\": \"option[60].text == 'MICROSOFT'\", \n" + " \"option-def\": [ \n" + " { \n" + " \"name\": \"vendor-encapsulated-options\", \n" + " \"code\": 43, \n" + " \"space\": \"dhcp4\", \n" + " \"type\": \"empty\", \n" + " \"encapsulate\": \"vsi\" \n" + " } \n" + " ], \n" + " \"option-data\": [ \n" + " { \n" + " \"name\": \"vendor-encapsulated-options\" \n" + " }, \n" + " { \n" + " \"code\": 1, \n" + " \"space\": \"vsi\", \n" + " \"csv-format\": false, \n" + " \"data\": \"C0000200\" \n" + " } \n" + " ] \n" + "} \n"; + + ClientClassDefPtr cclass; + ASSERT_NO_THROW(cclass = parseClientClassDef(cfg_text, AF_INET)); + + // We should find our class. + ASSERT_TRUE(cclass); + + // And the option definition. + CfgOptionDefPtr cfg_def = cclass->getCfgOptionDef(); + ASSERT_TRUE(cfg_def); + EXPECT_TRUE(cfg_def->get(DHCP4_OPTION_SPACE, 43)); + + // Verify the option data. + OptionDescriptor od = cclass->getCfgOption()->get(DHCP4_OPTION_SPACE, 43); + ASSERT_TRUE(od.option_); + EXPECT_EQ(43, od.option_->getType()); + const OptionCollection& oc = od.option_->getOptions(); + ASSERT_EQ(1, oc.size()); + OptionPtr opt = od.option_->getOption(1); + ASSERT_TRUE(opt); + EXPECT_EQ(1, opt->getType()); + ASSERT_EQ(4, opt->getData().size()); + const uint8_t expected[4] = { 0xc0, 0x00, 0x02, 0x00 }; + EXPECT_EQ(0, std::memcmp(expected, &opt->getData()[0], 4)); +} + + +// Test verifies that it is possible to define next-server field and it +// is actually set in the class properly. +TEST_F(ClientClassDefParserTest, nextServer) { + + std::string cfg_text = + "{ \n" + " \"name\": \"MICROSOFT\", \n" + " \"next-server\": \"192.0.2.254\",\n" + " \"option-data\": [ \n" + " { \n" + " \"name\": \"domain-name-servers\", \n" + " \"data\": \"192.0.2.1, 192.0.2.2\" \n" + " } \n" + " ] \n" + "} \n"; + + ClientClassDefPtr cclass; + ASSERT_NO_THROW(cclass = parseClientClassDef(cfg_text, AF_INET)); + + // We should find our class. + ASSERT_TRUE(cclass); + + // And it should have next-server set, but everything else not set. + EXPECT_EQ(IOAddress("192.0.2.254"), cclass->getNextServer()); + EXPECT_EQ(0, cclass->getSname().size()); + EXPECT_EQ(0, cclass->getFilename().size()); +} + +// Test verifies that the parser rejects bogus next-server value. +TEST_F(ClientClassDefParserTest, nextServerBogus) { + + std::string bogus_v6 = + "{ \n" + " \"name\": \"MICROSOFT\", \n" + " \"next-server\": \"2001:db8::1\",\n" + " \"option-data\": [ \n" + " { \n" + " \"name\": \"domain-name-servers\", \n" + " \"data\": \"192.0.2.1, 192.0.2.2\" \n" + " } \n" + " ] \n" + "} \n"; + std::string bogus_junk = + "{ \n" + " \"name\": \"MICROSOFT\", \n" + " \"next-server\": \"not-an-address\",\n" + " \"option-data\": [ \n" + " { \n" + " \"name\": \"domain-name-servers\", \n" + " \"data\": \"192.0.2.1, 192.0.2.2\" \n" + " } \n" + " ] \n" + "} \n"; + std::string bogus_broadcast = + "{ \n" + " \"name\": \"MICROSOFT\", \n" + " \"next-server\": \"255.255.255.255\",\n" + " \"option-data\": [ \n" + " { \n" + " \"name\": \"domain-name-servers\", \n" + " \"data\": \"192.0.2.1, 192.0.2.2\" \n" + " } \n" + " ] \n" + "} \n"; + + EXPECT_THROW(parseClientClassDef(bogus_v6, AF_INET), DhcpConfigError); + EXPECT_THROW(parseClientClassDef(bogus_junk, AF_INET), DhcpConfigError); + EXPECT_THROW(parseClientClassDef(bogus_broadcast, AF_INET), DhcpConfigError); +} + +// Test verifies that it is possible to define server-hostname field and it +// is actually set in the class properly. +TEST_F(ClientClassDefParserTest, serverName) { + + std::string cfg_text = + "{ \n" + " \"name\": \"MICROSOFT\", \n" + " \"server-hostname\": \"hal9000\",\n" + " \"option-data\": [ \n" + " { \n" + " \"name\": \"domain-name-servers\", \n" + " \"data\": \"192.0.2.1, 192.0.2.2\" \n" + " } \n" + " ] \n" + "} \n"; + + ClientClassDefPtr cclass; + ASSERT_NO_THROW(cclass = parseClientClassDef(cfg_text, AF_INET)); + + // We should find our class. + ASSERT_TRUE(cclass); + + // And it should not have any fixed fields set + std::string exp_sname("hal9000"); + + EXPECT_EQ(exp_sname, cclass->getSname()); +} + +// Test verifies that the parser rejects bogus server-hostname value. +TEST_F(ClientClassDefParserTest, serverNameInvalid) { + + std::string cfg_too_long = + "{ \n" + " \"name\": \"MICROSOFT\", \n" + " \"server-hostname\": \"1234567890123456789012345678901234567890" + "1234567890123456789012345\", \n" + " \"option-data\": [ \n" + " { \n" + " \"name\": \"domain-name-servers\", \n" + " \"data\": \"192.0.2.1, 192.0.2.2\" \n" + " } \n" + " ] \n" + "} \n"; + + EXPECT_THROW(parseClientClassDef(cfg_too_long, AF_INET), DhcpConfigError); +} + + +// Test verifies that it is possible to define boot-file-name field and it +// is actually set in the class properly. +TEST_F(ClientClassDefParserTest, filename) { + + std::string cfg_text = + "{ \n" + " \"name\": \"MICROSOFT\", \n" + " \"boot-file-name\": \"ipxe.efi\", \n" + " \"option-data\": [ \n" + " { \n" + " \"name\": \"domain-name-servers\", \n" + " \"data\": \"192.0.2.1, 192.0.2.2\" \n" + " } \n" + " ] \n" + "} \n"; + + ClientClassDefPtr cclass; + ASSERT_NO_THROW(cclass = parseClientClassDef(cfg_text, AF_INET)); + + // We should find our class. + ASSERT_TRUE(cclass); + + // And it should not have any fixed fields set + std::string exp_filename("ipxe.efi"); + EXPECT_EQ(exp_filename, cclass->getFilename()); +} + +// Test verifies that the parser rejects bogus boot-file-name value. +TEST_F(ClientClassDefParserTest, filenameBogus) { + + // boot-file-name is allowed up to 128 bytes, this one is 129. + std::string cfg_too_long = + "{ \n" + " \"name\": \"MICROSOFT\", \n" + " \"boot-file-name\": \"1234567890123456789012345678901234567890" + "1234567890123456789012345678901234567890" + "1234567890123456789012345678901234567890" + "1234567890123456789012345678901234567890" + "1234567890123456789012345678901234567890" + "1234567890123456789012345678901234567890" + "123456789\", \n" + " \"option-data\": [ \n" + " { \n" + " \"name\": \"domain-name-servers\", \n" + " \"data\": \"192.0.2.1, 192.0.2.2\" \n" + " } \n" + " ] \n" + "} \n"; + + EXPECT_THROW(parseClientClassDef(cfg_too_long, AF_INET), DhcpConfigError); +} + +// Verifies that backward and built-in dependencies will parse. +TEST_F(ClientClassDefListParserTest, dependentList) { + std::string cfg_text = + "[ \n" + " { \n" + " \"name\": \"one\", \n" + " \"test\": \"member('VENDOR_CLASS_foo')\" \n" + " }, \n" + " { \n" + " \"name\": \"two\" \n" + " }, \n" + " { \n" + " \"name\": \"three\", \n" + " \"test\": \"member('two')\" \n" + " } \n" + "] \n"; + + // Parsing the list should succeed. + ClientClassDictionaryPtr dictionary; + ASSERT_NO_THROW(dictionary = parseClientClassDefList(cfg_text, AF_INET)); + ASSERT_TRUE(dictionary); + + // We should have three classes in the dictionary. + EXPECT_EQ(3, dictionary->getClasses()->size()); + + // Make sure we can find all three. + ClientClassDefPtr cclass; + ASSERT_NO_THROW(cclass = dictionary->findClass("one")); + ASSERT_TRUE(cclass); + EXPECT_EQ("one", cclass->getName()); + + ASSERT_NO_THROW(cclass = dictionary->findClass("two")); + ASSERT_TRUE(cclass); + EXPECT_EQ("two", cclass->getName()); + + ASSERT_NO_THROW(cclass = dictionary->findClass("three")); + ASSERT_TRUE(cclass); + EXPECT_EQ("three", cclass->getName()); +} + +// Verifies that not defined dependencies will not parse. +TEST_F(ClientClassDefListParserTest, dependentNotDefined) { + std::string cfg_text = + "[ \n" + " { \n" + " \"name\": \"one\", \n" + " \"test\": \"member('foo')\" \n" + " } \n" + "] \n"; + + EXPECT_THROW(parseClientClassDefList(cfg_text, AF_INET6), DhcpConfigError); +} + +// Verifies that error is not reported when a class references another +// not defined class, but dependency checking is disabled. +TEST_F(ClientClassDefListParserTest, dependencyCheckingDisabled) { + std::string cfg_text = + "[ \n" + " { \n" + " \"name\": \"one\", \n" + " \"test\": \"member('foo')\" \n" + " } \n" + "] \n"; + try { + parseClientClassDefList(cfg_text, AF_INET6, false); + } catch ( const std::exception& ex) { + std::cout << ex.what() << std::endl; + } + EXPECT_NO_THROW(parseClientClassDefList(cfg_text, AF_INET6, false)); +} + +// Verifies that forward dependencies will not parse. +TEST_F(ClientClassDefListParserTest, dependentForwardError) { + std::string cfg_text = + "[ \n" + " { \n" + " \"name\": \"one\", \n" + " \"test\": \"member('foo')\" \n" + " }, \n" + " { \n" + " \"name\": \"foo\" \n" + " } \n" + "] \n"; + + EXPECT_THROW(parseClientClassDefList(cfg_text, AF_INET6), DhcpConfigError); +} + +// Verifies that backward dependencies will parse. +TEST_F(ClientClassDefListParserTest, dependentBackward) { + std::string cfg_text = + "[ \n" + " { \n" + " \"name\": \"foo\" \n" + " }, \n" + " { \n" + " \"name\": \"one\", \n" + " \"test\": \"member('foo')\" \n" + " } \n" + "] \n"; + + EXPECT_NO_THROW(parseClientClassDefList(cfg_text, AF_INET6)); +} + +// Verifies that the depend on known flag is correctly handled. +TEST_F(ClientClassDefListParserTest, dependOnKnown) { + std::string cfg_text = + "[ \n" + " { \n" + " \"name\": \"alpha\", \n" + " \"test\": \"member('ALL')\" \n" + " }, \n" + " { \n" + " \"name\": \"beta\", \n" + " \"test\": \"member('alpha')\" \n" + " }, \n" + " { \n" + " \"name\": \"gamma\", \n" + " \"test\": \"member('KNOWN') and member('alpha')\" \n" + " }, \n" + " { \n" + " \"name\": \"delta\", \n" + " \"test\": \"member('beta') and member('gamma')\" \n" + " }, \n" + " { \n" + " \"name\": \"zeta\", \n" + " \"test\": \"not member('UNKNOWN') and member('alpha')\" \n" + " } \n" + "] \n"; + + // Parsing the list should succeed. + ClientClassDictionaryPtr dictionary; + EXPECT_NO_THROW(dictionary = parseClientClassDefList(cfg_text, AF_INET6)); + ASSERT_TRUE(dictionary); + + // We should have five classes in the dictionary. + EXPECT_EQ(5, dictionary->getClasses()->size()); + + // Check alpha. + ClientClassDefPtr cclass; + ASSERT_NO_THROW(cclass = dictionary->findClass("alpha")); + ASSERT_TRUE(cclass); + EXPECT_EQ("alpha", cclass->getName()); + EXPECT_FALSE(cclass->getDependOnKnown()); + + // Check beta. + ASSERT_NO_THROW(cclass = dictionary->findClass("beta")); + ASSERT_TRUE(cclass); + EXPECT_EQ("beta", cclass->getName()); + EXPECT_FALSE(cclass->getDependOnKnown()); + + // Check gamma which directly depends on KNOWN. + ASSERT_NO_THROW(cclass = dictionary->findClass("gamma")); + ASSERT_TRUE(cclass); + EXPECT_EQ("gamma", cclass->getName()); + EXPECT_TRUE(cclass->getDependOnKnown()); + + // Check delta which indirectly depends on KNOWN. + ASSERT_NO_THROW(cclass = dictionary->findClass("delta")); + ASSERT_TRUE(cclass); + EXPECT_EQ("delta", cclass->getName()); + EXPECT_TRUE(cclass->getDependOnKnown()); + + // Check that zeta which directly depends on UNKNOWN. + // (and yes I know that I skipped epsilon) + ASSERT_NO_THROW(cclass = dictionary->findClass("zeta")); + ASSERT_TRUE(cclass); + EXPECT_EQ("zeta", cclass->getName()); + EXPECT_TRUE(cclass->getDependOnKnown()); +} + +// Verifies that a built-in class can't be required or evaluated. +TEST_F(ClientClassDefListParserTest, builtinCheckError) { + std::string cfg_text = + "[ \n" + " { \n" + " \"name\": \"ALL\" \n" + " } \n" + "] \n"; + + EXPECT_NO_THROW(parseClientClassDefList(cfg_text, AF_INET6)); + + cfg_text = + "[ \n" + " { \n" + " \"name\": \"ALL\", \n" + " \"only-if-required\": true \n" + " } \n" + "] \n"; + + EXPECT_THROW(parseClientClassDefList(cfg_text, AF_INET), DhcpConfigError); + + cfg_text = + "[ \n" + " { \n" + " \"name\": \"ALL\", \n" + " \"test\": \"'aa' == 'aa'\" \n" + " } \n" + "] \n"; + + EXPECT_THROW(parseClientClassDefList(cfg_text, AF_INET6), DhcpConfigError); + + cfg_text = + "[ \n" + " { \n" + " \"name\": \"KNOWN\", \n" + " \"only-if-required\": true \n" + " } \n" + "] \n"; + + EXPECT_THROW(parseClientClassDefList(cfg_text, AF_INET), DhcpConfigError); + + cfg_text = + "[ \n" + " { \n" + " \"name\": \"KNOWN\", \n" + " \"test\": \"'aa' == 'aa'\" \n" + " } \n" + "] \n"; + + EXPECT_THROW(parseClientClassDefList(cfg_text, AF_INET6), DhcpConfigError); + + cfg_text = + "[ \n" + " { \n" + " \"name\": \"UNKNOWN\", \n" + " \"only-if-required\": true \n" + " } \n" + "] \n"; + + EXPECT_THROW(parseClientClassDefList(cfg_text, AF_INET), DhcpConfigError); + + cfg_text = + "[ \n" + " { \n" + " \"name\": \"UNKNOWN\", \n" + " \"test\": \"'aa' == 'aa'\" \n" + " } \n" + "] \n"; + + EXPECT_THROW(parseClientClassDefList(cfg_text, AF_INET6), DhcpConfigError); +} + +// Verifies that the special DROP class can't be required. +TEST_F(ClientClassDefListParserTest, dropCheckError) { + std::string cfg_text = + "[ \n" + " { \n" + " \"name\": \"DROP\", \n" + " \"test\": \"option[123].text == 'abc'\" \n" + " } \n" + "] \n"; + + EXPECT_NO_THROW(parseClientClassDefList(cfg_text, AF_INET6)); + + cfg_text = + "[ \n" + " { \n" + " \"name\": \"DROP\", \n" + " \"only-if-required\": true \n" + " } \n" + "] \n"; + + EXPECT_THROW(parseClientClassDefList(cfg_text, AF_INET), DhcpConfigError); + + // This constraint was relaxed in #1815. + cfg_text = + "[ \n" + " { \n" + " \"name\": \"DROP\", \n" + " \"test\": \"member('KNOWN')\" \n" + " } \n" + "] \n"; + + EXPECT_NO_THROW(parseClientClassDefList(cfg_text, AF_INET6)); +} + +// Verify the ability to configure valid lifetime triplet. +TEST_F(ClientClassDefParserTest, validLifetimeTests) { + + struct Scenario { + std::string desc_; + std::string cfg_txt_; + Triplet<uint32_t> exp_triplet_; + }; + + std::vector<Scenario> scenarios = { + { + "unspecified", + "", + Triplet<uint32_t>() + }, + { + "valid only", + "\"valid-lifetime\": 100", + Triplet<uint32_t>(100) + }, + { + "min only", + "\"min-valid-lifetime\": 50", + Triplet<uint32_t>(50, 50, 50) + }, + { + "max only", + "\"max-valid-lifetime\": 75", + Triplet<uint32_t>(75, 75, 75) + }, + { + "all three", + "\"min-valid-lifetime\": 25, \"valid-lifetime\": 50, \"max-valid-lifetime\": 75", + Triplet<uint32_t>(25, 50, 75) + } + }; + + for (auto scenario : scenarios) { + SCOPED_TRACE(scenario.desc_); { + std::stringstream oss; + oss << "{ \"name\": \"foo\""; + if (!scenario.cfg_txt_.empty()) { + oss << ",\n" << scenario.cfg_txt_; + } + oss << "\n}\n"; + + ClientClassDefPtr class_def; + ASSERT_NO_THROW_LOG(class_def = parseClientClassDef(oss.str(), AF_INET)); + ASSERT_TRUE(class_def); + if (scenario.exp_triplet_.unspecified()) { + EXPECT_TRUE(class_def->getValid().unspecified()); + } else { + EXPECT_EQ(class_def->getValid(), scenario.exp_triplet_); + EXPECT_EQ(class_def->getValid().getMin(), scenario.exp_triplet_.getMin()); + EXPECT_EQ(class_def->getValid().get(), scenario.exp_triplet_.get()); + EXPECT_EQ(class_def->getValid().getMax(), scenario.exp_triplet_.getMax()); + } + } + } +} + +// Verify the ability to configure lease preferred lifetime triplet. +TEST_F(ClientClassDefParserTest, preferredLifetimeTests) { + + struct Scenario { + std::string desc_; + std::string cfg_txt_; + Triplet<uint32_t> exp_triplet_; + }; + + std::vector<Scenario> scenarios = { + { + "unspecified", + "", + Triplet<uint32_t>() + }, + { + "preferred only", + "\"preferred-lifetime\": 100", + Triplet<uint32_t>(100) + }, + { + "min only", + "\"min-preferred-lifetime\": 50", + Triplet<uint32_t>(50, 50, 50) + }, + { + "max only", + "\"max-preferred-lifetime\": 75", + Triplet<uint32_t>(75, 75, 75) + }, + { + "all three", + "\"min-preferred-lifetime\": 25," + "\"preferred-lifetime\": 50," + "\"max-preferred-lifetime\": 75", + Triplet<uint32_t>(25, 50, 75) + } + }; + + for (auto scenario : scenarios) { + SCOPED_TRACE(scenario.desc_); { + std::stringstream oss; + oss << "{ \"name\": \"foo\""; + if (!scenario.cfg_txt_.empty()) { + oss << ",\n" << scenario.cfg_txt_; + } + oss << "\n}\n"; + + ClientClassDefPtr class_def; + ASSERT_NO_THROW_LOG(class_def = parseClientClassDef(oss.str(), AF_INET6)); + ASSERT_TRUE(class_def); + if (scenario.exp_triplet_.unspecified()) { + EXPECT_TRUE(class_def->getPreferred().unspecified()); + } else { + EXPECT_EQ(class_def->getPreferred(), scenario.exp_triplet_); + EXPECT_EQ(class_def->getPreferred().getMin(), scenario.exp_triplet_.getMin()); + EXPECT_EQ(class_def->getPreferred().get(), scenario.exp_triplet_.get()); + EXPECT_EQ(class_def->getPreferred().getMax(), scenario.exp_triplet_.getMax()); + } + } + } +} + +// Verifies that an invalid user-context fails to parse. +TEST_F(ClientClassDefParserTest, invalidUserContext) { + std::string cfg_text = + "{ \n" + " \"name\": \"one\", \n" + " \"user-context\": \"i am not a map\" \n" + "} \n"; + + ClientClassDefPtr cclass; + ASSERT_THROW_MSG(cclass = parseClientClassDef(cfg_text, AF_INET), + DhcpConfigError, "User context has to be a map (<string>:3:20)"); +} + +} // end of anonymous namespace |