1 #include "bmcweb_config.h"
2 
3 #include "error_messages.hpp"
4 #include "http_response.hpp"
5 #include "utils/query_param.hpp"
6 
7 #include <boost/beast/http/status.hpp>
8 #include <boost/system/result.hpp>
9 #include <boost/url/parse.hpp>
10 #include <boost/url/url_view.hpp>
11 #include <nlohmann/json.hpp>
12 
13 #include <array>
14 #include <optional>
15 #include <span>
16 #include <string>
17 #include <utility>
18 #include <vector>
19 
20 #include <gmock/gmock.h>
21 #include <gtest/gtest.h>
22 
23 namespace redfish::query_param
24 {
25 namespace
26 {
27 
28 using ::testing::UnorderedElementsAre;
29 
TEST(Delegate,OnlyPositive)30 TEST(Delegate, OnlyPositive)
31 {
32     Query query{
33         .isOnly = true,
34     };
35     QueryCapabilities capabilities{
36         .canDelegateOnly = true,
37     };
38     Query delegated = delegate(capabilities, query);
39     EXPECT_TRUE(delegated.isOnly);
40     EXPECT_FALSE(query.isOnly);
41 }
42 
TEST(Delegate,ExpandPositive)43 TEST(Delegate, ExpandPositive)
44 {
45     Query query{
46         .isOnly = false,
47         .expandLevel = 5,
48         .expandType = ExpandType::Both,
49     };
50     QueryCapabilities capabilities{
51         .canDelegateExpandLevel = 3,
52     };
53     Query delegated = delegate(capabilities, query);
54     EXPECT_FALSE(delegated.isOnly);
55     EXPECT_EQ(delegated.expandLevel, capabilities.canDelegateExpandLevel);
56     EXPECT_EQ(delegated.expandType, ExpandType::Both);
57     EXPECT_EQ(query.expandLevel, 5);
58 }
59 
TEST(Delegate,OnlyNegative)60 TEST(Delegate, OnlyNegative)
61 {
62     Query query{
63         .isOnly = true,
64     };
65     QueryCapabilities capabilities{
66         .canDelegateOnly = false,
67     };
68     Query delegated = delegate(capabilities, query);
69     EXPECT_FALSE(delegated.isOnly);
70     EXPECT_EQ(query.isOnly, true);
71 }
72 
TEST(Delegate,ExpandNegative)73 TEST(Delegate, ExpandNegative)
74 {
75     Query query{
76         .isOnly = false,
77         .expandType = ExpandType::None,
78     };
79     Query delegated = delegate(QueryCapabilities{}, query);
80     EXPECT_EQ(delegated.expandType, ExpandType::None);
81 }
82 
TEST(Delegate,TopNegative)83 TEST(Delegate, TopNegative)
84 {
85     Query query{
86         .top = 42,
87     };
88     Query delegated = delegate(QueryCapabilities{}, query);
89     EXPECT_EQ(delegated.top, std::nullopt);
90     EXPECT_EQ(query.top, 42);
91 }
92 
TEST(Delegate,TopPositive)93 TEST(Delegate, TopPositive)
94 {
95     Query query{
96         .top = 42,
97     };
98     QueryCapabilities capabilities{
99         .canDelegateTop = true,
100     };
101     Query delegated = delegate(capabilities, query);
102     EXPECT_EQ(delegated.top, 42);
103     EXPECT_EQ(query.top, std::nullopt);
104 }
105 
TEST(Delegate,SkipNegative)106 TEST(Delegate, SkipNegative)
107 {
108     Query query{
109         .skip = 42,
110     };
111     Query delegated = delegate(QueryCapabilities{}, query);
112     EXPECT_EQ(delegated.skip, std::nullopt);
113     EXPECT_EQ(query.skip, 42);
114 }
115 
TEST(Delegate,SkipPositive)116 TEST(Delegate, SkipPositive)
117 {
118     Query query{
119         .skip = 42,
120     };
121     QueryCapabilities capabilities{
122         .canDelegateSkip = true,
123     };
124     Query delegated = delegate(capabilities, query);
125     EXPECT_EQ(delegated.skip, 42);
126     EXPECT_EQ(query.skip, 0);
127 }
128 
TEST(FormatQueryForExpand,NoSubQueryWhenQueryIsEmpty)129 TEST(FormatQueryForExpand, NoSubQueryWhenQueryIsEmpty)
130 {
131     EXPECT_EQ(formatQueryForExpand(Query{}), "");
132 }
133 
TEST(FormatQueryForExpand,NoSubQueryWhenExpandLevelsLeOne)134 TEST(FormatQueryForExpand, NoSubQueryWhenExpandLevelsLeOne)
135 {
136     EXPECT_EQ(formatQueryForExpand(
137                   Query{.expandLevel = 1, .expandType = ExpandType::Both}),
138               "");
139     EXPECT_EQ(formatQueryForExpand(Query{.expandType = ExpandType::Links}), "");
140     EXPECT_EQ(formatQueryForExpand(Query{.expandType = ExpandType::NotLinks}),
141               "");
142 }
143 
TEST(FormatQueryForExpand,NoSubQueryWhenExpandTypeIsNone)144 TEST(FormatQueryForExpand, NoSubQueryWhenExpandTypeIsNone)
145 {
146     EXPECT_EQ(formatQueryForExpand(
147                   Query{.expandLevel = 2, .expandType = ExpandType::None}),
148               "");
149 }
150 
TEST(FormatQueryForExpand,DelegatedSubQueriesHaveSameTypeAndOneLessLevels)151 TEST(FormatQueryForExpand, DelegatedSubQueriesHaveSameTypeAndOneLessLevels)
152 {
153     EXPECT_EQ(formatQueryForExpand(
154                   Query{.expandLevel = 3, .expandType = ExpandType::Both}),
155               "?$expand=*($levels=2)");
156     EXPECT_EQ(formatQueryForExpand(
157                   Query{.expandLevel = 4, .expandType = ExpandType::Links}),
158               "?$expand=~($levels=3)");
159     EXPECT_EQ(formatQueryForExpand(
160                   Query{.expandLevel = 2, .expandType = ExpandType::NotLinks}),
161               "?$expand=.($levels=1)");
162 }
163 
TEST(IsSelectedPropertyAllowed,NotAllowedCharactersReturnsFalse)164 TEST(IsSelectedPropertyAllowed, NotAllowedCharactersReturnsFalse)
165 {
166     EXPECT_FALSE(isSelectedPropertyAllowed("?"));
167     EXPECT_FALSE(isSelectedPropertyAllowed("!"));
168     EXPECT_FALSE(isSelectedPropertyAllowed("-"));
169     EXPECT_FALSE(isSelectedPropertyAllowed("/"));
170 }
171 
TEST(IsSelectedPropertyAllowed,EmptyStringReturnsFalse)172 TEST(IsSelectedPropertyAllowed, EmptyStringReturnsFalse)
173 {
174     EXPECT_FALSE(isSelectedPropertyAllowed(""));
175 }
176 
TEST(IsSelectedPropertyAllowed,TooLongStringReturnsFalse)177 TEST(IsSelectedPropertyAllowed, TooLongStringReturnsFalse)
178 {
179     std::string strUnderTest = "ab";
180     // 2^10
181     for (int i = 0; i < 10; ++i)
182     {
183         strUnderTest += strUnderTest;
184     }
185     EXPECT_FALSE(isSelectedPropertyAllowed(strUnderTest));
186 }
187 
TEST(IsSelectedPropertyAllowed,ValidPropertReturnsTrue)188 TEST(IsSelectedPropertyAllowed, ValidPropertReturnsTrue)
189 {
190     EXPECT_TRUE(isSelectedPropertyAllowed("Chassis"));
191     EXPECT_TRUE(isSelectedPropertyAllowed("@odata.type"));
192     EXPECT_TRUE(isSelectedPropertyAllowed("#ComputerSystem.Reset"));
193     EXPECT_TRUE(isSelectedPropertyAllowed(
194         "BootSourceOverrideTarget@Redfish.AllowableValues"));
195 }
196 
TEST(GetSelectParam,EmptyValueReturnsError)197 TEST(GetSelectParam, EmptyValueReturnsError)
198 {
199     Query query;
200     EXPECT_FALSE(getSelectParam("", query));
201 }
202 
TEST(GetSelectParam,EmptyPropertyReturnsError)203 TEST(GetSelectParam, EmptyPropertyReturnsError)
204 {
205     Query query;
206     EXPECT_FALSE(getSelectParam(",", query));
207     EXPECT_FALSE(getSelectParam(",,", query));
208 }
209 
TEST(GetSelectParam,InvalidPathPropertyReturnsError)210 TEST(GetSelectParam, InvalidPathPropertyReturnsError)
211 {
212     Query query;
213     EXPECT_FALSE(getSelectParam("\0,\0", query));
214     EXPECT_FALSE(getSelectParam("%%%", query));
215 }
216 
TEST(GetSelectParam,TrieNodesRespectAllProperties)217 TEST(GetSelectParam, TrieNodesRespectAllProperties)
218 {
219     Query query;
220     ASSERT_TRUE(getSelectParam("foo/bar,bar", query));
221     ASSERT_FALSE(query.selectTrie.root.empty());
222 
223     const SelectTrieNode* child = query.selectTrie.root.find("foo");
224     ASSERT_NE(child, nullptr);
225     EXPECT_FALSE(child->isSelected());
226     ASSERT_NE(child->find("bar"), nullptr);
227     EXPECT_TRUE(child->find("bar")->isSelected());
228 
229     ASSERT_NE(query.selectTrie.root.find("bar"), nullptr);
230     EXPECT_TRUE(query.selectTrie.root.find("bar")->isSelected());
231 }
232 
getTrie(std::span<std::string_view> properties)233 SelectTrie getTrie(std::span<std::string_view> properties)
234 {
235     SelectTrie trie;
236     for (const auto& property : properties)
237     {
238         EXPECT_TRUE(trie.insertNode(property));
239     }
240     return trie;
241 }
242 
TEST(RecursiveSelect,ExpectedKeysAreSelectInSimpleObject)243 TEST(RecursiveSelect, ExpectedKeysAreSelectInSimpleObject)
244 {
245     std::vector<std::string_view> properties = {"SelectMe"};
246     SelectTrie trie = getTrie(properties);
247     nlohmann::json root = R"({"SelectMe" : "foo", "OmitMe" : "bar"})"_json;
248     nlohmann::json expected = R"({"SelectMe" : "foo"})"_json;
249     recursiveSelect(root, trie.root);
250     EXPECT_EQ(root, expected);
251 }
252 
TEST(RecursiveSelect,ExpectedKeysAreSelectInNestedObject)253 TEST(RecursiveSelect, ExpectedKeysAreSelectInNestedObject)
254 {
255     std::vector<std::string_view> properties = {
256         "SelectMe", "Prefix0/ExplicitSelectMe", "Prefix1", "Prefix2",
257         "Prefix4/ExplicitSelectMe"};
258     SelectTrie trie = getTrie(properties);
259     nlohmann::json root = R"(
260 {
261   "SelectMe":[
262     "foo"
263   ],
264   "OmitMe":"bar",
265   "Prefix0":{
266     "ExplicitSelectMe":"123",
267     "OmitMe":"456"
268   },
269   "Prefix1":{
270     "ImplicitSelectMe":"123"
271   },
272   "Prefix2":[
273     {
274       "ImplicitSelectMe":"123"
275     }
276   ],
277   "Prefix3":[
278     "OmitMe"
279   ],
280   "Prefix4":[
281     {
282       "ExplicitSelectMe":"123",
283       "OmitMe": "456"
284     }
285   ]
286 }
287 )"_json;
288     nlohmann::json expected = R"(
289 {
290   "SelectMe":[
291     "foo"
292   ],
293   "Prefix0":{
294     "ExplicitSelectMe":"123"
295   },
296   "Prefix1":{
297     "ImplicitSelectMe":"123"
298   },
299   "Prefix2":[
300     {
301       "ImplicitSelectMe":"123"
302     }
303   ],
304   "Prefix4":[
305     {
306       "ExplicitSelectMe":"123"
307     }
308   ]
309 }
310 )"_json;
311     recursiveSelect(root, trie.root);
312     EXPECT_EQ(root, expected);
313 }
314 
TEST(RecursiveSelect,ReservedPropertiesAreSelected)315 TEST(RecursiveSelect, ReservedPropertiesAreSelected)
316 {
317     nlohmann::json root = R"(
318 {
319   "OmitMe":"bar",
320   "@odata.id":1,
321   "@odata.type":2,
322   "@odata.context":3,
323   "@odata.etag":4,
324   "Prefix1":{
325     "OmitMe":"bar",
326     "@odata.id":1,
327     "ExplicitSelectMe": 1
328   },
329   "Prefix2":[1, 2, 3],
330   "Prefix3":[
331     {
332       "OmitMe":"bar",
333       "@odata.id":1,
334       "ExplicitSelectMe": 1
335     }
336   ]
337 }
338 )"_json;
339     nlohmann::json expected = R"(
340 {
341   "@odata.id":1,
342   "@odata.type":2,
343   "@odata.context":3,
344   "@odata.etag":4,
345   "Prefix1":{
346     "@odata.id":1,
347     "ExplicitSelectMe": 1
348   },
349   "Prefix3":[
350     {
351       "@odata.id":1,
352       "ExplicitSelectMe": 1
353     }
354   ]
355 }
356 )"_json;
357     auto ret = boost::urls::parse_relative_ref(
358         "/redfish/v1?$select=Prefix1/ExplicitSelectMe,Prefix3/ExplicitSelectMe");
359     ASSERT_TRUE(ret);
360     crow::Response res;
361     std::optional<Query> query = parseParameters(ret->params(), res);
362     ASSERT_TRUE(query);
363     if (!query)
364     {
365         return;
366     }
367     recursiveSelect(root, query.value().selectTrie.root);
368     EXPECT_EQ(root, expected);
369 }
370 
371 TEST(PropogateErrorCode, 500IsWorst)
372 {
373     constexpr std::array<unsigned, 7> codes = {100, 200, 300, 400,
374                                                401, 500, 501};
375     for (auto code : codes)
376     {
377         EXPECT_EQ(propogateErrorCode(500, code), 500);
378         EXPECT_EQ(propogateErrorCode(code, 500), 500);
379     }
380 }
381 
382 TEST(PropogateErrorCode, 5xxAreWorseThanOthers)
383 {
384     constexpr std::array<unsigned, 7> codes = {100, 200, 300, 400,
385                                                401, 501, 502};
386     for (auto code : codes)
387     {
388         EXPECT_EQ(propogateErrorCode(code, 505), 505);
389         EXPECT_EQ(propogateErrorCode(505, code), 505);
390     }
391     EXPECT_EQ(propogateErrorCode(502, 501), 502);
392     EXPECT_EQ(propogateErrorCode(501, 502), 502);
393     EXPECT_EQ(propogateErrorCode(503, 502), 503);
394 }
395 
396 TEST(PropogateErrorCode, 401IsWorseThanOthers)
397 {
398     constexpr std::array<unsigned, 7> codes = {100, 200, 300, 400, 401};
399     for (auto code : codes)
400     {
401         EXPECT_EQ(propogateErrorCode(code, 401), 401);
402         EXPECT_EQ(propogateErrorCode(401, code), 401);
403     }
404 }
405 
406 TEST(PropogateErrorCode, 4xxIsWorseThanOthers)
407 {
408     constexpr std::array<unsigned, 7> codes = {100, 200, 300, 400, 402};
409     for (auto code : codes)
410     {
411         EXPECT_EQ(propogateErrorCode(code, 405), 405);
412         EXPECT_EQ(propogateErrorCode(405, code), 405);
413     }
414     EXPECT_EQ(propogateErrorCode(400, 402), 402);
415     EXPECT_EQ(propogateErrorCode(402, 403), 403);
416     EXPECT_EQ(propogateErrorCode(403, 402), 403);
417 }
418 
TEST(PropogateError,IntermediateNoErrorMessageMakesNoChange)419 TEST(PropogateError, IntermediateNoErrorMessageMakesNoChange)
420 {
421     crow::Response intermediate;
422     intermediate.result(boost::beast::http::status::ok);
423 
424     crow::Response finalRes;
425     finalRes.result(boost::beast::http::status::ok);
426     propogateError(finalRes, intermediate);
427     EXPECT_EQ(finalRes.result(), boost::beast::http::status::ok);
428     EXPECT_EQ(finalRes.jsonValue.find("error"), finalRes.jsonValue.end());
429 }
430 
TEST(PropogateError,ErrorsArePropergatedWithErrorInRoot)431 TEST(PropogateError, ErrorsArePropergatedWithErrorInRoot)
432 {
433     nlohmann::json root = R"(
434 {
435     "@odata.type": "#Message.v1_1_1.Message",
436     "Message": "The request failed due to an internal service error.  The service is still operational.",
437     "MessageArgs": [],
438     "MessageId": "Base.1.13.0.InternalError",
439     "MessageSeverity": "Critical",
440     "Resolution": "Resubmit the request.  If the problem persists, consider resetting the service."
441 }
442 )"_json;
443     crow::Response intermediate;
444     intermediate.result(boost::beast::http::status::internal_server_error);
445     intermediate.jsonValue = root;
446 
447     crow::Response final;
448     final.result(boost::beast::http::status::ok);
449 
450     propogateError(final, intermediate);
451 
452     EXPECT_EQ(final.jsonValue["error"]["code"].get<std::string>(),
453               "Base.1.13.0.InternalError");
454     EXPECT_EQ(
455         final.jsonValue["error"]["message"].get<std::string>(),
456         "The request failed due to an internal service error.  The service is still operational.");
457     EXPECT_EQ(intermediate.jsonValue, R"({})"_json);
458     EXPECT_EQ(final.result(),
459               boost::beast::http::status::internal_server_error);
460 }
461 
TEST(PropogateError,ErrorsArePropergatedWithErrorCode)462 TEST(PropogateError, ErrorsArePropergatedWithErrorCode)
463 {
464     crow::Response intermediate;
465     intermediate.result(boost::beast::http::status::internal_server_error);
466 
467     nlohmann::json error = R"(
468 {
469     "error": {
470         "@Message.ExtendedInfo": [],
471         "code": "Base.1.13.0.InternalError",
472         "message": "The request failed due to an internal service error.  The service is still operational."
473     }
474 }
475 )"_json;
476     nlohmann::json extendedInfo = R"(
477 {
478     "@odata.type": "#Message.v1_1_1.Message",
479     "Message": "The request failed due to an internal service error.  The service is still operational.",
480     "MessageArgs": [],
481     "MessageId": "Base.1.13.0.InternalError",
482     "MessageSeverity": "Critical",
483     "Resolution": "Resubmit the request.  If the problem persists, consider resetting the service."
484 }
485 )"_json;
486 
487     for (int i = 0; i < 10; ++i)
488     {
489         error["error"][messages::messageAnnotation].push_back(extendedInfo);
490     }
491     intermediate.jsonValue = error;
492     crow::Response final;
493     final.result(boost::beast::http::status::ok);
494 
495     propogateError(final, intermediate);
496     EXPECT_EQ(final.jsonValue["error"][messages::messageAnnotation],
497               error["error"][messages::messageAnnotation]);
498     std::string errorCode = messages::messageVersionPrefix;
499     errorCode += "GeneralError";
500     std::string errorMessage =
501         "A general error has occurred. See Resolution for "
502         "information on how to resolve the error.";
503     EXPECT_EQ(final.jsonValue["error"]["code"].get<std::string>(), errorCode);
504     EXPECT_EQ(final.jsonValue["error"]["message"].get<std::string>(),
505               errorMessage);
506     EXPECT_EQ(intermediate.jsonValue, R"({})"_json);
507     EXPECT_EQ(final.result(),
508               boost::beast::http::status::internal_server_error);
509 }
510 
TEST(QueryParams,ParseParametersOnly)511 TEST(QueryParams, ParseParametersOnly)
512 {
513     auto ret = boost::urls::parse_relative_ref("/redfish/v1?only");
514     ASSERT_TRUE(ret);
515     if (!ret)
516     {
517         return;
518     }
519 
520     crow::Response res;
521     std::optional<Query> query = parseParameters(ret->params(), res);
522     ASSERT_TRUE(query);
523     if (!query)
524     {
525         return;
526     }
527     EXPECT_TRUE(query->isOnly);
528 }
529 
TEST(QueryParams,ParseParametersExpand)530 TEST(QueryParams, ParseParametersExpand)
531 {
532     auto ret = boost::urls::parse_relative_ref("/redfish/v1?$expand=*");
533     ASSERT_TRUE(ret);
534     if (!ret)
535     {
536         return;
537     }
538 
539     crow::Response res;
540 
541     std::optional<Query> query = parseParameters(ret->params(), res);
542     if constexpr (BMCWEB_INSECURE_ENABLE_REDFISH_QUERY)
543     {
544         ASSERT_TRUE(query);
545         if (!query)
546         {
547             return;
548         }
549         EXPECT_TRUE(query.value().expandType ==
550                     redfish::query_param::ExpandType::Both);
551     }
552     else
553     {
554         ASSERT_EQ(query, std::nullopt);
555     }
556 }
557 
TEST(QueryParams,ParseParametersTop)558 TEST(QueryParams, ParseParametersTop)
559 {
560     auto ret = boost::urls::parse_relative_ref("/redfish/v1?$top=1");
561     ASSERT_TRUE(ret);
562     if (!ret)
563     {
564         return;
565     }
566 
567     crow::Response res;
568 
569     std::optional<Query> query = parseParameters(ret->params(), res);
570     ASSERT_TRUE(query);
571     if (!query)
572     {
573         return;
574     }
575     EXPECT_EQ(query.value().top, 1);
576 }
577 
TEST(QueryParams,ParseParametersTopOutOfRangeNegative)578 TEST(QueryParams, ParseParametersTopOutOfRangeNegative)
579 {
580     auto ret = boost::urls::parse_relative_ref("/redfish/v1?$top=-1");
581     ASSERT_TRUE(ret);
582 
583     crow::Response res;
584 
585     std::optional<Query> query = parseParameters(ret->params(), res);
586     ASSERT_TRUE(query == std::nullopt);
587 }
588 
TEST(QueryParams,ParseParametersTopOutOfRangePositive)589 TEST(QueryParams, ParseParametersTopOutOfRangePositive)
590 {
591     auto ret = boost::urls::parse_relative_ref("/redfish/v1?$top=1001");
592     ASSERT_TRUE(ret);
593     if (!ret)
594     {
595         return;
596     }
597     crow::Response res;
598 
599     std::optional<Query> query = parseParameters(ret->params(), res);
600     ASSERT_TRUE(query == std::nullopt);
601 }
602 
TEST(QueryParams,ParseParametersSkip)603 TEST(QueryParams, ParseParametersSkip)
604 {
605     auto ret = boost::urls::parse_relative_ref("/redfish/v1?$skip=1");
606     ASSERT_TRUE(ret);
607 
608     crow::Response res;
609 
610     std::optional<Query> query = parseParameters(ret->params(), res);
611     ASSERT_TRUE(query);
612     if (!query)
613     {
614         return;
615     }
616     EXPECT_EQ(query.value().skip, 1);
617 }
TEST(QueryParams,ParseParametersSkipOutOfRange)618 TEST(QueryParams, ParseParametersSkipOutOfRange)
619 {
620     auto ret = boost::urls::parse_relative_ref(
621         "/redfish/v1?$skip=99999999999999999999");
622     ASSERT_TRUE(ret);
623 
624     crow::Response res;
625 
626     std::optional<Query> query = parseParameters(ret->params(), res);
627     ASSERT_EQ(query, std::nullopt);
628 }
629 
TEST(QueryParams,ParseParametersUnexpectedGetsIgnored)630 TEST(QueryParams, ParseParametersUnexpectedGetsIgnored)
631 {
632     auto ret = boost::urls::parse_relative_ref("/redfish/v1?unexpected_param");
633     ASSERT_TRUE(ret);
634 
635     crow::Response res;
636 
637     std::optional<Query> query = parseParameters(ret->params(), res);
638     ASSERT_TRUE(query != std::nullopt);
639 }
640 
TEST(QueryParams,ParseParametersUnexpectedDollarGetsError)641 TEST(QueryParams, ParseParametersUnexpectedDollarGetsError)
642 {
643     auto ret = boost::urls::parse_relative_ref("/redfish/v1?$unexpected_param");
644     ASSERT_TRUE(ret);
645 
646     crow::Response res;
647 
648     std::optional<Query> query = parseParameters(ret->params(), res);
649     ASSERT_TRUE(query == std::nullopt);
650     EXPECT_EQ(res.result(), boost::beast::http::status::not_implemented);
651 }
652 
TEST(QueryParams,GetExpandType)653 TEST(QueryParams, GetExpandType)
654 {
655     Query query{};
656 
657     EXPECT_FALSE(getExpandType("", query));
658     EXPECT_FALSE(getExpandType(".(", query));
659     EXPECT_FALSE(getExpandType(".()", query));
660     EXPECT_FALSE(getExpandType(".($levels=1", query));
661 
662     EXPECT_TRUE(getExpandType("*", query));
663     EXPECT_EQ(query.expandType, ExpandType::Both);
664     EXPECT_TRUE(getExpandType(".", query));
665     EXPECT_EQ(query.expandType, ExpandType::NotLinks);
666     EXPECT_TRUE(getExpandType("~", query));
667     EXPECT_EQ(query.expandType, ExpandType::Links);
668 
669     // Per redfish specification, level defaults to 1
670     EXPECT_TRUE(getExpandType(".", query));
671     EXPECT_EQ(query.expandLevel, 1);
672 
673     EXPECT_TRUE(getExpandType(".($levels=42)", query));
674     EXPECT_EQ(query.expandLevel, 42);
675 
676     // Overflow
677     EXPECT_FALSE(getExpandType(".($levels=256)", query));
678 
679     // Negative
680     EXPECT_FALSE(getExpandType(".($levels=-1)", query));
681 
682     // No number
683     EXPECT_FALSE(getExpandType(".($levels=a)", query));
684 }
685 
TEST(QueryParams,FindNavigationReferencesNonLink)686 TEST(QueryParams, FindNavigationReferencesNonLink)
687 {
688     using nlohmann::json;
689 
690     // Responses must include their "@odata.id" property for $expand to work
691     // correctly
692     json singleTreeNode =
693         R"({"@odata.id": "/redfish/v1",
694         "Foo" : {"@odata.id": "/foobar"}})"_json;
695 
696     // Parsing as the root should net one entry
697     EXPECT_THAT(
698         findNavigationReferences(ExpandType::Both, 1, 0, singleTreeNode),
699         UnorderedElementsAre(
700             ExpandNode{json::json_pointer("/Foo"), "/foobar"}));
701 
702     // Parsing in Non-hyperlinks mode should net one entry
703     EXPECT_THAT(
704         findNavigationReferences(ExpandType::NotLinks, 1, 0, singleTreeNode),
705         UnorderedElementsAre(
706             ExpandNode{json::json_pointer("/Foo"), "/foobar"}));
707 
708     // Searching for not types should return empty set
709     EXPECT_TRUE(findNavigationReferences(ExpandType::None, 1, 0, singleTreeNode)
710                     .empty());
711 
712     // Searching for hyperlinks only should return empty set
713     EXPECT_TRUE(
714         findNavigationReferences(ExpandType::Links, 1, 0, singleTreeNode)
715             .empty());
716 
717     // Responses must include their "@odata.id" property for $expand to work
718     // correctly
719     json multiTreeNodes =
720         R"({"@odata.id": "/redfish/v1",
721         "Links": {"@odata.id": "/links"},
722         "Foo" : {"@odata.id": "/foobar"}})"_json;
723 
724     // Should still find Foo
725     EXPECT_THAT(
726         findNavigationReferences(ExpandType::NotLinks, 1, 0, multiTreeNodes),
727         UnorderedElementsAre(
728             ExpandNode{json::json_pointer("/Foo"), "/foobar"}));
729 }
730 
TEST(QueryParams,FindNavigationReferencesLink)731 TEST(QueryParams, FindNavigationReferencesLink)
732 {
733     using nlohmann::json;
734 
735     // Responses must include their "@odata.id" property for $expand to work
736     // correctly
737     json singleLinkNode =
738         R"({"@odata.id": "/redfish/v1",
739         "Links" : {"Sessions": {"@odata.id": "/foobar"}}})"_json;
740 
741     // Parsing as the root should net one entry
742     EXPECT_THAT(
743         findNavigationReferences(ExpandType::Both, 1, 0, singleLinkNode),
744         UnorderedElementsAre(
745             ExpandNode{json::json_pointer("/Links/Sessions"), "/foobar"}));
746     // Parsing in hyperlinks mode should net one entry
747     EXPECT_THAT(
748         findNavigationReferences(ExpandType::Links, 1, 0, singleLinkNode),
749         UnorderedElementsAre(
750             ExpandNode{json::json_pointer("/Links/Sessions"), "/foobar"}));
751 
752     // Searching for not types should return empty set
753     EXPECT_TRUE(findNavigationReferences(ExpandType::None, 1, 0, singleLinkNode)
754                     .empty());
755 
756     // Searching for non-hyperlinks only should return empty set
757     EXPECT_TRUE(
758         findNavigationReferences(ExpandType::NotLinks, 1, 0, singleLinkNode)
759             .empty());
760 }
761 
TEST(QueryParams,PreviouslyExpanded)762 TEST(QueryParams, PreviouslyExpanded)
763 {
764     using nlohmann::json;
765 
766     // Responses must include their "@odata.id" property for $expand to work
767     // correctly
768     json expNode = json::parse(R"(
769 {
770   "@odata.id": "/redfish/v1/Chassis",
771   "@odata.type": "#ChassisCollection.ChassisCollection",
772   "Members": [
773     {
774       "@odata.id": "/redfish/v1/Chassis/5B247A_Sat1",
775       "@odata.type": "#Chassis.v1_17_0.Chassis",
776       "Sensors": {
777         "@odata.id": "/redfish/v1/Chassis/5B247A_Sat1/Sensors"
778       }
779     },
780     {
781       "@odata.id": "/redfish/v1/Chassis/5B247A_Sat2",
782       "@odata.type": "#Chassis.v1_17_0.Chassis",
783       "Sensors": {
784         "@odata.id": "/redfish/v1/Chassis/5B247A_Sat2/Sensors"
785       }
786     }
787   ],
788   "Members@odata.count": 2,
789   "Name": "Chassis Collection"
790 }
791 )",
792                                nullptr, false);
793 
794     // Expand has already occurred so we should not do anything
795     EXPECT_TRUE(
796         findNavigationReferences(ExpandType::NotLinks, 1, 0, expNode).empty());
797 
798     // Previous expand was only a single level so we should further expand
799     EXPECT_THAT(findNavigationReferences(ExpandType::NotLinks, 2, 0, expNode),
800                 UnorderedElementsAre(
801                     ExpandNode{json::json_pointer("/Members/0/Sensors"),
802                                "/redfish/v1/Chassis/5B247A_Sat1/Sensors"},
803                     ExpandNode{json::json_pointer("/Members/1/Sensors"),
804                                "/redfish/v1/Chassis/5B247A_Sat2/Sensors"}));
805 
806     // Make sure we can handle when an array was expanded further down the tree
807     json expNode2 = R"({"@odata.id" : "/redfish/v1"})"_json;
808     expNode2["Chassis"] = std::move(expNode);
809     EXPECT_TRUE(
810         findNavigationReferences(ExpandType::NotLinks, 1, 0, expNode2).empty());
811     EXPECT_TRUE(
812         findNavigationReferences(ExpandType::NotLinks, 2, 0, expNode2).empty());
813 
814     // Previous expand was two levels so we should further expand
815     EXPECT_THAT(findNavigationReferences(ExpandType::NotLinks, 3, 0, expNode2),
816                 UnorderedElementsAre(
817                     ExpandNode{json::json_pointer("/Chassis/Members/0/Sensors"),
818                                "/redfish/v1/Chassis/5B247A_Sat1/Sensors"},
819                     ExpandNode{json::json_pointer("/Chassis/Members/1/Sensors"),
820                                "/redfish/v1/Chassis/5B247A_Sat2/Sensors"}));
821 }
822 
TEST(QueryParams,DelegatedSkipExpanded)823 TEST(QueryParams, DelegatedSkipExpanded)
824 {
825     using nlohmann::json;
826 
827     // Responses must include their "@odata.id" property for $expand to work
828     // correctly
829     json expNode = json::parse(R"(
830 {
831   "@odata.id": "/redfish/v1",
832   "Foo": {
833     "@odata.id": "/foo"
834   },
835   "Bar": {
836     "@odata.id": "/bar",
837     "Foo": {
838       "@odata.id": "/barfoo"
839     }
840   }
841 }
842 )",
843                                nullptr, false);
844 
845     EXPECT_THAT(findNavigationReferences(ExpandType::NotLinks, 2, 0, expNode),
846                 UnorderedElementsAre(
847                     ExpandNode{json::json_pointer("/Foo"), "/foo"},
848                     ExpandNode{json::json_pointer("/Bar/Foo"), "/barfoo"}));
849 
850     // Skip the first expand level
851     EXPECT_THAT(findNavigationReferences(ExpandType::NotLinks, 1, 1, expNode),
852                 UnorderedElementsAre(
853                     ExpandNode{json::json_pointer("/Bar/Foo"), "/barfoo"}));
854 }
855 
TEST(QueryParams,PartiallyPreviouslyExpanded)856 TEST(QueryParams, PartiallyPreviouslyExpanded)
857 {
858     using nlohmann::json;
859 
860     // Responses must include their "@odata.id" property for $expand to work
861     // correctly
862     json expNode = json::parse(R"(
863 {
864   "@odata.id": "/redfish/v1/Chassis",
865   "@odata.type": "#ChassisCollection.ChassisCollection",
866   "Members": [
867     {
868       "@odata.id": "/redfish/v1/Chassis/Local"
869     },
870     {
871       "@odata.id": "/redfish/v1/Chassis/5B247A_Sat1",
872       "@odata.type": "#Chassis.v1_17_0.Chassis",
873       "Sensors": {
874         "@odata.id": "/redfish/v1/Chassis/5B247A_Sat1/Sensors"
875       }
876     }
877   ],
878   "Members@odata.count": 2,
879   "Name": "Chassis Collection"
880 }
881 )",
882                                nullptr, false);
883 
884     // The 5B247A_Sat1 Chassis was already expanded a single level so we should
885     // only want to expand the Local Chassis
886     EXPECT_THAT(
887         findNavigationReferences(ExpandType::NotLinks, 1, 0, expNode),
888         UnorderedElementsAre(ExpandNode{json::json_pointer("/Members/0"),
889                                         "/redfish/v1/Chassis/Local"}));
890 
891     // The 5B247A_Sat1 Chassis was already expanded a single level so we should
892     // further expand it as well as the Local Chassis
893     EXPECT_THAT(findNavigationReferences(ExpandType::NotLinks, 2, 0, expNode),
894                 UnorderedElementsAre(
895                     ExpandNode{json::json_pointer("/Members/0"),
896                                "/redfish/v1/Chassis/Local"},
897                     ExpandNode{json::json_pointer("/Members/1/Sensors"),
898                                "/redfish/v1/Chassis/5B247A_Sat1/Sensors"}));
899 
900     // Now the response has paths that have been expanded 0, 1, and 2 times
901     json expNode2 = R"({"@odata.id" : "/redfish/v1",
902                         "Systems": {"@odata.id": "/redfish/v1/Systems"}})"_json;
903     expNode2["Chassis"] = std::move(expNode);
904 
905     EXPECT_THAT(findNavigationReferences(ExpandType::NotLinks, 1, 0, expNode2),
906                 UnorderedElementsAre(ExpandNode{json::json_pointer("/Systems"),
907                                                 "/redfish/v1/Systems"}));
908 
909     EXPECT_THAT(
910         findNavigationReferences(ExpandType::NotLinks, 2, 0, expNode2),
911         UnorderedElementsAre(
912             ExpandNode{json::json_pointer("/Systems"), "/redfish/v1/Systems"},
913             ExpandNode{json::json_pointer("/Chassis/Members/0"),
914                        "/redfish/v1/Chassis/Local"}));
915 
916     EXPECT_THAT(
917         findNavigationReferences(ExpandType::NotLinks, 3, 0, expNode2),
918         UnorderedElementsAre(
919             ExpandNode{json::json_pointer("/Systems"), "/redfish/v1/Systems"},
920             ExpandNode{json::json_pointer("/Chassis/Members/0"),
921                        "/redfish/v1/Chassis/Local"},
922             ExpandNode{json::json_pointer("/Chassis/Members/1/Sensors"),
923                        "/redfish/v1/Chassis/5B247A_Sat1/Sensors"}));
924 }
925 
926 } // namespace
927 } // namespace redfish::query_param
928