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