1 // Copyright (C) 2016-2019 Internet Systems Consortium, Inc. ("ISC")
2 //
3 // This Source Code Form is subject to the terms of the Mozilla Public
4 // License, v. 2.0. If a copy of the MPL was not distributed with this
5 // file, You can obtain one at http://mozilla.org/MPL/2.0/.
6 
7 #include <config.h>
8 
9 #include <stdint.h>
10 #include <cc/simple_parser.h>
11 #include <gtest/gtest.h>
12 
13 using namespace isc;
14 using namespace isc::data;
15 using namespace isc::asiolink;
16 using isc::dhcp::DhcpConfigError;
17 
18 /// This list defines required keywords.
19 const SimpleRequiredKeywords REQUIRED_KEYWORDS = { "foobar" };
20 
21 /// This table defines keywords and types.
22 const SimpleKeywords KEYWORDS = {
23     { "id",     Element::integer },
24     { "prefix", Element::string },
25     { "map",    Element::map },
26     { "any",    Element::any }
27 };
28 
29 /// This table defines sample default values. Although these are DHCPv6
30 /// specific, the mechanism is generic and can be used by any other component.
31 const SimpleDefaults SAMPLE_DEFAULTS = {
32     { "renew-timer",        Element::integer, "900" },
33     { "rebind-timer",       Element::integer, "1800" },
34     { "preferred-lifetime", Element::integer, "3600" },
35     { "valid-lifetime",     Element::integer, "7200" }
36 };
37 
38 /// This list defines parameters that can be inherited from one scope
39 /// to another. Although these are DHCPv6 specific, the mechanism is generic and
40 /// can be used by any other component.
41 const ParamsList SAMPLE_INHERITS = {
42     "renew-timer",
43     "rebind-timer",
44     "preferred-lifetime",
45     "valid-lifetime"
46 };
47 
48 /// @brief Simple Parser test fixture class
49 class SimpleParserTest : public ::testing::Test {
50 public:
51     /// @brief Checks if specified map has an integer parameter with expected value
52     ///
53     /// @param map map to be checked
54     /// @param param_name name of the parameter to be checked
55     /// @param exp_value expected value of the parameter.
checkIntegerValue(const ConstElementPtr & map,const std::string & param_name,int64_t exp_value)56     void checkIntegerValue(const ConstElementPtr& map, const std::string& param_name,
57                            int64_t exp_value) {
58 
59         // First check if the passed element is a map.
60         ASSERT_EQ(Element::map, map->getType());
61 
62         // Now try to get the element being checked
63         ConstElementPtr elem = map->get(param_name);
64         ASSERT_TRUE(elem);
65 
66         // Now check if it's indeed integer
67         ASSERT_EQ(Element::integer, elem->getType());
68 
69         // Finally, check if its value meets expectation.
70         EXPECT_EQ(exp_value, elem->intValue());
71     }
72 };
73 
74 class SimpleParserClassTest : public SimpleParser {
75 public:
76     /// @brief Instantiation of getAndConvert
77     ///
78     /// @param scope specified parameter will be extracted from this scope
79     /// @param name name of the parameter for error report
80     /// @return a bool value
getAsBool(ConstElementPtr scope,const std::string & name)81     bool getAsBool(ConstElementPtr scope, const std::string& name) {
82         return (getAndConvert<bool, toBool>(scope, name, "boolean"));
83     }
84 
85     /// @brief Convert to boolean
86     ///
87     /// @param str the string "false" or "true"
88     /// @return false for "false" and true for "true"
89     /// @thrown isc::OutOfRange if not "false" or "true'
toBool(const std::string & str)90     static bool toBool(const std::string& str) {
91         if (str == "false") {
92             return (false);
93         } else if (str == "true") {
94             return (true);
95         } else {
96             isc_throw(TypeError, "not a boolean: " << str);
97         }
98     }
99 };
100 
101 // This test checks if the checkRequired method works as expected.
TEST_F(SimpleParserTest,checkRequired)102 TEST_F(SimpleParserTest, checkRequired) {
103     ConstElementPtr empty = Element::fromJSON("{ }");
104     EXPECT_THROW(SimpleParser::checkRequired(REQUIRED_KEYWORDS, empty),
105                  DhcpConfigError);
106     ConstElementPtr other = Element::fromJSON("{ \"foo\": 1, \"bar\": 2 }");
107     EXPECT_THROW(SimpleParser::checkRequired(REQUIRED_KEYWORDS, other),
108                  DhcpConfigError);
109     ConstElementPtr good = Element::fromJSON("{ \"foobar\": 2 }");
110     EXPECT_NO_THROW(SimpleParser::checkRequired(REQUIRED_KEYWORDS, good));
111 }
112 
113 // This test checks if the checkKeywords method works as expected.
TEST_F(SimpleParserTest,checkKeywords)114 TEST_F(SimpleParserTest, checkKeywords) {
115     ConstElementPtr empty = Element::fromJSON("{ }");
116     EXPECT_NO_THROW(SimpleParser::checkKeywords(KEYWORDS, empty));
117     ConstElementPtr id = Element::fromJSON("{ \"id\": 1 }");
118     EXPECT_NO_THROW(SimpleParser::checkKeywords(KEYWORDS, id));
119     ConstElementPtr any = Element::fromJSON("{ \"any\": 1 }");
120     EXPECT_NO_THROW(SimpleParser::checkKeywords(KEYWORDS, any));
121     ConstElementPtr bad_id = Element::fromJSON("{ \"id\": true }");
122     EXPECT_THROW(SimpleParser::checkKeywords(KEYWORDS, bad_id),
123                  DhcpConfigError);
124     ConstElementPtr bad_prefix = Element::fromJSON("{ \"prefix\": 12 }");
125     EXPECT_THROW(SimpleParser::checkKeywords(KEYWORDS, bad_prefix),
126                  DhcpConfigError);
127     ConstElementPtr bad_map = Element::fromJSON("{ \"map\": [ ] }");
128     EXPECT_THROW(SimpleParser::checkKeywords(KEYWORDS, bad_map),
129                  DhcpConfigError);
130     ConstElementPtr spurious = Element::fromJSON("{ \"spurious\": 1 }");
131     EXPECT_THROW(SimpleParser::checkKeywords(KEYWORDS, spurious),
132                  DhcpConfigError);
133 
134     // Bad type has precedence.
135     ConstElementPtr bad = Element::fromJSON("{ \"spurious\": 1, \"id\": true }");
136     try {
137         SimpleParser::checkKeywords(KEYWORDS, bad);
138         ADD_FAILURE() << "expect exception";
139     } catch (const DhcpConfigError& ex) {
140         EXPECT_EQ("'id' parameter is not an integer", std::string(ex.what()));
141     } catch (...) {
142         ADD_FAILURE() << "expect DhcpConfigError";
143     }
144 }
145 
146 // This test checks if the parameters can be inherited from the global
147 // scope to the subnet scope.
TEST_F(SimpleParserTest,deriveParams)148 TEST_F(SimpleParserTest, deriveParams) {
149     ElementPtr global = Element::fromJSON("{ \"renew-timer\": 1,"
150                                           "  \"rebind-timer\": 2,"
151                                           "  \"preferred-lifetime\": 3,"
152                                           "  \"valid-lifetime\": 4"
153                                           "}");
154     ElementPtr subnet = Element::fromJSON("{ \"renew-timer\": 100 }");
155 
156     // we should inherit 3 parameters. Renew-timer should remain intact,
157     // as it was already defined in the subnet scope.
158     size_t num;
159     EXPECT_NO_THROW(num = SimpleParser::deriveParams(global, subnet,
160                                                      SAMPLE_INHERITS));
161     EXPECT_EQ(3, num);
162 
163     // Check the values. 3 of them are inherited, while the fourth one
164     // was already defined in the subnet, so should not be inherited.
165     checkIntegerValue(subnet, "renew-timer", 100);
166     checkIntegerValue(subnet, "rebind-timer", 2);
167     checkIntegerValue(subnet, "preferred-lifetime", 3);
168     checkIntegerValue(subnet, "valid-lifetime", 4);
169 }
170 
171 // This test checks if global defaults are properly set for DHCPv6.
TEST_F(SimpleParserTest,setDefaults)172 TEST_F(SimpleParserTest, setDefaults) {
173 
174     ElementPtr empty = Element::fromJSON("{ }");
175     size_t num = 0;
176 
177     EXPECT_NO_THROW(num = SimpleParser::setDefaults(empty, SAMPLE_DEFAULTS));
178 
179     // We expect at least 4 parameters to be inserted.
180     EXPECT_GE(num, 3);
181 
182     checkIntegerValue(empty, "valid-lifetime", 7200);
183     checkIntegerValue(empty, "preferred-lifetime", 3600);
184     checkIntegerValue(empty, "rebind-timer", 1800);
185     checkIntegerValue(empty, "renew-timer", 900);
186 }
187 
188 // This test checks if global defaults are properly set for DHCPv6.
TEST_F(SimpleParserTest,setListDefaults)189 TEST_F(SimpleParserTest, setListDefaults) {
190 
191     ElementPtr empty = Element::fromJSON("[{}, {}, {}]");
192     size_t num;
193 
194     EXPECT_NO_THROW(num = SimpleParser::setListDefaults(empty, SAMPLE_DEFAULTS));
195 
196     // We expect at least 12 parameters to be inserted (3 entries, with
197     // 4 parameters inserted in each)
198     EXPECT_EQ(12, num);
199 
200     ASSERT_EQ(Element::list, empty->getType());
201     ASSERT_EQ(3, empty->size());
202 
203     ConstElementPtr first = empty->get(0);
204     ConstElementPtr second = empty->get(1);
205     ConstElementPtr third = empty->get(2);
206 
207     checkIntegerValue(first, "valid-lifetime", 7200);
208     checkIntegerValue(first, "preferred-lifetime", 3600);
209     checkIntegerValue(first, "rebind-timer", 1800);
210     checkIntegerValue(first, "renew-timer", 900);
211 
212     checkIntegerValue(second, "valid-lifetime", 7200);
213     checkIntegerValue(second, "preferred-lifetime", 3600);
214     checkIntegerValue(second, "rebind-timer", 1800);
215     checkIntegerValue(second, "renew-timer", 900);
216 
217     checkIntegerValue(third, "valid-lifetime", 7200);
218     checkIntegerValue(third, "preferred-lifetime", 3600);
219     checkIntegerValue(third, "rebind-timer", 1800);
220     checkIntegerValue(third, "renew-timer", 900);
221 }
222 
223 // This test exercises the getIntType template
TEST_F(SimpleParserTest,getIntType)224 TEST_F(SimpleParserTest, getIntType) {
225 
226     SimpleParserClassTest parser;
227 
228     // getIntType checks it can be found
229     ElementPtr not_found = Element::fromJSON("{ \"bar\": 1 }");
230     EXPECT_THROW(parser.getUint8(not_found, "foo"), DhcpConfigError);
231 
232     // getIntType checks if it is an integer
233     ElementPtr not_int = Element::fromJSON("{ \"foo\": \"xyz\" }");
234     EXPECT_THROW(parser.getUint8(not_int, "foo"), DhcpConfigError);
235 
236     // getIntType checks bounds
237     ElementPtr negative = Element::fromJSON("{ \"foo\": -1 }");
238     EXPECT_THROW(parser.getUint8(negative, "foo"), DhcpConfigError);
239     ElementPtr too_large = Element::fromJSON("{ \"foo\": 1024 }");
240     EXPECT_THROW(parser.getUint8(too_large, "foo"), DhcpConfigError);
241 
242     // checks if getIntType can return the expected value
243     ElementPtr hundred = Element::fromJSON("{ \"foo\": 100 }");
244     uint8_t val = 0;
245     EXPECT_NO_THROW(val = parser.getUint8(hundred, "foo"));
246     EXPECT_EQ(100, val);
247 }
248 
249 // This test exercises the getInteger with range checking
TEST_F(SimpleParserTest,getInteger)250 TEST_F(SimpleParserTest, getInteger) {
251 
252     // The value specified is 100.
253     ElementPtr json = Element::fromJSON("{ \"bar\": 100 }");
254     int64_t x;
255 
256     // Positive case: we expect value in range 0..200. All ok.
257     EXPECT_NO_THROW(x = SimpleParser::getInteger(json, "bar", 0, 200));
258     EXPECT_EQ(100, x);
259 
260     // Border checks: 100 for 100..200 range is still ok.
261     EXPECT_NO_THROW(x = SimpleParser::getInteger(json, "bar", 100, 200));
262     // Border checks: 100 for 1..100 range is still ok.
263     EXPECT_NO_THROW(x = SimpleParser::getInteger(json, "bar", 1, 100));
264 
265     // Out of expected range. Should throw.
266     EXPECT_THROW(x = SimpleParser::getInteger(json, "bar", 101, 200), OutOfRange);
267     EXPECT_THROW(x = SimpleParser::getInteger(json, "bar", 1, 99), OutOfRange);
268 }
269 
270 // This test exercises the getAndConvert template
TEST_F(SimpleParserTest,getAndConvert)271 TEST_F(SimpleParserTest, getAndConvert) {
272 
273     SimpleParserClassTest parser;
274 
275     // getAndConvert checks it can be found
276     ElementPtr not_found = Element::fromJSON("{ \"bar\": \"true\" }");
277     EXPECT_THROW(parser.getAsBool(not_found, "foo"), DhcpConfigError);
278 
279     // getAndConvert checks if it is a string
280     ElementPtr not_bool = Element::fromJSON("{ \"foo\": 1 }");
281     EXPECT_THROW(parser.getAsBool(not_bool, "foo"), DhcpConfigError);
282 
283     // checks if getAndConvert can return the expected value
284     ElementPtr a_bool = Element::fromJSON("{ \"foo\": \"false\" }");
285     bool val = true;
286     EXPECT_NO_THROW(val = parser.getAsBool(a_bool, "foo"));
287     EXPECT_FALSE(val);
288 
289     // getAndConvert checks conversion
290     ElementPtr bad_bool = Element::fromJSON("{ \"foo\": \"bar\" }");
291     EXPECT_THROW(parser.getAsBool(bad_bool, "bar"), DhcpConfigError);
292 }
293 
294 // This test exercises the getIOAddress
TEST_F(SimpleParserTest,getIOAddress)295 TEST_F(SimpleParserTest, getIOAddress) {
296 
297     SimpleParserClassTest parser;
298 
299     // getAddress checks it can be found
300     ElementPtr not_found = Element::fromJSON("{ \"bar\": 1 }");
301     EXPECT_THROW(parser.getAddress(not_found, "foo"), DhcpConfigError);
302 
303     // getAddress checks if it is a string
304     ElementPtr not_addr = Element::fromJSON("{ \"foo\": 1234 }");
305     EXPECT_THROW(parser.getAddress(not_addr, "foo"), DhcpConfigError);
306 
307     // checks if getAddress can return the expected value of v4 address
308     ElementPtr v4 = Element::fromJSON("{ \"foo\": \"192.0.2.1\" }");
309     IOAddress val("::");
310     EXPECT_NO_THROW(val = parser.getAddress(v4, "foo"));
311     EXPECT_EQ("192.0.2.1" , val.toText());
312 
313     // checks if getAddress can return the expected value of v4 address
314     ElementPtr v6 = Element::fromJSON("{ \"foo\": \"2001:db8::1\" }");
315     EXPECT_NO_THROW(val = parser.getAddress(v6, "foo"));
316     EXPECT_EQ("2001:db8::1" , val.toText());
317 }
318 
319 // This test exercises getDouble()
TEST_F(SimpleParserTest,getDouble)320 TEST_F(SimpleParserTest, getDouble) {
321 
322     SimpleParserClassTest parser;
323     std::string json =
324     "{\n"
325     "  \"string\" : \"12.3\",\n"
326     "  \"bool\" : true, \n"
327     "  \"int\" : 777, \n"
328     "  \"map\" : {}, \n"
329     "  \"list\" : [], \n"
330     "  \"zero\" : 0.0, \n"
331     "  \"fraction\" : .75, \n"
332     "  \"negative\" : -1.45, \n"
333     "  \"positive\" : 346.7 \n"
334     "}\n";
335 
336     // Create our test set of parameters.
337     ElementPtr elems;
338     ASSERT_NO_THROW(elems = Element::fromJSON(json)) << " invalid JSON, test is broken";
339 
340     // Verify that a non-existant element is caught.
341     EXPECT_THROW(parser.getDouble(elems, "not-there"), DhcpConfigError);
342 
343     // Verify that wrong element types are caught.
344     EXPECT_THROW(parser.getDouble(elems, "string"), DhcpConfigError);
345     EXPECT_THROW(parser.getDouble(elems, "int"), DhcpConfigError);
346     EXPECT_THROW(parser.getDouble(elems, "bool"), DhcpConfigError);
347     EXPECT_THROW(parser.getDouble(elems, "map"), DhcpConfigError);
348     EXPECT_THROW(parser.getDouble(elems, "list"), DhcpConfigError);
349 
350     // Verify valid values are correct.
351     double value;
352 
353     EXPECT_NO_THROW(value = parser.getDouble(elems, "zero"));
354     EXPECT_EQ(0.0, value);
355 
356     EXPECT_NO_THROW(value = parser.getDouble(elems, "fraction"));
357     EXPECT_EQ(.75, value);
358 
359     EXPECT_NO_THROW(value = parser.getDouble(elems, "negative"));
360     EXPECT_EQ(-1.45, value);
361 
362     EXPECT_NO_THROW(value = parser.getDouble(elems, "positive"));
363     EXPECT_EQ(346.7, value);
364 }
365