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