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, 2);
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 
362     ASSERT_NE(query, std::nullopt);
363     recursiveSelect(root, query->selectTrie.root);
364     EXPECT_EQ(root, expected);
365 }
366 
367 TEST(PropogateErrorCode, 500IsWorst)
368 {
369     constexpr std::array<unsigned, 7> codes = {100, 200, 300, 400,
370                                                401, 500, 501};
371     for (auto code : codes)
372     {
373         EXPECT_EQ(propogateErrorCode(500, code), 500);
374         EXPECT_EQ(propogateErrorCode(code, 500), 500);
375     }
376 }
377 
378 TEST(PropogateErrorCode, 5xxAreWorseThanOthers)
379 {
380     constexpr std::array<unsigned, 7> codes = {100, 200, 300, 400,
381                                                401, 501, 502};
382     for (auto code : codes)
383     {
384         EXPECT_EQ(propogateErrorCode(code, 505), 505);
385         EXPECT_EQ(propogateErrorCode(505, code), 505);
386     }
387     EXPECT_EQ(propogateErrorCode(502, 501), 502);
388     EXPECT_EQ(propogateErrorCode(501, 502), 502);
389     EXPECT_EQ(propogateErrorCode(503, 502), 503);
390 }
391 
392 TEST(PropogateErrorCode, 401IsWorseThanOthers)
393 {
394     constexpr std::array<unsigned, 7> codes = {100, 200, 300, 400, 401};
395     for (auto code : codes)
396     {
397         EXPECT_EQ(propogateErrorCode(code, 401), 401);
398         EXPECT_EQ(propogateErrorCode(401, code), 401);
399     }
400 }
401 
402 TEST(PropogateErrorCode, 4xxIsWorseThanOthers)
403 {
404     constexpr std::array<unsigned, 7> codes = {100, 200, 300, 400, 402};
405     for (auto code : codes)
406     {
407         EXPECT_EQ(propogateErrorCode(code, 405), 405);
408         EXPECT_EQ(propogateErrorCode(405, code), 405);
409     }
410     EXPECT_EQ(propogateErrorCode(400, 402), 402);
411     EXPECT_EQ(propogateErrorCode(402, 403), 403);
412     EXPECT_EQ(propogateErrorCode(403, 402), 403);
413 }
414 
415 TEST(PropogateError, IntermediateNoErrorMessageMakesNoChange)
416 {
417     crow::Response intermediate;
418     intermediate.result(boost::beast::http::status::ok);
419 
420     crow::Response finalRes;
421     finalRes.result(boost::beast::http::status::ok);
422     propogateError(finalRes, intermediate);
423     EXPECT_EQ(finalRes.result(), boost::beast::http::status::ok);
424     EXPECT_EQ(finalRes.jsonValue.find("error"), finalRes.jsonValue.end());
425 }
426 
427 TEST(PropogateError, ErrorsArePropergatedWithErrorInRoot)
428 {
429     nlohmann::json root = R"(
430 {
431     "@odata.type": "#Message.v1_1_1.Message",
432     "Message": "The request failed due to an internal service error.  The service is still operational.",
433     "MessageArgs": [],
434     "MessageId": "Base.1.13.0.InternalError",
435     "MessageSeverity": "Critical",
436     "Resolution": "Resubmit the request.  If the problem persists, consider resetting the service."
437 }
438 )"_json;
439     crow::Response intermediate;
440     intermediate.result(boost::beast::http::status::internal_server_error);
441     intermediate.jsonValue = root;
442 
443     crow::Response final;
444     final.result(boost::beast::http::status::ok);
445 
446     propogateError(final, intermediate);
447 
448     EXPECT_EQ(final.jsonValue["error"]["code"].get<std::string>(),
449               "Base.1.13.0.InternalError");
450     EXPECT_EQ(
451         final.jsonValue["error"]["message"].get<std::string>(),
452         "The request failed due to an internal service error.  The service is still operational.");
453     EXPECT_EQ(intermediate.jsonValue, R"({})"_json);
454     EXPECT_EQ(final.result(),
455               boost::beast::http::status::internal_server_error);
456 }
457 
458 TEST(PropogateError, ErrorsArePropergatedWithErrorCode)
459 {
460     crow::Response intermediate;
461     intermediate.result(boost::beast::http::status::internal_server_error);
462 
463     nlohmann::json error = R"(
464 {
465     "error": {
466         "@Message.ExtendedInfo": [],
467         "code": "Base.1.13.0.InternalError",
468         "message": "The request failed due to an internal service error.  The service is still operational."
469     }
470 }
471 )"_json;
472     nlohmann::json extendedInfo = R"(
473 {
474     "@odata.type": "#Message.v1_1_1.Message",
475     "Message": "The request failed due to an internal service error.  The service is still operational.",
476     "MessageArgs": [],
477     "MessageId": "Base.1.13.0.InternalError",
478     "MessageSeverity": "Critical",
479     "Resolution": "Resubmit the request.  If the problem persists, consider resetting the service."
480 }
481 )"_json;
482 
483     for (int i = 0; i < 10; ++i)
484     {
485         error["error"][messages::messageAnnotation].push_back(extendedInfo);
486     }
487     intermediate.jsonValue = error;
488     crow::Response final;
489     final.result(boost::beast::http::status::ok);
490 
491     propogateError(final, intermediate);
492     EXPECT_EQ(final.jsonValue["error"][messages::messageAnnotation],
493               error["error"][messages::messageAnnotation]);
494     std::string errorCode = messages::messageVersionPrefix;
495     errorCode += "GeneralError";
496     std::string errorMessage =
497         "A general error has occurred. See Resolution for "
498         "information on how to resolve the error.";
499     EXPECT_EQ(final.jsonValue["error"]["code"].get<std::string>(), errorCode);
500     EXPECT_EQ(final.jsonValue["error"]["message"].get<std::string>(),
501               errorMessage);
502     EXPECT_EQ(intermediate.jsonValue, R"({})"_json);
503     EXPECT_EQ(final.result(),
504               boost::beast::http::status::internal_server_error);
505 }
506 
507 TEST(QueryParams, ParseParametersOnly)
508 {
509     auto ret = boost::urls::parse_relative_ref("/redfish/v1?only");
510     ASSERT_TRUE(ret);
511 
512     crow::Response res;
513     std::optional<Query> query = parseParameters(ret->params(), res);
514     ASSERT_TRUE(query != std::nullopt);
515     EXPECT_TRUE(query->isOnly);
516 }
517 
518 TEST(QueryParams, ParseParametersExpand)
519 {
520     auto ret = boost::urls::parse_relative_ref("/redfish/v1?$expand=*");
521     ASSERT_TRUE(ret);
522 
523     crow::Response res;
524 
525     std::optional<Query> query = parseParameters(ret->params(), res);
526     if constexpr (bmcwebInsecureEnableQueryParams)
527     {
528         ASSERT_NE(query, std::nullopt);
529         EXPECT_TRUE(query->expandType ==
530                     redfish::query_param::ExpandType::Both);
531     }
532     else
533     {
534         ASSERT_EQ(query, std::nullopt);
535     }
536 }
537 
538 TEST(QueryParams, ParseParametersTop)
539 {
540     auto ret = boost::urls::parse_relative_ref("/redfish/v1?$top=1");
541     ASSERT_TRUE(ret);
542 
543     crow::Response res;
544 
545     std::optional<Query> query = parseParameters(ret->params(), res);
546     ASSERT_TRUE(query != std::nullopt);
547     EXPECT_EQ(query->top, 1);
548 }
549 
550 TEST(QueryParams, ParseParametersTopOutOfRangeNegative)
551 {
552     auto ret = boost::urls::parse_relative_ref("/redfish/v1?$top=-1");
553     ASSERT_TRUE(ret);
554 
555     crow::Response res;
556 
557     std::optional<Query> query = parseParameters(ret->params(), res);
558     ASSERT_TRUE(query == std::nullopt);
559 }
560 
561 TEST(QueryParams, ParseParametersTopOutOfRangePositive)
562 {
563     auto ret = boost::urls::parse_relative_ref("/redfish/v1?$top=1001");
564     ASSERT_TRUE(ret);
565 
566     crow::Response res;
567 
568     std::optional<Query> query = parseParameters(ret->params(), res);
569     ASSERT_TRUE(query == std::nullopt);
570 }
571 
572 TEST(QueryParams, ParseParametersSkip)
573 {
574     auto ret = boost::urls::parse_relative_ref("/redfish/v1?$skip=1");
575     ASSERT_TRUE(ret);
576 
577     crow::Response res;
578 
579     std::optional<Query> query = parseParameters(ret->params(), res);
580     ASSERT_TRUE(query != std::nullopt);
581     EXPECT_EQ(query->skip, 1);
582 }
583 TEST(QueryParams, ParseParametersSkipOutOfRange)
584 {
585     auto ret = boost::urls::parse_relative_ref(
586         "/redfish/v1?$skip=99999999999999999999");
587     ASSERT_TRUE(ret);
588 
589     crow::Response res;
590 
591     std::optional<Query> query = parseParameters(ret->params(), res);
592     ASSERT_EQ(query, std::nullopt);
593 }
594 
595 TEST(QueryParams, ParseParametersUnexpectedGetsIgnored)
596 {
597     auto ret = boost::urls::parse_relative_ref("/redfish/v1?unexpected_param");
598     ASSERT_TRUE(ret);
599 
600     crow::Response res;
601 
602     std::optional<Query> query = parseParameters(ret->params(), res);
603     ASSERT_TRUE(query != std::nullopt);
604 }
605 
606 TEST(QueryParams, ParseParametersUnexpectedDollarGetsError)
607 {
608     auto ret = boost::urls::parse_relative_ref("/redfish/v1?$unexpected_param");
609     ASSERT_TRUE(ret);
610 
611     crow::Response res;
612 
613     std::optional<Query> query = parseParameters(ret->params(), res);
614     ASSERT_TRUE(query == std::nullopt);
615     EXPECT_EQ(res.result(), boost::beast::http::status::not_implemented);
616 }
617 
618 TEST(QueryParams, GetExpandType)
619 {
620     Query query{};
621 
622     EXPECT_FALSE(getExpandType("", query));
623     EXPECT_FALSE(getExpandType(".(", query));
624     EXPECT_FALSE(getExpandType(".()", query));
625     EXPECT_FALSE(getExpandType(".($levels=1", query));
626 
627     EXPECT_TRUE(getExpandType("*", query));
628     EXPECT_EQ(query.expandType, ExpandType::Both);
629     EXPECT_TRUE(getExpandType(".", query));
630     EXPECT_EQ(query.expandType, ExpandType::NotLinks);
631     EXPECT_TRUE(getExpandType("~", query));
632     EXPECT_EQ(query.expandType, ExpandType::Links);
633 
634     // Per redfish specification, level defaults to 1
635     EXPECT_TRUE(getExpandType(".", query));
636     EXPECT_EQ(query.expandLevel, 1);
637 
638     EXPECT_TRUE(getExpandType(".($levels=42)", query));
639     EXPECT_EQ(query.expandLevel, 42);
640 
641     // Overflow
642     EXPECT_FALSE(getExpandType(".($levels=256)", query));
643 
644     // Negative
645     EXPECT_FALSE(getExpandType(".($levels=-1)", query));
646 
647     // No number
648     EXPECT_FALSE(getExpandType(".($levels=a)", query));
649 }
650 
651 TEST(QueryParams, FindNavigationReferencesNonLink)
652 {
653     using nlohmann::json;
654 
655     // Responses must include their "@odata.id" property for $expand to work
656     // correctly
657     json singleTreeNode =
658         R"({"@odata.id": "/redfish/v1",
659         "Foo" : {"@odata.id": "/foobar"}})"_json;
660 
661     // Parsing as the root should net one entry
662     EXPECT_THAT(findNavigationReferences(ExpandType::Both, 1, singleTreeNode),
663                 UnorderedElementsAre(
664                     ExpandNode{json::json_pointer("/Foo"), "/foobar"}));
665 
666     // Parsing in Non-hyperlinks mode should net one entry
667     EXPECT_THAT(
668         findNavigationReferences(ExpandType::NotLinks, 1, singleTreeNode),
669         UnorderedElementsAre(
670             ExpandNode{json::json_pointer("/Foo"), "/foobar"}));
671 
672     // Searching for not types should return empty set
673     EXPECT_TRUE(
674         findNavigationReferences(ExpandType::None, 1, singleTreeNode).empty());
675 
676     // Searching for hyperlinks only should return empty set
677     EXPECT_TRUE(
678         findNavigationReferences(ExpandType::Links, 1, singleTreeNode).empty());
679 
680     // Responses must include their "@odata.id" property for $expand to work
681     // correctly
682     json multiTreeNodes =
683         R"({"@odata.id": "/redfish/v1",
684         "Links": {"@odata.id": "/links"},
685         "Foo" : {"@odata.id": "/foobar"}})"_json;
686 
687     // Should still find Foo
688     EXPECT_THAT(
689         findNavigationReferences(ExpandType::NotLinks, 1, multiTreeNodes),
690         UnorderedElementsAre(
691             ExpandNode{json::json_pointer("/Foo"), "/foobar"}));
692 }
693 
694 TEST(QueryParams, FindNavigationReferencesLink)
695 {
696     using nlohmann::json;
697 
698     // Responses must include their "@odata.id" property for $expand to work
699     // correctly
700     json singleLinkNode =
701         R"({"@odata.id": "/redfish/v1",
702         "Links" : {"Sessions": {"@odata.id": "/foobar"}}})"_json;
703 
704     // Parsing as the root should net one entry
705     EXPECT_THAT(findNavigationReferences(ExpandType::Both, 1, singleLinkNode),
706                 UnorderedElementsAre(ExpandNode{
707                     json::json_pointer("/Links/Sessions"), "/foobar"}));
708     // Parsing in hyperlinks mode should net one entry
709     EXPECT_THAT(findNavigationReferences(ExpandType::Links, 1, singleLinkNode),
710                 UnorderedElementsAre(ExpandNode{
711                     json::json_pointer("/Links/Sessions"), "/foobar"}));
712 
713     // Searching for not types should return empty set
714     EXPECT_TRUE(
715         findNavigationReferences(ExpandType::None, 1, singleLinkNode).empty());
716 
717     // Searching for non-hyperlinks only should return empty set
718     EXPECT_TRUE(
719         findNavigationReferences(ExpandType::NotLinks, 1, singleLinkNode)
720             .empty());
721 }
722 
723 TEST(QueryParams, PreviouslyExpanded)
724 {
725     using nlohmann::json;
726 
727     // Responses must include their "@odata.id" property for $expand to work
728     // correctly
729     json expNode = json::parse(R"(
730 {
731   "@odata.id": "/redfish/v1/Chassis",
732   "@odata.type": "#ChassisCollection.ChassisCollection",
733   "Members": [
734     {
735       "@odata.id": "/redfish/v1/Chassis/5B247A_Sat1",
736       "@odata.type": "#Chassis.v1_17_0.Chassis",
737       "Sensors": {
738         "@odata.id": "/redfish/v1/Chassis/5B247A_Sat1/Sensors"
739       }
740     },
741     {
742       "@odata.id": "/redfish/v1/Chassis/5B247A_Sat2",
743       "@odata.type": "#Chassis.v1_17_0.Chassis",
744       "Sensors": {
745         "@odata.id": "/redfish/v1/Chassis/5B247A_Sat2/Sensors"
746       }
747     }
748   ],
749   "Members@odata.count": 2,
750   "Name": "Chassis Collection"
751 }
752 )",
753                                nullptr, false);
754 
755     // Expand has already occurred so we should not do anything
756     EXPECT_TRUE(
757         findNavigationReferences(ExpandType::NotLinks, 1, expNode).empty());
758 
759     // Previous expand was only a single level so we should further expand
760     EXPECT_THAT(findNavigationReferences(ExpandType::NotLinks, 2, expNode),
761                 UnorderedElementsAre(
762                     ExpandNode{json::json_pointer("/Members/0/Sensors"),
763                                "/redfish/v1/Chassis/5B247A_Sat1/Sensors"},
764                     ExpandNode{json::json_pointer("/Members/1/Sensors"),
765                                "/redfish/v1/Chassis/5B247A_Sat2/Sensors"}));
766 
767     // Make sure we can handle when an array was expanded further down the tree
768     json expNode2 = R"({"@odata.id" : "/redfish/v1"})"_json;
769     expNode2["Chassis"] = std::move(expNode);
770     EXPECT_TRUE(
771         findNavigationReferences(ExpandType::NotLinks, 1, expNode2).empty());
772     EXPECT_TRUE(
773         findNavigationReferences(ExpandType::NotLinks, 2, expNode2).empty());
774 
775     // Previous expand was two levels so we should further expand
776     EXPECT_THAT(findNavigationReferences(ExpandType::NotLinks, 3, expNode2),
777                 UnorderedElementsAre(
778                     ExpandNode{json::json_pointer("/Chassis/Members/0/Sensors"),
779                                "/redfish/v1/Chassis/5B247A_Sat1/Sensors"},
780                     ExpandNode{json::json_pointer("/Chassis/Members/1/Sensors"),
781                                "/redfish/v1/Chassis/5B247A_Sat2/Sensors"}));
782 }
783 
784 TEST(QueryParams, PartiallyPreviouslyExpanded)
785 {
786     using nlohmann::json;
787 
788     // Responses must include their "@odata.id" property for $expand to work
789     // correctly
790     json expNode = json::parse(R"(
791 {
792   "@odata.id": "/redfish/v1/Chassis",
793   "@odata.type": "#ChassisCollection.ChassisCollection",
794   "Members": [
795     {
796       "@odata.id": "/redfish/v1/Chassis/Local"
797     },
798     {
799       "@odata.id": "/redfish/v1/Chassis/5B247A_Sat1",
800       "@odata.type": "#Chassis.v1_17_0.Chassis",
801       "Sensors": {
802         "@odata.id": "/redfish/v1/Chassis/5B247A_Sat1/Sensors"
803       }
804     }
805   ],
806   "Members@odata.count": 2,
807   "Name": "Chassis Collection"
808 }
809 )",
810                                nullptr, false);
811 
812     // The 5B247A_Sat1 Chassis was already expanded a single level so we should
813     // only want to expand the Local Chassis
814     EXPECT_THAT(
815         findNavigationReferences(ExpandType::NotLinks, 1, expNode),
816         UnorderedElementsAre(ExpandNode{json::json_pointer("/Members/0"),
817                                         "/redfish/v1/Chassis/Local"}));
818 
819     // The 5B247A_Sat1 Chassis was already expanded a single level so we should
820     // further expand it as well as the Local Chassis
821     EXPECT_THAT(findNavigationReferences(ExpandType::NotLinks, 2, expNode),
822                 UnorderedElementsAre(
823                     ExpandNode{json::json_pointer("/Members/0"),
824                                "/redfish/v1/Chassis/Local"},
825                     ExpandNode{json::json_pointer("/Members/1/Sensors"),
826                                "/redfish/v1/Chassis/5B247A_Sat1/Sensors"}));
827 
828     // Now the response has paths that have been expanded 0, 1, and 2 times
829     json expNode2 = R"({"@odata.id" : "/redfish/v1",
830                         "Systems": {"@odata.id": "/redfish/v1/Systems"}})"_json;
831     expNode2["Chassis"] = std::move(expNode);
832 
833     EXPECT_THAT(findNavigationReferences(ExpandType::NotLinks, 1, expNode2),
834                 UnorderedElementsAre(ExpandNode{json::json_pointer("/Systems"),
835                                                 "/redfish/v1/Systems"}));
836 
837     EXPECT_THAT(
838         findNavigationReferences(ExpandType::NotLinks, 2, expNode2),
839         UnorderedElementsAre(
840             ExpandNode{json::json_pointer("/Systems"), "/redfish/v1/Systems"},
841             ExpandNode{json::json_pointer("/Chassis/Members/0"),
842                        "/redfish/v1/Chassis/Local"}));
843 
844     EXPECT_THAT(
845         findNavigationReferences(ExpandType::NotLinks, 3, expNode2),
846         UnorderedElementsAre(
847             ExpandNode{json::json_pointer("/Systems"), "/redfish/v1/Systems"},
848             ExpandNode{json::json_pointer("/Chassis/Members/0"),
849                        "/redfish/v1/Chassis/Local"},
850             ExpandNode{json::json_pointer("/Chassis/Members/1/Sensors"),
851                        "/redfish/v1/Chassis/5B247A_Sat1/Sensors"}));
852 }
853 
854 } // namespace
855 } // namespace redfish::query_param
856