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