diff options
Diffstat (limited to 'src/lib/dhcpsrv/tests/cfg_option_unittest.cc')
-rw-r--r-- | src/lib/dhcpsrv/tests/cfg_option_unittest.cc | 1148 |
1 files changed, 1148 insertions, 0 deletions
diff --git a/src/lib/dhcpsrv/tests/cfg_option_unittest.cc b/src/lib/dhcpsrv/tests/cfg_option_unittest.cc new file mode 100644 index 0000000..2e1acb2 --- /dev/null +++ b/src/lib/dhcpsrv/tests/cfg_option_unittest.cc @@ -0,0 +1,1148 @@ +// Copyright (C) 2014-2021 Internet Systems Consortium, Inc. ("ISC") +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +#include <config.h> +#include <cc/data.h> +#include <dhcp/dhcp6.h> +#include <dhcp/option.h> +#include <dhcp/option_custom.h> +#include <dhcp/option_int.h> +#include <dhcp/option_int_array.h> +#include <dhcp/option_space.h> +#include <dhcp/option_string.h> +#include <dhcp/option4_addrlst.h> +#include <dhcpsrv/cfg_option.h> +#include <testutils/gtest_utils.h> +#include <testutils/test_to_element.h> +#include <boost/foreach.hpp> +#include <boost/pointer_cast.hpp> +#include <gtest/gtest.h> +#include <iterator> +#include <limits> +#include <list> +#include <sstream> + +using namespace isc; +using namespace isc::asiolink; +using namespace isc::data; +using namespace isc::dhcp; + +namespace { + +// This test verifies that the OptionDescriptor factory function creates a +// valid instance. +TEST(OptionDescriptorTest, create) { + OptionPtr option = Option::create(Option::V4, 234); + ElementPtr context = Element::createMap(); + context->set("name", Element::create("value")); + auto desc = OptionDescriptor::create(option, true, "value", context); + + ASSERT_TRUE(desc); + EXPECT_EQ(option, desc->option_); + EXPECT_TRUE(desc->persistent_); + EXPECT_EQ("value", desc->formatted_value_); + EXPECT_EQ(context, desc->getContext()); +} + +// This test verifies that the OptionDescriptor factory function variant +// taking persistent flag as an argument creates valid instance. +TEST(OptionDescriptorTest, createPersistent) { + auto desc = OptionDescriptor::create(true); + ASSERT_TRUE(desc); + + EXPECT_FALSE(desc->option_); + EXPECT_TRUE(desc->persistent_); + EXPECT_TRUE(desc->formatted_value_.empty()); + EXPECT_FALSE(desc->getContext()); +} + +// This test verifies that the OptionDescriptor factory function variant +// copying a descriptor provided as an argument creates valid instance. +TEST(OptionDescriptorTest, createCopy) { + OptionPtr option = Option::create(Option::V4, 234); + ElementPtr context = Element::createMap(); + context->set("name", Element::create("value")); + auto desc = OptionDescriptor::create(option, true, "value", context); + + auto desc_copy = OptionDescriptor::create(*desc); + ASSERT_TRUE(desc_copy); + + ASSERT_TRUE(desc_copy); + EXPECT_EQ(option, desc_copy->option_); + EXPECT_TRUE(desc_copy->persistent_); + EXPECT_EQ("value", desc_copy->formatted_value_); + EXPECT_EQ(context, desc_copy->getContext()); +} + +// This test verifies that the OptionDescriptor assignment operator +// does the shallow copy. +TEST(OptionDescriptorTest, assign) { + // Create a persistent option descriptor. + auto desc = OptionDescriptor::create(true); + ASSERT_TRUE(desc); + + // Create another option descriptor. + OptionPtr option = Option::create(Option::V4, 234); + ElementPtr context = Element::createMap(); + context->set("name", Element::create("value")); + auto desc1 = OptionDescriptor::create(option, true, "value", context); + ASSERT_TRUE(desc1); + + // Assign the option descriptor. + desc = desc1; + + // Check it. + ASSERT_TRUE(desc); + EXPECT_EQ(option, desc->option_); + EXPECT_TRUE(desc->persistent_); + EXPECT_EQ("value", desc->formatted_value_); + EXPECT_EQ(context, desc->getContext()); +} + +/// This class fixture for testing @c CfgOption class, holding option +/// configuration. +class CfgOptionTest : public ::testing::Test { +public: + + /// @brief Generates encapsulated options and adds them to CfgOption + /// + /// This method generates the following options: + /// - 1000-1019 options: uint16 with value 1234, encapsulate "foo" + /// - 1-19 options: uint8 with value 1, encapsulate "foo-subs" + /// - 1-9 options: uint8 with value 3 + /// - 1020-1039 options: uint16 with value 2345, encapsulate "bar" + /// - 100-119 options: uint8 with value 2, encapsulate "bar-subs" + /// - 501-509 options: uint8 with value 4 + void generateEncapsulatedOptions(CfgOption& cfg) { + // Create top-level options encapsulating "foo" option space. + for (uint16_t code = 1000; code < 1020; ++code) { + OptionUint16Ptr option = OptionUint16Ptr(new OptionUint16(Option::V6, + code, 1234)); + option->setEncapsulatedSpace("foo"); + + // In order to easier identify the options by id, let's use the option + // code as the id. + ASSERT_NO_THROW(cfg.add(option, false, DHCP6_OPTION_SPACE, + static_cast<uint64_t>(code))); + } + + // Create top level options encapsulating "bar" option space. + for (uint16_t code = 1020; code < 1040; ++code) { + OptionUint16Ptr option = OptionUint16Ptr(new OptionUint16(Option::V6, + code, 2345)); + option->setEncapsulatedSpace("bar"); + ASSERT_NO_THROW(cfg.add(option, false, DHCP6_OPTION_SPACE, + static_cast<uint64_t>(code))); + } + + // Create sub-options belonging to "foo" option space and encapsulating + // foo-subs option space. + for (uint16_t code = 1; code < 20; ++code) { + OptionUint8Ptr option = OptionUint8Ptr(new OptionUint8(Option::V6, code, + 0x01)); + option->setEncapsulatedSpace("foo-subs"); + ASSERT_NO_THROW(cfg.add(option, false, "foo", static_cast<uint64_t>(code))); + } + + // Create sub-options belonging to "bar" option space and encapsulating + // bar-subs option space. + for (uint16_t code = 100; code < 119; ++code) { + OptionUint8Ptr option = OptionUint8Ptr(new OptionUint8(Option::V6, + code, 0x02)); + option->setEncapsulatedSpace("bar-subs"); + ASSERT_NO_THROW(cfg.add(option, false, "bar", static_cast<uint64_t>(code))); + } + + // Create sub-options belonging to "foo-subs" option space. + for (uint16_t code = 1; code < 10; ++code) { + OptionUint8Ptr option = OptionUint8Ptr(new OptionUint8(Option::V6, code, + 0x03)); + ASSERT_NO_THROW(cfg.add(option, false, "foo-subs", + static_cast<uint64_t>(code))); + } + + // Create sub-options belonging to "bar-subs" option space. + for (uint16_t code = 501; code < 510; ++code) { + OptionUint8Ptr option = OptionUint8Ptr(new OptionUint8(Option::V6, + code, 0x04)); + ASSERT_NO_THROW(cfg.add(option, false, "bar-subs", + static_cast<uint64_t>(code))); + } + } +}; + +// This test verifies the empty predicate. +TEST_F(CfgOptionTest, empty) { + CfgOption cfg1; + CfgOption cfg2; + + // Initially the option configurations should be empty. + ASSERT_TRUE(cfg1.empty()); + ASSERT_TRUE(cfg2.empty()); + + // Add an option to each configuration + OptionPtr option(new Option(Option::V6, 1)); + ASSERT_NO_THROW(cfg1.add(option, false, DHCP6_OPTION_SPACE)); + ASSERT_NO_THROW(cfg2.add(option, true, "isc")); + + // The first option configuration has an option + ASSERT_FALSE(cfg1.empty()); + + // The second option configuration has a vendor option + ASSERT_FALSE(cfg2.empty()); +} + +// This test verifies that the option configurations can be compared. +TEST_F(CfgOptionTest, equals) { + CfgOption cfg1; + CfgOption cfg2; + + // Initially the configurations should be equal. + ASSERT_TRUE(cfg1 == cfg2); + ASSERT_FALSE(cfg1 != cfg2); + + // Add 9 options to two different option spaces. Each option have different + // option code and content. + for (uint16_t code = 1; code < 10; ++code) { + OptionPtr option(new Option(Option::V6, code, OptionBuffer(10, code))); + ASSERT_NO_THROW(cfg1.add(option, false, "isc")); + ASSERT_NO_THROW(cfg1.add(option, true, "vendor-123")); + } + + // Configurations should now be different. + ASSERT_FALSE(cfg1 == cfg2); + ASSERT_TRUE(cfg1 != cfg2); + + // Add 8 options (excluding the option with code 1) to the same option + // spaces. + for (uint16_t code = 2; code < 10; ++code) { + OptionPtr option(new Option(Option::V6, code, OptionBuffer(10, code))); + ASSERT_NO_THROW(cfg2.add(option, false, "isc")); + ASSERT_NO_THROW(cfg2.add(option, true, "vendor-123")); + } + + // Configurations should still be unequal. + ASSERT_FALSE(cfg1 == cfg2); + ASSERT_TRUE(cfg1 != cfg2); + + // Add missing option to the option space isc. + ASSERT_NO_THROW(cfg2.add(OptionPtr(new Option(Option::V6, 1, + OptionBuffer(10, 0x01))), + false, "isc")); + // Configurations should still be unequal because option with code 1 + // is missing in the option space vendor-123. + ASSERT_FALSE(cfg1 == cfg2); + ASSERT_TRUE(cfg1 != cfg2); + + // Add missing option. + ASSERT_NO_THROW(cfg2.add(OptionPtr(new Option(Option::V6, 1, + OptionBuffer(10, 0x01))), + true, "vendor-123")); + // Configurations should now be equal. + ASSERT_TRUE(cfg1 == cfg2); + ASSERT_FALSE(cfg1 != cfg2); + +} + +// This test verifies that multiple options can be added to the configuration +// and that they can be retrieved using the option space name. +TEST_F(CfgOptionTest, add) { + CfgOption cfg; + + // Differentiate options by their codes (100-109) + for (uint16_t code = 100; code < 110; ++code) { + OptionPtr option(new Option(Option::V6, code, OptionBuffer(10, 0xFF))); + ASSERT_NO_THROW(cfg.add(option, false, DHCP6_OPTION_SPACE)); + } + + // Add 7 options to another option space. The option codes partially overlap + // with option codes that we have added to dhcp6 option space. + for (uint16_t code = 105; code < 112; ++code) { + OptionPtr option(new Option(Option::V6, code, OptionBuffer(10, 0xFF))); + ASSERT_NO_THROW(cfg.add(option, false, "isc")); + } + + // Get options from the Subnet and check if all 10 are there. + OptionContainerPtr options = cfg.getAll(DHCP6_OPTION_SPACE); + ASSERT_TRUE(options); + ASSERT_EQ(10, options->size()); + + // Validate codes of options added to dhcp6 option space. + uint16_t expected_code = 100; + for (OptionContainer::const_iterator option_desc = options->begin(); + option_desc != options->end(); ++option_desc) { + ASSERT_TRUE(option_desc->option_); + EXPECT_EQ(expected_code, option_desc->option_->getType()); + ++expected_code; + } + + options = cfg.getAll("isc"); + ASSERT_TRUE(options); + ASSERT_EQ(7, options->size()); + + // Validate codes of options added to isc option space. + expected_code = 105; + for (OptionContainer::const_iterator option_desc = options->begin(); + option_desc != options->end(); ++option_desc) { + ASSERT_TRUE(option_desc->option_); + EXPECT_EQ(expected_code, option_desc->option_->getType()); + ++expected_code; + } + + // Try to get options from a non-existing option space. + options = cfg.getAll("abcd"); + ASSERT_TRUE(options); + EXPECT_TRUE(options->empty()); +} + +// This test verifies that options can be replaced with updated content. +TEST_F(CfgOptionTest, replace) { + CfgOption cfg; + + // Let's add some options to the config to the config. + OptionStringPtr option(new OptionString(Option::V6, 1, "one")); + ASSERT_NO_THROW(cfg.add(option, false, "isc")); + + option.reset(new OptionString(Option::V6, 2, "two")); + ASSERT_NO_THROW(cfg.add(option, false, "isc")); + + option.reset(new OptionString(Option::V6, 3, "three")); + ASSERT_NO_THROW(cfg.add(option, false, "isc")); + + // Now let's make sure we can find them and they are as expected. + OptionDescriptor desc = cfg.get("isc", 1); + ASSERT_TRUE(desc.option_); + OptionStringPtr opstr = boost::dynamic_pointer_cast<OptionString>(desc.option_); + ASSERT_TRUE(opstr); + EXPECT_EQ("one", opstr->getValue()); + + desc = cfg.get("isc", 2); + ASSERT_TRUE(desc.option_); + opstr = boost::dynamic_pointer_cast<OptionString>(desc.option_); + ASSERT_TRUE(opstr); + EXPECT_EQ("two", opstr->getValue()); + + desc = cfg.get("isc", 3); + ASSERT_TRUE(desc.option_); + opstr = boost::dynamic_pointer_cast<OptionString>(desc.option_); + ASSERT_TRUE(opstr); + EXPECT_EQ("three", opstr->getValue()); + + // Now let's replace one and three. + desc.option_.reset(new OptionString(Option::V6, 1, "new one")); + ASSERT_NO_THROW(cfg.replace(desc, "isc")); + + desc.option_.reset(new OptionString(Option::V6, 3, "new three")); + ASSERT_NO_THROW(cfg.replace(desc, "isc")); + + // Now let's make sure we can find them again and they are as expected. + desc = cfg.get("isc", 1); + ASSERT_TRUE(desc.option_); + opstr = boost::dynamic_pointer_cast<OptionString>(desc.option_); + ASSERT_TRUE(opstr); + EXPECT_EQ("new one", opstr->getValue()); + + desc = cfg.get("isc", 2); + ASSERT_TRUE(desc.option_); + opstr = boost::dynamic_pointer_cast<OptionString>(desc.option_); + ASSERT_TRUE(opstr); + EXPECT_EQ("two", opstr->getValue()); + + desc = cfg.get("isc", 3); + ASSERT_TRUE(desc.option_); + opstr = boost::dynamic_pointer_cast<OptionString>(desc.option_); + ASSERT_TRUE(opstr); + EXPECT_EQ("new three", opstr->getValue()); +} + +// This test verifies that one configuration can be merged into another. +TEST_F(CfgOptionTest, mergeTo) { + CfgOption cfg_src; + CfgOption cfg_dst; + + // Create collection of options in option space dhcp6, with option codes + // from the range of 100 to 109 and holding one byte of data equal to 0xFF. + for (uint16_t code = 100; code < 110; ++code) { + OptionPtr option(new Option(Option::V6, code, OptionBuffer(1, 0xFF))); + ASSERT_NO_THROW(cfg_src.add(option, false, DHCP6_OPTION_SPACE)); + } + + // Create collection of options in vendor space 123, with option codes + // from the range of 100 to 109 and holding one byte of data equal to 0xFF. + for (uint16_t code = 100; code < 110; code += 2) { + OptionPtr option(new Option(Option::V6, code, OptionBuffer(1, 0xFF))); + ASSERT_NO_THROW(cfg_src.add(option, false, "vendor-123")); + } + + // Create destination configuration (configuration that we merge the + // other configuration to). + + // Create collection of options having even option codes in the range of + // 100 to 108. + for (uint16_t code = 100; code < 110; code += 2) { + OptionPtr option(new Option(Option::V6, code, OptionBuffer(1, 0x01))); + ASSERT_NO_THROW(cfg_dst.add(option, false, DHCP6_OPTION_SPACE)); + } + + // Create collection of options having odd option codes in the range of + // 101 to 109. + for (uint16_t code = 101; code < 110; code += 2) { + OptionPtr option(new Option(Option::V6, code, OptionBuffer(1, 0x01))); + ASSERT_NO_THROW(cfg_dst.add(option, false, "vendor-123")); + } + + // Merge source configuration to the destination configuration. The options + // in the destination should be preserved. The options from the source + // configuration should be added. + ASSERT_NO_THROW(cfg_src.mergeTo(cfg_dst)); + + // Validate the options in the dhcp6 option space in the destination. + for (uint16_t code = 100; code < 110; ++code) { + OptionDescriptor desc = cfg_dst.get(DHCP6_OPTION_SPACE, code); + ASSERT_TRUE(desc.option_); + ASSERT_EQ(1, desc.option_->getData().size()); + // The options with even option codes should hold one byte of data + // equal to 0x1. These are the ones that we have initially added to + // the destination configuration. The other options should hold the + // values of 0xFF which indicates that they have been merged from the + // source configuration. + if ((code % 2) == 0) { + EXPECT_EQ(0x01, desc.option_->getData()[0]); + } else { + EXPECT_EQ(0xFF, desc.option_->getData()[0]); + } + } + + // Validate the options in the vendor space. + for (uint16_t code = 100; code < 110; ++code) { + OptionDescriptor desc = cfg_dst.get(123, code); + ASSERT_TRUE(desc.option_); + ASSERT_EQ(1, desc.option_->getData().size()); + // This time, the options with even option codes should hold a byte + // of data equal to 0xFF. The other options should hold the byte of + // data equal to 0x01. + if ((code % 2) == 0) { + EXPECT_EQ(0xFF, desc.option_->getData()[0]); + } else { + EXPECT_EQ(0x01, desc.option_->getData()[0]); + } + } +} + +// This test verifies that the options configuration can be copied between +// objects. +TEST_F(CfgOptionTest, copy) { + CfgOption cfg_src; + // Add 10 options to the custom option space in the source configuration. + for (uint16_t code = 100; code < 110; ++code) { + OptionPtr option(new Option(Option::V6, code, OptionBuffer(1, 0x01))); + ASSERT_NO_THROW(cfg_src.add(option, false, "foo")); + } + + CfgOption cfg_dst; + // Add 20 options to the custom option space in destination configuration. + for (uint16_t code = 100; code < 120; ++code) { + OptionPtr option(new Option(Option::V6, code, OptionBuffer(1, 0xFF))); + ASSERT_NO_THROW(cfg_dst.add(option, false, "isc")); + } + + // Copy entire configuration to the destination. This should override any + // existing data. + ASSERT_NO_THROW(cfg_src.copyTo(cfg_dst)); + + // Validate options in the destination configuration. + for (uint16_t code = 100; code < 110; ++code) { + OptionDescriptor desc = cfg_dst.get("foo", code); + ASSERT_TRUE(desc.option_); + ASSERT_EQ(1, desc.option_->getData().size()); + EXPECT_EQ(0x01, desc.option_->getData()[0]); + } + + // Any existing options should be removed. + OptionContainerPtr container = cfg_dst.getAll("isc"); + ASSERT_TRUE(container); + EXPECT_TRUE(container->empty()); + + // The option space "foo" should contain exactly 10 options. + container = cfg_dst.getAll("foo"); + ASSERT_TRUE(container); + EXPECT_EQ(10, container->size()); +} + +// This test verifies that DHCP options from one configuration +// can be used to update options in another configuration. +// In other words, options from an "other" configuration +// can be merged into an existing configuration, with any +// duplicates in other overwriting those in the existing +// configuration. +TEST_F(CfgOptionTest, validMerge) { + CfgOption this_cfg; + CfgOption other_cfg; + + // We need to create a dictionary of definitions pass into option merge. + CfgOptionDefPtr defs(new CfgOptionDef()); + defs->add((OptionDefinitionPtr(new OptionDefinition("one", 1, "isc", "uint8")))); + defs->add((OptionDefinitionPtr(new OptionDefinition("two", 2, "isc", "uint8")))); + defs->add((OptionDefinitionPtr(new OptionDefinition("four", 4, "isc", "uint8")))); + defs->add((OptionDefinitionPtr(new OptionDefinition("three", 3, "fluff", "uint8")))); + defs->add((OptionDefinitionPtr(new OptionDefinition("four", 4, "fluff", "uint8")))); + + // Create our existing config, that gets merged into. + OptionPtr option(new Option(Option::V4, 1, OptionBuffer(1, 0x01))); + ASSERT_NO_THROW(this_cfg.add(option, false, "isc")); + + // Add option 3 to "fluff" + option.reset(new Option(Option::V4, 3, OptionBuffer(1, 0x03))); + ASSERT_NO_THROW(this_cfg.add(option, false, "fluff")); + + // Add option 4 to "fluff". + option.reset(new Option(Option::V4, 4, OptionBuffer(1, 0x04))); + ASSERT_NO_THROW(this_cfg.add(option, false, "fluff")); + + // Create our other config that will be merged from. + // Add Option 1 to "isc", this should "overwrite" the original. + option.reset(new Option(Option::V4, 1, OptionBuffer(1, 0x10))); + ASSERT_NO_THROW(other_cfg.add(option, false, "isc")); + + // Add option 2 to "isc". + option.reset(new Option(Option::V4, 2, OptionBuffer(1, 0x20))); + ASSERT_NO_THROW(other_cfg.add(option, false, "isc")); + + // Add option 4 to "isc". + option.reset(new Option(Option::V4, 4, OptionBuffer(1, 0x40))); + ASSERT_NO_THROW(other_cfg.add(option, false, "isc")); + + // Merge source configuration to the destination configuration. The options + // in the destination should be preserved. The options from the source + // configuration should be added. + ASSERT_NO_THROW(this_cfg.merge(defs, other_cfg)); + + // isc:1 should come from "other" config. + OptionDescriptor desc = this_cfg.get("isc", 1); + ASSERT_TRUE(desc.option_); + OptionUint8Ptr opint = boost::dynamic_pointer_cast<OptionUint8>(desc.option_); + ASSERT_TRUE(opint); + EXPECT_EQ(16, opint->getValue()); + + // isc:2 should come from "other" config. + desc = this_cfg.get("isc", 2); + ASSERT_TRUE(desc.option_); + opint = boost::dynamic_pointer_cast<OptionUint8>(desc.option_); + ASSERT_TRUE(opint); + EXPECT_EQ(32, opint->getValue()); + + // isc:4 should come from "other" config. + desc = this_cfg.get("isc", 4); + ASSERT_TRUE(desc.option_); + opint = boost::dynamic_pointer_cast<OptionUint8>(desc.option_); + ASSERT_TRUE(opint); + EXPECT_EQ(64, opint->getValue()); + + // fluff:3 should come from "this" config. + desc = this_cfg.get("fluff", 3); + ASSERT_TRUE(desc.option_); + opint = boost::dynamic_pointer_cast<OptionUint8>(desc.option_); + ASSERT_TRUE(opint); + EXPECT_EQ(3, opint->getValue()); + + // fluff:4 should come from "this" config. + desc = this_cfg.get("fluff", 4); + ASSERT_TRUE(desc.option_); + opint = boost::dynamic_pointer_cast<OptionUint8>(desc.option_); + ASSERT_TRUE(opint); + EXPECT_EQ(4, opint->getValue()); +} + +// This test verifies that attempting to merge options +// which are by incompatible with "known" option definitions +// are detected. +TEST_F(CfgOptionTest, mergeInvalid) { + CfgOption this_cfg; + CfgOption other_cfg; + + // Create an empty dictionary of defintions pass into option merge. + CfgOptionDefPtr defs(new CfgOptionDef()); + + // Create our other config that will be merged from. + // Add an option without a formatted value. + OptionPtr option(new Option(Option::V4, 1, OptionBuffer(1, 0x01))); + ASSERT_NO_THROW(other_cfg.add(option, false, "isc")); + + // Add an option with a formatted value. + option.reset(new Option(Option::V4, 2)); + OptionDescriptor desc(option, false, "one,two,three"); + ASSERT_NO_THROW(other_cfg.add(desc, "isc")); + + // When we attempt to merge, it should fail, recognizing that + // option 2, which has a formatted value, has no definition. + ASSERT_THROW_MSG(this_cfg.merge(defs, other_cfg), InvalidOperation, + "option: isc.2 has a formatted value: " + "'one,two,three' but no option definition"); + + // Now let's add an option definition that will force data truncated + // error for option 1. + defs->add((OptionDefinitionPtr(new OptionDefinition("one", 1, "isc", "uint16")))); + + // When we attempt to merge, it should fail because option 1's data + // is not valid per its definition. + EXPECT_THROW_MSG(this_cfg.merge(defs, other_cfg), InvalidOperation, + "could not create option: isc.1" + " from data specified, reason:" + " OptionInt 1 truncated"); +} + +// This test verifies the all of the valid option cases +// in createDescriptorOption(): +// 1. standard option +// 2. vendor option +// 3. user-defined, unformatted option +// 4. user-defined, formatted option +// 5. undefined, unformatted option +TEST_F(CfgOptionTest, createDescriptorOptionValid) { + // First we'll create our "known" user definitions + CfgOptionDefPtr defs(new CfgOptionDef()); + defs->add((OptionDefinitionPtr(new OptionDefinition("one", 1, "isc", "uint8")))); + defs->add((OptionDefinitionPtr(new OptionDefinition("two", 2, "isc", "uint8", true)))); + + // We'll try a standard V4 option first. + std::string space = DHCP4_OPTION_SPACE; + std::string value = "v4.example.com"; + OptionPtr option(new Option(Option::V6, DHO_HOST_NAME)); + option->setData(value.begin(), value.end()); + OptionDescriptorPtr desc(new OptionDescriptor(option, false)); + + bool updated = false; + ASSERT_NO_THROW(updated = CfgOption::createDescriptorOption(defs, space, *desc)); + ASSERT_TRUE(updated); + OptionStringPtr opstr = boost::dynamic_pointer_cast<OptionString>(desc->option_); + ASSERT_TRUE(opstr); + EXPECT_EQ("v4.example.com", opstr->getValue()); + + // Next we'll try a standard V6 option. + space = DHCP6_OPTION_SPACE; + std::vector<uint8_t> fqdn = + { 2, 'v', '6', 7, 'e', 'x', 'a', 'm', 'p', 'l', 'e', 3, 'c', 'o', 'm', 0 }; + option.reset(new Option(Option::V6, D6O_AFTR_NAME)); + option->setData(fqdn.begin(), fqdn.end()); + desc.reset(new OptionDescriptor(option, false)); + + ASSERT_NO_THROW(updated = CfgOption::createDescriptorOption(defs, space, *desc)); + ASSERT_TRUE(updated); + OptionCustomPtr opcustom = boost::dynamic_pointer_cast<OptionCustom>(desc->option_); + ASSERT_TRUE(opcustom); + EXPECT_EQ("v6.example.com.", opcustom->readFqdn()); + + // Next we'll try a vendor option with a formatted value + space = "vendor-4491"; + value = "192.0.2.1, 192.0.2.2"; + option.reset(new Option(Option::V4, 2)); + desc.reset(new OptionDescriptor(option, false, value)); + + ASSERT_NO_THROW(updated = CfgOption::createDescriptorOption(defs, space, *desc)); + ASSERT_TRUE(updated); + Option4AddrLstPtr opaddrs = boost::dynamic_pointer_cast<Option4AddrLst>(desc->option_); + ASSERT_TRUE(opaddrs); + Option4AddrLst::AddressContainer exp_addresses = { IOAddress("192.0.2.1"), IOAddress("192.0.2.2") }; + EXPECT_EQ(exp_addresses, opaddrs->getAddresses()); + + // Now, a user defined uint8 option + space = "isc"; + option.reset(new Option(Option::V4, 1, OptionBuffer(1, 0x77))); + desc.reset(new OptionDescriptor(option, false)); + + ASSERT_NO_THROW(updated = CfgOption::createDescriptorOption(defs, space, *desc)); + ASSERT_TRUE(updated); + OptionUint8Ptr opint = boost::dynamic_pointer_cast<OptionUint8>(desc->option_); + ASSERT_TRUE(opint); + EXPECT_EQ(119, opint->getValue()); + + // Now, a user defined array of ints from a formatted value + option.reset(new Option(Option::V4, 2)); + desc.reset(new OptionDescriptor(option, false, "1,2,3")); + + ASSERT_NO_THROW(updated = CfgOption::createDescriptorOption(defs, space, *desc)); + ASSERT_TRUE(updated); + OptionUint8ArrayPtr oparray = boost::dynamic_pointer_cast<OptionUint8Array>(desc->option_); + ASSERT_TRUE(oparray); + std::vector<uint8_t> exp_ints = { 1, 2, 3 }; + EXPECT_EQ(exp_ints, oparray->getValues()); + + // Finally, a generic, undefined option + option.reset(new Option(Option::V4, 199, OptionBuffer(1, 0x77))); + desc.reset(new OptionDescriptor(option, false)); + + ASSERT_NO_THROW(updated = CfgOption::createDescriptorOption(defs, space, *desc)); + ASSERT_FALSE(updated); + ASSERT_EQ(1, desc->option_->getData().size()); + EXPECT_EQ(0x77, desc->option_->getData()[0]); +} + +// This test verifies that encapsulated options are added as sub-options +// to the top level options on request. +TEST_F(CfgOptionTest, encapsulate) { + CfgOption cfg; + + generateEncapsulatedOptions(cfg); + + // Append options from "foo" and "bar" space as sub-options and options + // from "foo-subs" and "bar-subs" as sub-options of "foo" and "bar" + // options. + ASSERT_NO_THROW(cfg.encapsulate()); + + // Verify that we have 40 top-level options. + OptionContainerPtr options = cfg.getAll(DHCP6_OPTION_SPACE); + ASSERT_EQ(40, options->size()); + + // Iterate over top level options. + for (uint16_t code = 1000; code < 1040; ++code) { + + OptionUint16Ptr option = boost::dynamic_pointer_cast< + OptionUint16>(cfg.get(DHCP6_OPTION_SPACE, code).option_); + ASSERT_TRUE(option) << "option with code " << code << " not found"; + + // First level sub options. There are 19 sub-options for each top + // level option. + const OptionCollection& first_level = option->getOptions(); + ASSERT_EQ(19, first_level.size()); + + // Iterate over all first level sub-options. + std::pair<unsigned int, OptionPtr> first_level_opt; + BOOST_FOREACH(first_level_opt, first_level) { + // Each option in this test comprises a single one byte field and + // should cast to OptionUint8 type. + OptionUint8Ptr first_level_uint8 = boost::dynamic_pointer_cast< + OptionUint8>(first_level_opt.second); + ASSERT_TRUE(first_level_uint8); + + const unsigned int value = static_cast<unsigned int>(first_level_uint8->getValue()); + // There are two sets of first level sub-options. Those that include + // a value of 1 and those that include a value of 2. + if (first_level_uint8->getType() < 20) { + EXPECT_EQ(1, value); + } else { + EXPECT_EQ(2, value); + } + + // Each first level sub-option should include 9 second level + // sub options. + const OptionCollection& second_level = first_level_uint8->getOptions(); + ASSERT_EQ(9, second_level.size()); + + // Iterate over sub-options and make sure they include the expected + // values. + std::pair<unsigned int, OptionPtr> second_level_opt; + BOOST_FOREACH(second_level_opt, second_level) { + OptionUint8Ptr second_level_uint8 = boost::dynamic_pointer_cast< + OptionUint8>(second_level_opt.second); + ASSERT_TRUE(second_level_uint8); + const unsigned value = static_cast< + unsigned>(second_level_uint8->getValue()); + // Certain sub-options should have a value of 3, other the values + // of 4. + if (second_level_uint8->getType() < 20) { + EXPECT_EQ(3, value); + } else { + EXPECT_EQ(4, value); + } + } + } + } +} + +// This test verifies that an option can be deleted from the configuration. +TEST_F(CfgOptionTest, deleteOptions) { + CfgOption cfg; + + generateEncapsulatedOptions(cfg); + + // Append options from "foo" and "bar" space as sub-options and options + // from "foo-subs" and "bar-subs" as sub-options of "foo" and "bar" + // options. + ASSERT_NO_THROW(cfg.encapsulate()); + + // The option with the code 5 should exist in the option space "foo". + ASSERT_TRUE(cfg.get("foo", 5).option_); + + // Because we called "encapsulate", this option should have been + // propagated to the options encapsulating option space "foo". + for (uint16_t code = 1000; code < 1020; ++code) { + OptionDescriptor top_level_option(false); + ASSERT_NO_THROW(top_level_option = cfg.get(DHCP6_OPTION_SPACE, code)); + // Make sure that the option with code 5 is there. + ASSERT_TRUE(top_level_option.option_); + ASSERT_TRUE(top_level_option.option_->getOption(5)); + + // Make sure that options belonging to space "foo-subs" should contain + // options with the code of 5. + for (uint16_t code_subs = 1; code_subs != 19; ++code_subs) { + OptionPtr sub_option; + ASSERT_NO_THROW(sub_option = top_level_option.option_->getOption(code_subs)); + ASSERT_TRUE(sub_option); + ASSERT_TRUE(sub_option->getOption(5)); + } + } + + // Delete option with the code 5 and belonging to option space "foo". + uint64_t deleted_num; + ASSERT_NO_THROW(deleted_num = cfg.del("foo", 5)); + EXPECT_EQ(1, deleted_num); + + // The option should now be gone from options config. + EXPECT_FALSE(cfg.get("foo", 5).option_); + // Option with the code of 5 in other option spaces should remain. + EXPECT_TRUE(cfg.get("foo-subs", 5).option_); + + // Iterate over the options encapsulating "foo" option space. Make sure + // that the option with code 5 is no longer encapsulated by these options. + for (uint16_t code = 1000; code < 1020; ++code) { + OptionDescriptor top_level_option(false); + ASSERT_NO_THROW(top_level_option = cfg.get(DHCP6_OPTION_SPACE, code)); + ASSERT_TRUE(top_level_option.option_); + EXPECT_FALSE(top_level_option.option_->getOption(5)); + // Other options should remain. + EXPECT_TRUE(top_level_option.option_->getOption(1)); + + // Iterate over options within foo-subs option space and make sure + // that they still contain options with the code of 5. + for (uint16_t code_subs = 1; code_subs != 19; ++code_subs) { + // There shouldn't be option with the code of 5 in the "foo" space. + if (code_subs == 5) { + continue; + } + + OptionPtr sub_option; + ASSERT_NO_THROW(sub_option = top_level_option.option_->getOption(code_subs)); + // Options belonging to option space "foo-subs" should include option + // with the code of 5. + ASSERT_TRUE(sub_option); + ASSERT_TRUE(sub_option->getOption(5)); + } + } +} + +// This test verifies that an option can be deleted from the configuration +// by database id. +TEST_F(CfgOptionTest, deleteOptionsById) { + CfgOption cfg; + + generateEncapsulatedOptions(cfg); + + // Append options from "foo" and "bar" space as sub-options and options + // from "foo-subs" and "bar-subs" as sub-options of "foo" and "bar" + // options. + ASSERT_NO_THROW(cfg.encapsulate()); + + // Create multiple vendor options for vendor id 123. + for (uint16_t code = 100; code < 110; ++code) { + OptionPtr option(new Option(Option::V6, code, OptionBuffer(1, 0xFF))); + ASSERT_NO_THROW(cfg.add(option, false, "vendor-123", static_cast<uint64_t>(code))); + } + + // Delete options with id of 100. It includes both regular options and + // the vendor options. There are two options with id of 100. + EXPECT_EQ(2, cfg.del(100)); + + // Make sure that the option 100 was deleted but another option + // in the same option space was not. + EXPECT_FALSE(cfg.get("bar", 100).option_); + EXPECT_TRUE(cfg.get("bar", 101).option_); + + // Make sure that the deleted option was dereferenced from the + // top level options but that does not affect encapsulation of + // other options. + for (uint16_t option_code = 1020; option_code < 1040; ++option_code) { + auto top_level_option = cfg.get(DHCP6_OPTION_SPACE, option_code); + ASSERT_TRUE(top_level_option.option_); + EXPECT_FALSE(top_level_option.option_->getOption(100)); + + // The second level encapsulation should have been preserved. + auto second_level_option = top_level_option.option_->getOption(101); + ASSERT_TRUE(second_level_option); + EXPECT_TRUE(second_level_option->getOption(501)); + } + + // Vendor option with id of 100 should have been deleted too. + EXPECT_FALSE(cfg.get(123, 100).option_); + EXPECT_TRUE(cfg.get(123, 101).option_); +} + +// This test verifies that a vendor option can be deleted from the configuration. +TEST_F(CfgOptionTest, delVendorOption) { + CfgOption cfg; + + // Create multiple vendor options for vendor id 123. + for (uint16_t code = 100; code < 110; ++code) { + OptionPtr option(new Option(Option::V6, code, OptionBuffer(1, 0xFF))); + ASSERT_NO_THROW(cfg.add(option, false, "vendor-123")); + } + + // Create multiple vendor options for vendor id 234. + for (uint16_t code = 100; code < 110; ++code) { + OptionPtr option(new Option(Option::V6, code, OptionBuffer(1, 0xFF))); + ASSERT_NO_THROW(cfg.add(option, false, "vendor-234")); + } + + // Make sure that the option we're trying to delete is there. + ASSERT_TRUE(cfg.get(123, 105).option_); + // There should also be an option 105 for vendor id 234. + ASSERT_TRUE(cfg.get(234, 105).option_); + + // Delete the option for vendor id 123. + uint64_t deleted_num; + ASSERT_NO_THROW(deleted_num = cfg.del(123, 105)); + EXPECT_EQ(1, deleted_num); + + // Make sure the option is gone. + EXPECT_FALSE(cfg.get(123, 105).option_); + // Make sure that the option with code 105 is not affected for vendor id 234. + EXPECT_TRUE(cfg.get(234, 105).option_); + + // Other options, like 107, shouldn't be gone. + EXPECT_TRUE(cfg.get(123, 107).option_); +} + +// This test verifies that single option can be retrieved from the configuration +// using option code and option space. +TEST_F(CfgOptionTest, get) { + CfgOption cfg; + + // Add 10 options to a "dhcp6" option space in the subnet. + for (uint16_t code = 100; code < 110; ++code) { + OptionPtr option(new Option(Option::V6, code, OptionBuffer(10, 0xFF))); + ASSERT_NO_THROW(cfg.add(option, false, DHCP6_OPTION_SPACE)); + } + + // Check that we can get each added option descriptor using + // individually. + for (uint16_t code = 100; code < 110; ++code) { + std::ostringstream stream; + // First, try the invalid option space name. + OptionDescriptor desc = cfg.get("isc", code); + // Returned descriptor should contain NULL option ptr. + EXPECT_FALSE(desc.option_); + // Now, try the valid option space. + desc = cfg.get(DHCP6_OPTION_SPACE, code); + // Test that the option code matches the expected code. + ASSERT_TRUE(desc.option_); + EXPECT_EQ(code, desc.option_->getType()); + } +} + +// This test verifies that the same options can be added to the configuration +// under different option space. +TEST_F(CfgOptionTest, addNonUniqueOptions) { + CfgOption cfg; + + // Create a set of options with non-unique codes. + for (int i = 0; i < 2; ++i) { + // In the inner loop we create options with unique codes (100-109). + for (uint16_t code = 100; code < 110; ++code) { + OptionPtr option(new Option(Option::V6, code, OptionBuffer(10, 0xFF))); + ASSERT_NO_THROW(cfg.add(option, false, DHCP6_OPTION_SPACE)); + } + } + + // Sanity check that all options are there. + OptionContainerPtr options = cfg.getAll(DHCP6_OPTION_SPACE); + ASSERT_EQ(20, options->size()); + + // Use container index #1 to get the options by their codes. + OptionContainerTypeIndex& idx = options->get<1>(); + // Look for the codes 100-109. + for (uint16_t code = 100; code < 110; ++ code) { + // For each code we should get two instances of options-> + OptionContainerTypeRange range = idx.equal_range(code); + // Distance between iterators indicates how many options + // have been returned for the particular code. + ASSERT_EQ(2, distance(range.first, range.second)); + // Check that returned options actually have the expected option code. + for (OptionContainerTypeIndex::const_iterator option_desc = range.first; + option_desc != range.second; ++option_desc) { + ASSERT_TRUE(option_desc->option_); + EXPECT_EQ(code, option_desc->option_->getType()); + } + } + + // Let's try to find some non-exiting option. + const uint16_t non_existing_code = 150; + OptionContainerTypeRange range = idx.equal_range(non_existing_code); + // Empty set is expected. + EXPECT_EQ(0, distance(range.first, range.second)); +} + +// This test verifies that the option with the persistency flag can be +// added to the configuration and that options with the persistency flags +// can be retrieved. +TEST(Subnet6Test, addPersistentOption) { + CfgOption cfg; + + // Add 10 options to the subnet with option codes 100 - 109. + for (uint16_t code = 100; code < 110; ++code) { + OptionPtr option(new Option(Option::V6, code, OptionBuffer(10, 0xFF))); + // We create 10 options and want some of them to be flagged + // persistent and some non-persistent. Persistent options are + // those that server sends to clients regardless if they ask + // for them or not. We pick 3 out of 10 options and mark them + // non-persistent and 7 other options persistent. + // Code values: 102, 105 and 108 are divisible by 3 + // and options with these codes will be flagged non-persistent. + // Options with other codes will be flagged persistent. + bool persistent = (code % 3) ? true : false; + ASSERT_NO_THROW(cfg.add(option, persistent, DHCP6_OPTION_SPACE)); + } + + // Get added options from the subnet. + OptionContainerPtr options = cfg.getAll(DHCP6_OPTION_SPACE); + + // options->get<2> returns reference to container index #2. This + // index is used to access options by the 'persistent' flag. + OptionContainerPersistIndex& idx = options->get<2>(); + + // Get all persistent options-> + OptionContainerPersistRange range_persistent = idx.equal_range(true); + // 7 out of 10 options have been flagged persistent. + ASSERT_EQ(7, distance(range_persistent.first, range_persistent.second)); + + // Get all non-persistent options-> + OptionContainerPersistRange range_non_persistent = idx.equal_range(false); + // 3 out of 10 options have been flagged not persistent. + ASSERT_EQ(3, distance(range_non_persistent.first, range_non_persistent.second)); +} + +// This test verifies that the vendor option can be added to the configuration. +TEST_F(CfgOptionTest, addVendorOptions) { + CfgOption cfg; + + // Differentiate options by their codes (100-109) + for (uint16_t code = 100; code < 110; ++code) { + OptionPtr option(new Option(Option::V6, code, OptionBuffer(10, 0xFF))); + ASSERT_NO_THROW(cfg.add(option, false, "vendor-12345678")); + } + + // Second option space uses corner case value for vendor id = max uint8. + uint32_t vendor_id = std::numeric_limits<uint32_t>::max(); + std::ostringstream option_space; + option_space << "vendor-" << vendor_id; + + // Add 7 options to another option space. The option codes partially overlap + // with option codes that we have added to dhcp6 option space. + for (uint16_t code = 105; code < 112; ++code) { + OptionPtr option(new Option(Option::V6, code, OptionBuffer(10, 0xFF))); + ASSERT_NO_THROW(cfg.add(option, false, option_space.str())); + } + + // Get options from the Subnet and check if all 10 are there. + OptionContainerPtr options = cfg.getAll(12345678); + ASSERT_TRUE(options); + ASSERT_EQ(10, options->size()); + + // Validate codes of options added to dhcp6 option space. + uint16_t expected_code = 100; + for (OptionContainer::const_iterator option_desc = options->begin(); + option_desc != options->end(); ++option_desc) { + ASSERT_TRUE(option_desc->option_); + EXPECT_EQ(expected_code, option_desc->option_->getType()); + ++expected_code; + } + + options = cfg.getAll(vendor_id); + ASSERT_TRUE(options); + ASSERT_EQ(7, options->size()); + + // Validate codes of options added to isc option space. + expected_code = 105; + for (OptionContainer::const_iterator option_desc = options->begin(); + option_desc != options->end(); ++option_desc) { + ASSERT_TRUE(option_desc->option_); + EXPECT_EQ(expected_code, option_desc->option_->getType()); + ++expected_code; + } + + // Try to get options from a non-existing option space. + options = cfg.getAll(1111111); + ASSERT_TRUE(options); + EXPECT_TRUE(options->empty()); +} + +// This test verifies that option space names for the vendor options are +// correct. +TEST_F(CfgOptionTest, getVendorIdsSpaceNames) { + CfgOption cfg; + + // Create 10 options, each goes under a different vendor id. + for (uint16_t code = 100; code < 110; ++code) { + OptionPtr option(new Option(Option::V6, code, OptionBuffer(10, 0xFF))); + // Generate space name for a unique vendor id. + std::ostringstream s; + s << "vendor-" << code; + ASSERT_NO_THROW(cfg.add(option, false, s.str())); + } + + // We should now have 10 different vendor ids. + std::list<std::string> space_names = cfg.getVendorIdsSpaceNames(); + ASSERT_EQ(10, space_names.size()); + + // Check that the option space names for those vendor ids are correct. + for (std::list<std::string>::iterator name = space_names.begin(); + name != space_names.end(); ++name) { + uint16_t id = static_cast<uint16_t>(std::distance(space_names.begin(), + name)); + std::ostringstream s; + s << "vendor-" << (100 + id); + EXPECT_EQ(s.str(), *name); + } +} + +// This test verifies that the unparse function returns what is expected. +TEST_F(CfgOptionTest, unparse) { + CfgOption cfg; + + // Add some options. + OptionPtr opt1(new Option(Option::V6, 100, OptionBuffer(4, 0x12))); + cfg.add(opt1, false, "dns"); + OptionPtr opt2(new Option(Option::V6, 101, OptionBuffer(4, 12))); + OptionDescriptor desc2(opt2, false, "12, 12, 12, 12"); + std::string ctx = "{ \"comment\": \"foo\", \"bar\": 1 }"; + desc2.setContext(data::Element::fromJSON(ctx)); + cfg.add(desc2, "dns"); + OptionPtr opt3(new Option(Option::V6, D6O_STATUS_CODE, OptionBuffer(2, 0))); + cfg.add(opt3, false, DHCP6_OPTION_SPACE); + OptionPtr opt4(new Option(Option::V6, 100, OptionBuffer(4, 0x21))); + cfg.add(opt4, true, "vendor-1234"); + + // Unparse + std::string expected = "[\n" + "{\n" + " \"code\": 100,\n" + " \"space\": \"dns\",\n" + " \"csv-format\": false,\n" + " \"data\": \"12121212\",\n" + " \"always-send\": false\n" + "},{\n" + " \"code\": 101,\n" + " \"space\": \"dns\",\n" + " \"csv-format\": true,\n" + " \"data\": \"12, 12, 12, 12\",\n" + " \"always-send\": false,\n" + " \"user-context\": { \"comment\": \"foo\", \"bar\": 1 }\n" + "},{\n" + " \"code\": 13,\n" + " \"name\": \"status-code\",\n" + " \"space\": \"dhcp6\",\n" + " \"csv-format\": false,\n" + " \"data\": \"0000\",\n" + " \"always-send\": false\n" + "},{\n" + " \"code\": 100,\n" + " \"space\": \"vendor-1234\",\n" + " \"csv-format\": false,\n" + " \"data\": \"21212121\",\n" + " \"always-send\": true\n" + "}]\n"; + isc::test::runToElementTest<CfgOption>(expected, cfg); +} + +} // end of anonymous namespace |