// Copyright (C) 2017-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 #include #include #include #include #include using namespace isc::config; using namespace isc::data; namespace { /// @brief Test fixture class for @ref JSONFeed class. class JSONFeedTest : public ::testing::Test { public: /// @brief Constructor. /// /// Initializes @ref json_map_ and @ref json_list_ which hold reference /// JSON structures. JSONFeedTest() : json_map_(), json_list_() { ElementPtr m = Element::fromJSON(createJSON()); ElementPtr l = Element::createList(); l->add(m); json_map_ = m; json_list_ = l; } /// @brief Creates a JSON map holding 20 elements. /// /// Each map value is a list of 20 elements. std::string createJSON() const { // Create a list of 20 elements. ElementPtr list_element = Element::createList(); for (unsigned i = 0; i < 20; ++i) { std::ostringstream s; s << "list_element" << i; list_element->add(Element::create(s.str())); } // Create a map of 20 elements. Each map element holds a list // of 20 elements. ElementPtr map_element = Element::createMap(); for (unsigned i = 0; i < 20; ++i) { std::ostringstream s; s << "map_element" << i; map_element->set(s.str(), list_element); } return (prettyPrint(map_element)); } /// @brief Test that the JSONFeed correctly recognizes the beginning /// and the end of the JSON structure. /// /// @param input_json A string holding an input JSON structure. /// @param expected_output A structure holding expected output from the /// @ref JSONFeed::toElement. void testRead(const std::string& input_json, const ConstElementPtr& expected_output) { JSONFeed feed; ASSERT_NO_THROW(feed.initModel()); // Post the data into the feed in 10 bytes long chunks. size_t chunk = 10; for (size_t i = 0; i < input_json.size(); i += chunk) { bool done = false; // When we're near the end of the data stream, the chunk length may // vary. if (i + chunk >= input_json.size()) { chunk = input_json.size() - i; done = true; } // Feed the parser with a data chunk and parse it. feed.postBuffer(&input_json[i], chunk); feed.poll(); if (!done) { ASSERT_TRUE(feed.needData()); } } // Convert parsed/collected data in the feed into the structure of // elements. ConstElementPtr element_from_feed = feed.toElement(); EXPECT_TRUE(element_from_feed->equals(*expected_output)); } /// @brief Test that the JSONFeed correctly recognizes the beginning /// and the end of the JSON structure. /// /// @param input_json A string holding an input JSON structure. /// @param expected_output A string holding expected output from the /// @ref JSONFeed::getProcessedText. void testRead(const std::string& input_json, const std::string& expected_output) { JSONFeed feed; ASSERT_NO_THROW(feed.initModel()); // Post the data into the feed in 10 bytes long chunks. size_t chunk = 10; for (size_t i = 0; i < input_json.size(); i += chunk) { bool done = false; // When we're near the end of the data stream, the chunk length may // vary. if (i + chunk >= input_json.size()) { chunk = input_json.size() - i; done = true; } // Feed the parser with a data chunk and parse it. feed.postBuffer(&input_json[i], chunk); feed.poll(); if (!done) { ASSERT_TRUE(feed.needData()); } } EXPECT_EQ(expected_output, feed.getProcessedText()); } /// @brief Test that the @ref JSONFeed signals an error when the input /// string holds invalid data. /// /// @param input_json A string holding an input JSON structure. /// @param err_msg A string holding an expected error message. void testInvalidRead(const std::string& input_json, const std::string& err_msg) { JSONFeed feed; ASSERT_NO_THROW(feed.initModel()); ASSERT_NO_THROW(feed.postBuffer(&input_json[0], input_json.size())); ASSERT_NO_THROW(feed.poll()); EXPECT_FALSE(feed.needData()); EXPECT_FALSE(feed.feedOk()); EXPECT_EQ(err_msg, feed.getErrorMessage()); } /// @brief JSON map holding a number of lists. ConstElementPtr json_map_; /// @brief JSON list holding a map of lists. ConstElementPtr json_list_; }; // This test verifies that toElement should not be called before // the feed detects the end of the data stream. TEST_F(JSONFeedTest, toElementTooSoon) { JSONFeed feed; ASSERT_NO_THROW(feed.initModel()); std::string json = "{\n"; feed.postBuffer(&json[0], json.size()); feed.poll(); EXPECT_TRUE(feed.needData()); EXPECT_THROW(feed.toElement(), JSONFeedError); } // This test verifies that toElement checks JSON syntax as a side effect. TEST_F(JSONFeedTest, badJSON) { JSONFeed feed; ASSERT_NO_THROW(feed.initModel()); std::string json = "{\n]\n"; feed.postBuffer(&json[0], json.size()); feed.poll(); EXPECT_FALSE(feed.needData()); EXPECT_THROW(feed.toElement(), JSONFeedError); } // This test verifies that a JSON structure starting with '{' is accepted // and parsed. TEST_F(JSONFeedTest, startWithBrace) { std::string json = createJSON(); testRead(json, json_map_); } // This test verifies that a JSON structure starting with '[' is accepted // and parsed. TEST_F(JSONFeedTest, startWithSquareBracket) { std::string json = createJSON(); json = std::string("[") + json + std::string("]"); testRead(json, json_list_); } // This test verifies that input JSON can be preceded with whitespaces. TEST_F(JSONFeedTest, startWithWhitespace) { std::string json = createJSON(); json = std::string(" \r\r\t ") + json; testRead(json, json_map_); } // This test verifies that an empty map is accepted and parsed. TEST_F(JSONFeedTest, emptyMap) { std::string json = "{}"; testRead(json, Element::createMap()); } // This test verifies that an empty list is accepted and parsed. TEST_F(JSONFeedTest, emptyList) { std::string json = "[ ]"; testRead(json, Element::createList()); } // This test verifies that an error is signalled when a JSON structure // is preceded by invalid character. TEST_F(JSONFeedTest, unexpectedFirstCharacter) { std::string json = "a {}"; std::string err_msg = "invalid first character a"; testInvalidRead(json, err_msg); } // This test verifies that an error is signalled when a JSON structure // is preceded by white spaces and an invalid character. TEST_F(JSONFeedTest, unexpectedCharacter) { std::string json = " a {}"; std::string err_msg = "invalid character a"; testInvalidRead(json, err_msg); } // This test verifies that an error is signalled when the JSON structure // begins by a string. TEST_F(JSONFeedTest, stringFirst) { std::string json = "\"foo\""; std::string err_msg = "invalid first character \""; testInvalidRead(json, err_msg); } // This test verifies that an error is signalled when the JSON structure // begins by white spaces followed by a string. TEST_F(JSONFeedTest, stringBefore) { std::string json = " \"foo\""; std::string err_msg = "invalid character \""; testInvalidRead(json, err_msg); } // This test verifies that an error is signalled when a JSON structure // lacks an opening brace character. TEST_F(JSONFeedTest, noOpeningBrace) { std::string json = "\"x\": \"y\" }"; std::string err_msg = "invalid first character \""; testInvalidRead(json, err_msg); } // This test verifies that an error is signalled when a JSON structure // lacks an opening square bracket. TEST_F(JSONFeedTest, noOpeningSquareBracket) { std::string json = "1, 2 ]"; std::string err_msg = "invalid first character 1"; testInvalidRead(json, err_msg); } // This test verifies that a string is correctly handled TEST_F(JSONFeedTest, string) { std::string json = "{ \"braces\": \"}}}}\" }"; ElementPtr expected = Element::createMap(); expected->set("braces", Element::create("}}}}")); testRead(json, expected); } // This test verifies that a string with escapes is correctly handled TEST_F(JSONFeedTest, escape) { std::string json = "{ \"escapes\": \"\\n\\t\\\"\\\\\" }"; ElementPtr expected = Element::createMap(); expected->set("escapes", Element::create("\n\t\"\\")); testRead(json, expected); } // This test verifies that white spaces before JSON are ignored. TEST_F(JSONFeedTest, whiteSpaceBefore) { std::string json = " \n [ ]\n"; std::string expected = "[ ]"; testRead(json, expected); } // This test verifies that bash style comments before JSON are ignored. TEST_F(JSONFeedTest, bashCommentBefore) { std::string json = "# ahah\n # foo\"bar\n{ }\n"; std::string expected = "{ }"; testRead(json, expected); } // This test verifies that C++ style comments before JSON are ignored. TEST_F(JSONFeedTest, cppCommentBefore) { std::string json = "// ahah\n // foo\"bar\n[ 12 ]\n"; std::string expected = "[ 12 ]"; testRead(json, expected); } // This test verifies that multi-line comments before JSON are ignored. TEST_F(JSONFeedTest, multiLineCommentBefore) { std::string json = "/* ahah\n \"// foo*bar**/\n { \"foo\": \"bar\" }\n"; std::string expected = "{ \"foo\": \"bar\" }"; testRead(json, expected); } // This test verifies that an error is signalled when a slash does not // begin a C++ or C style comment before JSON. TEST_F(JSONFeedTest, badCommentBefore) { std::string json = "/# foo\n [ ]\n"; std::string err_msg = "invalid characters /#"; testInvalidRead(json, err_msg); } // This test verifies that bash style comments are ignored. TEST_F(JSONFeedTest, bashComments) { std::string json = "{ # ahah\n \"foo\": # value?\n \"bar\" }"; std::string expected = "{ \n \"foo\": \n \"bar\" }"; testRead(json, expected); } // This test verifies that C++ style comments are ignored. TEST_F(JSONFeedTest, cppComments) { std::string json = "[ // ahah\n \"foo\", /// value?\n \"bar\" ]"; std::string expected = "[ \n \"foo\", \n \"bar\" ]"; testRead(json, expected); } // This test verifies that multi-line comments are ignored. TEST_F(JSONFeedTest, multiLineComments) { std::string json = "{ /* ahah\n \"// foo*bar**/\n \"foo\": \"bar\" }\n"; std::string expected = "{ \n\n \"foo\": \"bar\" }"; testRead(json, expected); } // This test verifies that an error is signalled a slash does not begin // a C++ or C style comment. TEST_F(JSONFeedTest, badComment) { std::string json = "[ /# foo\n ]\n"; std::string err_msg = "invalid characters /#"; testInvalidRead(json, err_msg); } // This test verifies that trailing garbage is ignored. TEST_F(JSONFeedTest, trailing) { JSONFeed feed; ASSERT_NO_THROW(feed.initModel()); std::string json = "[ 1, 2] 3, 4]"; feed.postBuffer(&json[0], json.size()); feed.poll(); EXPECT_FALSE(feed.needData()); EXPECT_TRUE(feed.feedOk()); std::string expected = "[ 1, 2]"; EXPECT_EQ(expected, feed.getProcessedText()); } // Example from DHCPv4 unit tests. TEST_F(JSONFeedTest, bashComment4) { JSONFeed feed; ASSERT_NO_THROW(feed.initModel()); std::string json = "{ \"Dhcp4\": { \"interfaces-config\": {" " \"interfaces\": [ \"*\" ]" "},\n" "# this is a comment\n" "\"rebind-timer\": 2000, \n" "# lots of comments here\n" "# and here\n" "\"renew-timer\": 1000, \n" "\"subnet4\": [ { " " \"pools\": [ { \"pool\": \"192.0.2.1 - 192.0.2.100\" } ]," " \"subnet\": \"192.0.2.0/24\", " " \"id\": 1, " " \"interface\": \"eth0\"" " } ]," "\"valid-lifetime\": 4000 } }"; feed.postBuffer(&json[0], json.size()); feed.poll(); EXPECT_FALSE(feed.needData()); EXPECT_TRUE(feed.feedOk()); EXPECT_NO_THROW(feed.toElement()); } // Example from DHCPv4 unit tests. TEST_F(JSONFeedTest, bashCommentsInline4) { JSONFeed feed; ASSERT_NO_THROW(feed.initModel()); std::string json = "{ \"Dhcp4\": { \"interfaces-config\": {" " \"interfaces\": [ \"*\" ]" "},\n" "\"rebind-timer\": 2000, # everything after # is ignored\n" "\"renew-timer\": 1000, # this will be ignored, too\n" "\"subnet4\": [ { " " \"pools\": [ { \"pool\": \"192.0.2.1 - 192.0.2.100\" } ]," " \"subnet\": \"192.0.2.0/24\", " " \"id\": 1, " " \"interface\": \"eth0\"" " } ]," "\"valid-lifetime\": 4000 } }"; feed.postBuffer(&json[0], json.size()); feed.poll(); EXPECT_FALSE(feed.needData()); EXPECT_TRUE(feed.feedOk()); EXPECT_NO_THROW(feed.toElement()); } // Example from DHCPv6 unit tests. TEST_F(JSONFeedTest, cppComments6) { JSONFeed feed; ASSERT_NO_THROW(feed.initModel()); std::string json = "{ \"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\": [ { " " \"pools\": [ { \"pool\": \"2001:db8:1::/64\" } ]," " \"subnet\": \"2001:db8:1::/48\", " " \"id\": 1, " " \"interface\": \"eth0\"" " } ]," "\"valid-lifetime\": 4000 } }"; feed.postBuffer(&json[0], json.size()); feed.poll(); EXPECT_FALSE(feed.needData()); EXPECT_TRUE(feed.feedOk()); EXPECT_NO_THROW(feed.toElement()); } // Example from DHCPv6 unit tests. TEST_F(JSONFeedTest, multilineComments6) { JSONFeed feed; ASSERT_NO_THROW(feed.initModel()); std::string json = "{ \"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\": [ { " " \"pools\": [ { \"pool\": \"2001:db8:1::/64\" } ]," " \"subnet\": \"2001:db8:1::/48\", " " \"id\": 1, " " \"interface\": \"eth0\"" " } ]," "\"valid-lifetime\": 4000 } }"; feed.postBuffer(&json[0], json.size()); feed.poll(); EXPECT_FALSE(feed.needData()); EXPECT_TRUE(feed.feedOk()); EXPECT_NO_THROW(feed.toElement()); } } // end of anonymous namespace.