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