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