diff options
Diffstat (limited to 'src/bin/dhcp6/tests/parser_unittest.cc')
-rw-r--r-- | src/bin/dhcp6/tests/parser_unittest.cc | 980 |
1 files changed, 980 insertions, 0 deletions
diff --git a/src/bin/dhcp6/tests/parser_unittest.cc b/src/bin/dhcp6/tests/parser_unittest.cc new file mode 100644 index 0000000..4ea7a28 --- /dev/null +++ b/src/bin/dhcp6/tests/parser_unittest.cc @@ -0,0 +1,980 @@ +// Copyright (C) 2016-2023 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 <dhcp6/parser_context.h> +#include <dhcpsrv/parsers/simple_parser6.h> +#include <testutils/io_utils.h> +#include <testutils/log_utils.h> +#include <testutils/test_to_element.h> +#include <testutils/user_context_utils.h> +#include <testutils/gtest_utils.h> + +#include <gtest/gtest.h> + +#include <fstream> +#include <set> + +#include <boost/algorithm/string.hpp> + +using namespace isc::data; +using namespace std; +using namespace isc::test; + +namespace isc { +namespace dhcp { +namespace test { + +/// @brief compares two JSON trees +/// +/// If differences are discovered, gtest failure is reported (using EXPECT_EQ) +/// +/// @param a first to be compared +/// @param b second to be compared +void compareJSON(ConstElementPtr a, ConstElementPtr b) { + ASSERT_TRUE(a); + ASSERT_TRUE(b); + EXPECT_EQ(a->str(), b->str()) +#ifdef HAVE_CREATE_UNIFIED_DIFF + << "\nDiff:\n" << generateDiff(prettyPrint(a), prettyPrint(b)) << "\n" +#endif + ; +} + +/// @brief Tests if the input string can be parsed with specific parser +/// +/// The input text will be passed to bison parser of specified type. +/// Then the same input text is passed to legacy JSON parser and outputs +/// from both parsers are compared. The legacy comparison can be disabled, +/// if the feature tested is not supported by the old parser (e.g. +/// new comment styles) +/// +/// @param txt text to be compared +/// @param parser_type bison parser type to be instantiated +/// @param compare whether to compare the output with legacy JSON parser +void testParser(const std::string& txt, Parser6Context::ParserType parser_type, + bool compare = true) { + SCOPED_TRACE("\n=== tested config ===\n" + txt + "====================="); + + ConstElementPtr test_json; + ASSERT_NO_THROW({ + try { + Parser6Context ctx; + test_json = ctx.parseString(txt, parser_type); + } catch (const std::exception &e) { + cout << "EXCEPTION: " << e.what() << endl; + throw; + } + + }); + + if (!compare) { + return; + } + + // Now compare if both representations are the same. + ElementPtr reference_json; + ASSERT_NO_THROW(reference_json = Element::fromJSON(txt, true)); + compareJSON(reference_json, test_json); +} + +TEST(ParserTest, mapInMap) { + string txt = "{ \"xyzzy\": { \"foo\": 123, \"baz\": 456 } }"; + testParser(txt, Parser6Context::PARSER_JSON); +} + +TEST(ParserTest, listInList) { + string txt = "[ [ \"Britain\", \"Wales\", \"Scotland\" ], " + "[ \"Pomorze\", \"Wielkopolska\", \"Tatry\"] ]"; + testParser(txt, Parser6Context::PARSER_JSON); +} + +TEST(ParserTest, nestedMaps) { + string txt = "{ \"europe\": { \"UK\": { \"London\": { \"street\": \"221B Baker\" }}}}"; + testParser(txt, Parser6Context::PARSER_JSON); +} + +TEST(ParserTest, nestedLists) { + string txt = "[ \"half\", [ \"quarter\", [ \"eighth\", [ \"sixteenth\" ]]]]"; + testParser(txt, Parser6Context::PARSER_JSON); +} + +TEST(ParserTest, listsInMaps) { + string txt = "{ \"constellations\": { \"orion\": [ \"rigel\", \"betelgeuse\" ], " + "\"cygnus\": [ \"deneb\", \"albireo\"] } }"; + testParser(txt, Parser6Context::PARSER_JSON); +} + +TEST(ParserTest, mapsInLists) { + string txt = "[ { \"body\": \"earth\", \"gravity\": 1.0 }," + " { \"body\": \"mars\", \"gravity\": 0.376 } ]"; + testParser(txt, Parser6Context::PARSER_JSON); +} + +TEST(ParserTest, types) { + string txt = "{ \"string\": \"foo\"," + "\"integer\": 42," + "\"boolean\": true," + "\"map\": { \"foo\": \"bar\" }," + "\"list\": [ 1, 2, 3 ]," + "\"null\": null }"; + testParser(txt, Parser6Context::PARSER_JSON); +} + +TEST(ParserTest, keywordJSON) { + string txt = "{ \"name\": \"user\"," + "\"type\": \"password\"," + "\"user\": \"name\"," + "\"password\": \"type\" }"; + testParser(txt, Parser6Context::PARSER_JSON); +} + +TEST(ParserTest, keywordDhcp6) { + string txt = "{ \"Dhcp6\": { \"interfaces-config\": {" + " \"interfaces\": [ \"type\", \"htype\" ] },\n" + "\"preferred-lifetime\": 3000,\n" + "\"rebind-timer\": 2000, \n" + "\"renew-timer\": 1000, \n" + "\"subnet6\": [ { " + " \"id\": 1, " + " \"pools\": [ { \"pool\": \"2001:db8:1::/64\" } ]," + " \"subnet\": \"2001:db8:1::/48\", " + " \"interface\": \"test\" } ],\n" + "\"valid-lifetime\": 4000 } }"; + testParser(txt, Parser6Context::PARSER_DHCP6); +} + +// Tests if bash (#) comments are supported. That's the only comment type that +// was supported by the old parser. +TEST(ParserTest, bashComments) { + string txt= "{ \"Dhcp6\": { \"interfaces-config\": {" + " \"interfaces\": [ \"*\" ]" + "},\n" + "\"preferred-lifetime\": 3000,\n" + "# this is a comment\n" + "\"rebind-timer\": 2000, \n" + "# lots of comments here\n" + "# and here\n" + "\"renew-timer\": 1000, \n" + "\"subnet6\": [ { " + " \"id\": 1, " + " \"pools\": [ { \"pool\": \"2001:db8:1::/64\" } ]," + " \"subnet\": \"2001:db8:1::/48\", " + " \"interface\": \"eth0\"" + " } ]," + "\"valid-lifetime\": 4000 } }"; + testParser(txt, Parser6Context::PARSER_DHCP6); +} + +// Tests if C++ (//) comments can start anywhere, not just in the first line. +TEST(ParserTest, cppComments) { + string txt= "{ \"Dhcp6\": { \"interfaces-config\": {" + " \"interfaces\": [ \"*\" ]" + "},\n" + "\"preferred-lifetime\": 3000, // this is a comment \n" + "\"rebind-timer\": 2000, // everything after // is ignored\n" + "\"renew-timer\": 1000, // this will be ignored, too\n" + "\"subnet6\": [ { " + " \"id\": 1, " + " \"pools\": [ { \"pool\": \"2001:db8:1::/64\" } ]," + " \"subnet\": \"2001:db8:1::/48\", " + " \"interface\": \"eth0\"" + " } ]," + "\"valid-lifetime\": 4000 } }"; + testParser(txt, Parser6Context::PARSER_DHCP6, false); +} + +// Tests if bash (#) comments can start anywhere, not just in the first line. +TEST(ParserTest, bashCommentsInline) { + string txt= "{ \"Dhcp6\": { \"interfaces-config\": {" + " \"interfaces\": [ \"*\" ]" + "},\n" + "\"preferred-lifetime\": 3000, # this is a comment \n" + "\"rebind-timer\": 2000, # everything after # is ignored\n" + "\"renew-timer\": 1000, # this will be ignored, too\n" + "\"subnet6\": [ { " + " \"id\": 1, " + " \"pools\": [ { \"pool\": \"2001:db8:1::/64\" } ]," + " \"subnet\": \"2001:db8:1::/48\", " + " \"interface\": \"eth0\"" + " } ]," + "\"valid-lifetime\": 4000 } }"; + testParser(txt, Parser6Context::PARSER_DHCP6, false); +} + +// Tests if multi-line C style comments are handled correctly. +TEST(ParserTest, multilineComments) { + string txt= "{ \"Dhcp6\": { \"interfaces-config\": {" + " \"interfaces\": [ \"*\" ]" + "},\n" + "\"preferred-lifetime\": 3000, /* this is a C style comment\n" + "that\n can \n span \n multiple \n lines */ \n" + "\"rebind-timer\": 2000,\n" + "\"renew-timer\": 1000, \n" + "\"subnet6\": [ { " + " \"id\": 1, " + " \"pools\": [ { \"pool\": \"2001:db8:1::/64\" } ]," + " \"subnet\": \"2001:db8:1::/48\", " + " \"interface\": \"eth0\"" + " } ]," + "\"valid-lifetime\": 4000 } }"; + testParser(txt, Parser6Context::PARSER_DHCP6, false); +} + +// Tests if embedded comments are handled correctly. +TEST(ParserTest, embbededComments) { + string txt= "{ \"Dhcp6\": { \"interfaces-config\": {" + " \"interfaces\": [ \"*\" ]" + "},\n" + "\"comment\": \"a comment\",\n" + "\"preferred-lifetime\": 3000,\n" + "\"rebind-timer\": 2000,\n" + "\"renew-timer\": 1000, \n" + "\"subnet6\": [ { " + " \"id\": 1, " + " \"user-context\": { \"comment\": \"indirect\" }," + " \"pools\": [ { \"pool\": \"2001:db8:1::/64\" } ]," + " \"subnet\": \"2001:db8:1::/48\", " + " \"interface\": \"eth0\"" + " } ]," + "\"user-context\": { \"compatible\": true }," + "\"valid-lifetime\": 4000 } }"; + testParser(txt, Parser6Context::PARSER_DHCP6, false); +} + +/// @brief Loads specified example config file +/// +/// This test loads specified example file twice: first, using the legacy +/// JSON file and then second time using bison parser. Two created Element +/// trees are then compared. The input is decommented before it is passed +/// to legacy parser (as legacy support for comments is very limited). +/// +/// @param fname name of the file to be loaded +void testFile(const std::string& fname) { + ElementPtr json; + ElementPtr reference_json; + ConstElementPtr test_json; + + string decommented = decommentJSONfile(fname); + + cout << "Parsing file " << fname << "(" << decommented << ")" << endl; + + EXPECT_NO_THROW_LOG(json = Element::fromJSONFile(decommented, true)); + reference_json = moveComments(json); + + // remove the temporary file + EXPECT_NO_THROW(::remove(decommented.c_str())); + + EXPECT_NO_THROW( + try { + Parser6Context ctx; + test_json = ctx.parseFile(fname, Parser6Context::PARSER_DHCP6); + } catch (const std::exception &x) { + cout << "EXCEPTION: " << x.what() << endl; + throw; + }); + + ASSERT_TRUE(reference_json); + ASSERT_TRUE(test_json); + + compareJSON(reference_json, test_json); +} + +// This test loads all available existing files. Each config is loaded +// twice: first with the existing Element::fromJSONFile() and then +// the second time with Parser6. Both JSON trees are then compared. +TEST(ParserTest, file) { + vector<string> configs; + configs.push_back("advanced.json"); + configs.push_back("all-keys.json"); + configs.push_back("all-options.json"); + configs.push_back("backends.json"); + configs.push_back("classify.json"); + configs.push_back("classify2.json"); + configs.push_back("comments.json"); + configs.push_back("config-backend.json"); + configs.push_back("dhcpv4-over-dhcpv6.json"); + configs.push_back("dnr.json"); + configs.push_back("duid.json"); + configs.push_back("global-reservations.json"); + configs.push_back("ha-hot-standby-server1-with-tls.json"); + configs.push_back("ha-hot-standby-server2.json"); + configs.push_back("hooks.json"); + configs.push_back("iPXE.json"); + configs.push_back("leases-expiration.json"); + configs.push_back("multiple-options.json"); + configs.push_back("mysql-reservations.json"); + configs.push_back("pgsql-reservations.json"); + configs.push_back("reservations.json"); + configs.push_back("several-subnets.json"); + configs.push_back("shared-network.json"); + configs.push_back("simple.json"); + configs.push_back("softwire46.json"); + configs.push_back("stateless.json"); + configs.push_back("tee-times.json"); + configs.push_back("with-ddns.json"); + + for (int i = 0; i<configs.size(); i++) { + testFile(string(CFG_EXAMPLES) + "/" + configs[i]); + } +} + +// This test loads the all-keys.json file and checks global parameters. +TEST(ParserTest, globalParameters) { + ConstElementPtr json; + Parser6Context ctx; + string fname = string(CFG_EXAMPLES) + "/" + "all-keys.json"; + EXPECT_NO_THROW(json = ctx.parseFile(fname, Parser6Context::PARSER_DHCP6)); + EXPECT_NO_THROW(json = json->get("Dhcp6")); + SimpleParser6 parser; + EXPECT_NO_THROW(parser.checkKeywords(parser.GLOBAL6_PARAMETERS, json)); +} + +/// @brief Tests error conditions in Dhcp6Parser +/// +/// @param txt text to be parsed +/// @param parser_type type of the parser to be used in the test +/// @param msg expected content of the exception +void testError(const std::string& txt, + Parser6Context::ParserType parser_type, + const std::string& msg) { + SCOPED_TRACE("\n=== tested config ===\n" + txt + "====================="); + + try { + Parser6Context ctx; + ConstElementPtr parsed = ctx.parseString(txt, parser_type); + FAIL() << "Expected Dhcp6ParseError but nothing was raised (expected: " + << msg << ")"; + } + catch (const Dhcp6ParseError& ex) { + EXPECT_EQ(msg, ex.what()); + } + catch (...) { + FAIL() << "Expected Dhcp6ParseError but something else was raised"; + } +} + +// Verify that error conditions are handled correctly. +TEST(ParserTest, errors) { + // no input + testError("", Parser6Context::PARSER_JSON, + "<string>:1.1: syntax error, unexpected end of file"); + testError(" ", Parser6Context::PARSER_JSON, + "<string>:1.2: syntax error, unexpected end of file"); + testError("\n", Parser6Context::PARSER_JSON, + "<string>:2.1: syntax error, unexpected end of file"); + testError("\t", Parser6Context::PARSER_JSON, + "<string>:1.2: syntax error, unexpected end of file"); + testError("\r", Parser6Context::PARSER_JSON, + "<string>:1.2: syntax error, unexpected end of file"); + + // comments + testError("# nothing\n", + Parser6Context::PARSER_JSON, + "<string>:2.1: syntax error, unexpected end of file"); + testError(" #\n", + Parser6Context::PARSER_JSON, + "<string>:2.1: syntax error, unexpected end of file"); + testError("// nothing\n", + Parser6Context::PARSER_JSON, + "<string>:2.1: syntax error, unexpected end of file"); + testError("/* nothing */\n", + Parser6Context::PARSER_JSON, + "<string>:2.1: syntax error, unexpected end of file"); + testError("/* no\nthing */\n", + Parser6Context::PARSER_JSON, + "<string>:3.1: syntax error, unexpected end of file"); + testError("/* no\nthing */\n\n", + Parser6Context::PARSER_JSON, + "<string>:4.1: syntax error, unexpected end of file"); + testError("/* nothing\n", + Parser6Context::PARSER_JSON, + "Comment not closed. (/* in line 1"); + testError("\n\n\n/* nothing\n", + Parser6Context::PARSER_JSON, + "Comment not closed. (/* in line 4"); + testError("{ /* */*/ }\n", + Parser6Context::PARSER_JSON, + "<string>:1.3-8: Invalid character: *"); + testError("{ /* // *// }\n", + Parser6Context::PARSER_JSON, + "<string>:1.3-11: Invalid character: /"); + testError("{ /* // */// }\n", + Parser6Context::PARSER_JSON, + "<string>:2.1: syntax error, unexpected end of file, " + "expecting }"); + + // includes + testError("<?\n", + Parser6Context::PARSER_JSON, + "Directive not closed."); + testError("<?include\n", + Parser6Context::PARSER_JSON, + "Directive not closed."); + string file = string(CFG_EXAMPLES) + "/" + "stateless.json"; + testError("<?include \"" + file + "\"\n", + Parser6Context::PARSER_JSON, + "Directive not closed."); + testError("<?include \"/foo/bar\" ?>/n", + Parser6Context::PARSER_JSON, + "Can't open include file /foo/bar"); + + // JSON keywords + testError("{ \"foo\": True }", + Parser6Context::PARSER_JSON, + "<string>:1.10-13: JSON true reserved keyword is lower case only"); + testError("{ \"foo\": False }", + Parser6Context::PARSER_JSON, + "<string>:1.10-14: JSON false reserved keyword is lower case only"); + testError("{ \"foo\": NULL }", + Parser6Context::PARSER_JSON, + "<string>:1.10-13: JSON null reserved keyword is lower case only"); + testError("{ \"foo\": Tru }", + Parser6Context::PARSER_JSON, + "<string>:1.10: Invalid character: T"); + testError("{ \"foo\": nul }", + Parser6Context::PARSER_JSON, + "<string>:1.10: Invalid character: n"); + + // numbers + testError("123", + Parser6Context::PARSER_DHCP6, + "<string>:1.1-3: syntax error, unexpected integer, " + "expecting {"); + testError("-456", + Parser6Context::PARSER_DHCP6, + "<string>:1.1-4: syntax error, unexpected integer, " + "expecting {"); + testError("-0001", + Parser6Context::PARSER_DHCP6, + "<string>:1.1-5: syntax error, unexpected integer, " + "expecting {"); + testError("1234567890123456789012345678901234567890", + Parser6Context::PARSER_JSON, + "<string>:1.1-40: Failed to convert " + "1234567890123456789012345678901234567890" + " to an integer."); + testError("-3.14e+0", + Parser6Context::PARSER_DHCP6, + "<string>:1.1-8: syntax error, unexpected floating point, " + "expecting {"); + testError("1e50000", + Parser6Context::PARSER_JSON, + "<string>:1.1-7: Failed to convert 1e50000 " + "to a floating point."); + + // strings + testError("\"aabb\"", + Parser6Context::PARSER_DHCP6, + "<string>:1.1-6: syntax error, unexpected constant string, " + "expecting {"); + testError("{ \"aabb\"err", + Parser6Context::PARSER_JSON, + "<string>:1.9: Invalid character: e"); + testError("{ err\"aabb\"", + Parser6Context::PARSER_JSON, + "<string>:1.3: Invalid character: e"); + testError("\"a\n\tb\"", + Parser6Context::PARSER_JSON, + "<string>:1.1-6 (near 2): Invalid control in \"a\n\tb\""); + testError("\"a\n\\u12\"", + Parser6Context::PARSER_JSON, + "<string>:1.1-8 (near 2): Invalid control in \"a\n\\u12\""); + testError("\"a\\n\\tb\"", + Parser6Context::PARSER_DHCP6, + "<string>:1.1-8: syntax error, unexpected constant string, " + "expecting {"); + testError("\"a\\x01b\"", + Parser6Context::PARSER_JSON, + "<string>:1.1-8 (near 3): Bad escape in \"a\\x01b\""); + testError("\"a\\u0162\"", + Parser6Context::PARSER_JSON, + "<string>:1.1-9 (near 4): Unsupported unicode escape " + "in \"a\\u0162\""); + testError("\"a\\u062z\"", + Parser6Context::PARSER_JSON, + "<string>:1.1-9 (near 3): Bad escape in \"a\\u062z\""); + testError("\"abc\\\"", + Parser6Context::PARSER_JSON, + "<string>:1.1-6 (near 6): Overflow escape in \"abc\\\""); + testError("\"a\\u006\"", + Parser6Context::PARSER_JSON, + "<string>:1.1-8 (near 3): Overflow unicode escape " + "in \"a\\u006\""); + testError("\"\\u\"", + Parser6Context::PARSER_JSON, + "<string>:1.1-4 (near 2): Overflow unicode escape in \"\\u\""); + testError("\"\\u\x02\"", + Parser6Context::PARSER_JSON, + "<string>:1.1-5 (near 2): Bad escape in \"\\u\x02\""); + testError("\"\\u\\\"foo\"", + Parser6Context::PARSER_JSON, + "<string>:1.1-5 (near 2): Bad escape in \"\\u\\\"..."); + testError("\"\x02\\u\"", + Parser6Context::PARSER_JSON, + "<string>:1.1-5 (near 1): Invalid control in \"\x02\\u\""); + + // from data_unittest.c + testError("\\a", + Parser6Context::PARSER_JSON, + "<string>:1.1: Invalid character: \\"); + testError("\\", + Parser6Context::PARSER_JSON, + "<string>:1.1: Invalid character: \\"); + testError("\\\"\\\"", + Parser6Context::PARSER_JSON, + "<string>:1.1: Invalid character: \\"); + + // want a map + testError("[]\n", + Parser6Context::PARSER_DHCP6, + "<string>:1.1: syntax error, unexpected [, " + "expecting {"); + testError("[]\n", + Parser6Context::PARSER_DHCP6, + "<string>:1.1: syntax error, unexpected [, " + "expecting {"); + testError("{ 123 }\n", + Parser6Context::PARSER_JSON, + "<string>:1.3-5: syntax error, unexpected integer, " + "expecting }"); + testError("{ 123 }\n", + Parser6Context::PARSER_DHCP6, + "<string>:1.3-5: syntax error, unexpected integer, " + "expecting Dhcp6"); + testError("{ \"foo\" }\n", + Parser6Context::PARSER_JSON, + "<string>:1.9: syntax error, unexpected }, " + "expecting :"); + testError("{ \"foo\" }\n", + Parser6Context::PARSER_DHCP6, + "<string>:1.3-7: syntax error, unexpected constant string, " + "expecting Dhcp6"); + testError("{ \"foo\":null }\n", + Parser6Context::PARSER_DHCP6, + "<string>:1.3-7: syntax error, unexpected constant string, " + "expecting Dhcp6"); + testError("{ \"Logging\":null }\n", + Parser6Context::PARSER_DHCP6, + "<string>:1.3-11: syntax error, unexpected constant string, " + "expecting Dhcp6"); + testError("{ \"Dhcp6\" }\n", + Parser6Context::PARSER_DHCP6, + "<string>:1.11: syntax error, unexpected }, " + "expecting :"); + testError("{}{}\n", + Parser6Context::PARSER_JSON, + "<string>:1.3: syntax error, unexpected {, " + "expecting end of file"); + + // duplicate in map + testError("{ \"foo\": 1, \"foo\": true }\n", + Parser6Context::PARSER_JSON, + "<string>:1:13: duplicate foo entries in " + "JSON map (previous at <string>:1:10)"); + + // bad commas + testError("{ , }\n", + Parser6Context::PARSER_JSON, + "<string>:1.3: syntax error, unexpected \",\", " + "expecting }"); + testError("{ , \"foo\":true }\n", + Parser6Context::PARSER_JSON, + "<string>:1.3: syntax error, unexpected \",\", " + "expecting }"); + + // bad type + testError("{ \"Dhcp6\":{\n" + " \"preferred-lifetime\":false }}\n", + Parser6Context::PARSER_DHCP6, + "<string>:2.24-28: syntax error, unexpected boolean, " + "expecting integer"); + + // unknown keyword + testError("{ \"Dhcp6\":{\n" + " \"preferred_lifetime\":600 }}\n", + Parser6Context::PARSER_DHCP6, + "<string>:2.2-21: got unexpected keyword " + "\"preferred_lifetime\" in Dhcp6 map."); + + // missing parameter + testError("{ \"name\": \"foo\",\n" + " \"code\": 123 }\n", + Parser6Context::PARSER_OPTION_DEF, + "missing parameter 'type' (<string>:1:1) " + "[option-def map between <string>:1:1 and <string>:2:15]"); + + // user context and embedded comments + testError("{ \"Dhcp6\":{\n" + " \"comment\": true,\n" + " \"preferred-lifetime\": 600 }}\n", + Parser6Context::PARSER_DHCP6, + "<string>:2.14-17: syntax error, unexpected boolean, " + "expecting constant string"); + + testError("{ \"Dhcp6\":{\n" + " \"user-context\": \"a comment\",\n" + " \"preferred-lifetime\": 600 }}\n", + Parser6Context::PARSER_DHCP6, + "<string>:2.19-29: syntax error, unexpected constant string, " + "expecting {"); + + testError("{ \"Dhcp6\":{\n" + " \"comment\": \"a comment\",\n" + " \"comment\": \"another one\",\n" + " \"preferred-lifetime\": 600 }}\n", + Parser6Context::PARSER_DHCP6, + "<string>:3.3-11: duplicate user-context/comment entries " + "(previous at <string>:2:3)"); + + testError("{ \"Dhcp6\":{\n" + " \"user-context\": { \"version\": 1 },\n" + " \"user-context\": { \"one\": \"only\" },\n" + " \"preferred-lifetime\": 600 }}\n", + Parser6Context::PARSER_DHCP6, + "<string>:3.3-16: duplicate user-context entries " + "(previous at <string>:2:19)"); + + testError("{ \"Dhcp6\":{\n" + " \"user-context\": { \"comment\": \"indirect\" },\n" + " \"comment\": \"a comment\",\n" + " \"preferred-lifetime\": 600 }}\n", + Parser6Context::PARSER_DHCP6, + "<string>:3.3-11: duplicate user-context/comment entries " + "(previous at <string>:2:19)"); + + // duplicate Dhcp6 entries + testError("{ \"Dhcp6\":{\n" + " \"comment\": \"first\" },\n" + " \"Dhcp6\":{\n" + " \"comment\": \"second\" }}\n", + Parser6Context::PARSER_DHCP6, + "<string>:3.3-9: syntax error, unexpected Dhcp6, expecting \",\" or }"); + + // duplicate of not string entries + testError("{ \"Dhcp6\":{\n" + " \"subnet6\": [],\n" + " \"subnet6\": [] }}\n", + Parser6Context::PARSER_DHCP6, + "<string>:3:2: duplicate subnet6 entries in " + "Dhcp6 map (previous at <string>:2:2)"); + + // duplicate of string entries + testError("{ \"data\": \"foo\",\n" + " \"data\": \"bar\" }\n", + Parser6Context::PARSER_OPTION_DATA, + "<string>:2:2: duplicate data entries in " + "option-data map (previous at <string>:1:11)"); +} + +// Check unicode escapes +TEST(ParserTest, unicodeEscapes) { + ConstElementPtr result; + string json; + + // check we can reread output + for (char c = -128; c < 127; ++c) { + string ins(" "); + ins[1] = c; + ConstElementPtr e(new StringElement(ins)); + json = e->str(); + ASSERT_NO_THROW( + try { + Parser6Context ctx; + result = ctx.parseString(json, Parser6Context::PARSER_JSON); + } catch (const std::exception &x) { + cout << "EXCEPTION: " << x.what() << endl; + throw; + }); + ASSERT_EQ(Element::string, result->getType()); + EXPECT_EQ(ins, result->stringValue()); + } +} + +// This test checks that all representations of a slash is recognized properly. +TEST(ParserTest, unicodeSlash) { + // check the 4 possible encodings of solidus '/' + ConstElementPtr result; + string json = "\"/\\/\\u002f\\u002F\""; + ASSERT_NO_THROW( + try { + Parser6Context ctx; + result = ctx.parseString(json, Parser6Context::PARSER_JSON); + } catch (const std::exception &x) { + cout << "EXCEPTION: " << x.what() << endl; + throw; + }); + ASSERT_EQ(Element::string, result->getType()); + EXPECT_EQ("////", result->stringValue()); +} + +/// @brief Load a file into a JSON element. +/// +/// @param fname The name of the file to load. +/// @param list The JSON element list to add the parsing result to. +void loadFile(const string& fname, ElementPtr list) { + Parser6Context ctx; + ElementPtr json; + EXPECT_NO_THROW(json = ctx.parseFile(fname, Parser6Context::PARSER_DHCP6)); + ASSERT_TRUE(json); + list->add(json); +} + +// This test checks that all map entries are in the example files. +TEST(ParserTest, mapEntries) { + // Type of keyword set. + typedef set<string> KeywordSet; + + // Get keywords from the syntax file (dhcp6_parser.yy). + ifstream syntax_file(SYNTAX_FILE); + EXPECT_TRUE(syntax_file.is_open()); + string line; + KeywordSet syntax_keys = { "user-context" }; + // Code setting the map entry. + const string pattern = "ctx.stack_.back()->set(\""; + while (getline(syntax_file, line)) { + // Skip comments. + size_t comment = line.find("//"); + if (comment <= pattern.size()) { + continue; + } + if (comment != string::npos) { + line.resize(comment); + } + // Search for the code pattern. + size_t key_begin = line.find(pattern); + if (key_begin == string::npos) { + continue; + } + // Extract keywords. + line = line.substr(key_begin + pattern.size()); + size_t key_end = line.find_first_of('"'); + EXPECT_NE(string::npos, key_end); + string keyword = line.substr(0, key_end); + // Ignore result when adding the keyword to the syntax keyword set. + static_cast<void>(syntax_keys.insert(keyword)); + } + syntax_file.close(); + + // Get keywords from the example files. + string sample_dir(CFG_EXAMPLES); + sample_dir += "/"; + ElementPtr sample_json = Element::createList(); + loadFile(sample_dir + "advanced.json", sample_json); + loadFile(sample_dir + "all-keys.json", sample_json); + loadFile(sample_dir + "duid.json", sample_json); + loadFile(sample_dir + "reservations.json", sample_json); + loadFile(sample_dir + "all-keys-netconf.json", sample_json); + KeywordSet sample_keys = { + "hosts-database", + "reservation-mode" + }; + // Recursively extract keywords. + static void (*extract)(ConstElementPtr, KeywordSet&) = + [] (ConstElementPtr json, KeywordSet& set) { + if (json->getType() == Element::list) { + // Handle lists. + for (auto elem : json->listValue()) { + extract(elem, set); + } + } else if (json->getType() == Element::map) { + // Handle maps. + for (auto elem : json->mapValue()) { + static_cast<void>(set.insert(elem.first)); + // Skip entries with free content. + if ((elem.first != "user-context") && + (elem.first != "parameters")) { + extract(elem.second, set); + } + } + } + }; + extract(sample_json, sample_keys); + + // Compare. + auto print_keys = [](const KeywordSet& keys) { + string s = "{"; + bool first = true; + for (auto key : keys) { + if (first) { + first = false; + s += " "; + } else { + s += ", "; + } + s += "\"" + key + "\""; + } + return (s + " }"); + }; + EXPECT_EQ(syntax_keys, sample_keys) + << "syntax has: " << print_keys(syntax_keys) << endl + << "sample has: " << print_keys(sample_keys) << endl; +} + +/// @brief Tests a duplicate entry. +/// +/// The entry was duplicated by adding a new <name>DDDD entry. +/// An error is expected, usually it is a duplicate but there are +/// a few syntax errors when the syntax allows only one parameter. +/// +/// @param json the JSON configuration with the duplicate entry. +void testDuplicate(ConstElementPtr json) { + string config = json->str(); + size_t where = config.find("DDDD"); + ASSERT_NE(string::npos, where); + string before = config.substr(0, where); + string after = config.substr(where + 4, string::npos); + Parser6Context ctx; + EXPECT_THROW(ctx.parseString(before + after, + Parser6Context::PARSER_DHCP6), + Dhcp6ParseError) << "config: " << config; +} + +// This test checks that duplicate entries make parsing to fail. +TEST(ParserTest, duplicateMapEntries) { + // Get the config to work with from the all keys file. + string sample_fname(CFG_EXAMPLES); + sample_fname += "/all-keys.json"; + Parser6Context ctx; + ElementPtr sample_json; + EXPECT_NO_THROW(sample_json = + ctx.parseFile(sample_fname, Parser6Context::PARSER_DHCP6)); + ASSERT_TRUE(sample_json); + + // Recursively check duplicates. + static void (*test)(ElementPtr, ElementPtr, size_t&) = + [] (ElementPtr config, ElementPtr json, size_t& cnt) { + if (json->getType() == Element::list) { + // Handle lists. + for (auto elem : json->listValue()) { + test(config, elem, cnt); + } + } else if (json->getType() == Element::map) { + // Handle maps. + for (auto elem : json->mapValue()) { + // Skip entries with free content. + if ((elem.first == "user-context") || + (elem.first == "parameters")) { + continue; + } + + // Perform tests. + string dup = elem.first + "DDDD"; + json->set(dup, elem.second); + testDuplicate(config); + json->remove(dup); + ++cnt; + + // Recursive call. + ElementPtr mutable_json = + boost::const_pointer_cast<Element>(elem.second); + ASSERT_TRUE(mutable_json); + test(config, mutable_json, cnt); + } + } + }; + size_t cnt = 0; + test(sample_json, sample_json, cnt); + cout << "checked " << cnt << " duplicated map entries\n"; +} + +/// @brief Test fixture for trailing commas. +class TrailingCommasTest : public isc::dhcp::test::LogContentTest { +public: + /// @brief Add a log entry. + /// + /// @param loc Location of the trailing comma. + void addLog(const string& loc) { + string log = "DHCP6_CONFIG_SYNTAX_WARNING configuration syntax "; + log += "warning: " + loc; + log += ": Extraneous comma. "; + log += "A piece of configuration may have been omitted."; + addString(log); + } +}; + +// Test that trailing commas are allowed. +TEST_F(TrailingCommasTest, tests) { + string txt(R"({ + "Dhcp6": { + "control-socket": { + "socket-name": "/tmp/kea-dhcp6-ctrl.sock", + "socket-type": "unix", + }, + "hooks-libraries": [ + { + "library": "/usr/local/lib/kea/hooks/libdhcp_dummy.so", + }, + ], + "interfaces-config": { + "interfaces": [ + "eth0", + ], + }, + "lease-database": { + "name": "/tmp/kea-dhcp6.csv", + "persist": true, + "type": "memfile", + }, + "loggers": [ + { + "debuglevel": 99, + "name": "kea-dhcp6", + "output_options": [ + { + "output": "stdout", + }, + ], + "severity": "DEBUG", + }, + ], + "multi-threading": { + "enable-multi-threading": false, + "packet-queue-size": 0, + "thread-pool-size": 0 + }, + "subnet6": [ + { + "pools": [ + { + "pool": "2001:db8:1::/64", + }, + ], + "subnet": "2001:db8:1::/64", + "id": 1, + }, + ], + }, +})"); + testParser(txt, Parser6Context::PARSER_DHCP6, false); + + addLog("<string>:5.28"); + addLog("<string>:9.63"); + addLog("<string>:10.8"); + addLog("<string>:14.15"); + addLog("<string>:15.8"); + addLog("<string>:20.24"); + addLog("<string>:28.31"); + addLog("<string>:29.12"); + addLog("<string>:31.28"); + addLog("<string>:32.8"); + addLog("<string>:43.38"); + addLog("<string>:44.12"); + addLog("<string>:47.16"); + addLog("<string>:48.8"); + addLog("<string>:49.6"); + addLog("<string>:50.4"); + EXPECT_TRUE(checkFile()); + + // Test with many consecutive commas. + boost::replace_all(txt, ",", ",,,,"); + testParser(txt, Parser6Context::PARSER_DHCP6, false); +} + +} // namespace test +} // namespace dhcp +} // namespace isc |