summaryrefslogtreecommitdiff
path: root/test/redfish-core/include/utils/query_param_test.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'test/redfish-core/include/utils/query_param_test.cpp')
-rw-r--r--test/redfish-core/include/utils/query_param_test.cpp709
1 files changed, 709 insertions, 0 deletions
diff --git a/test/redfish-core/include/utils/query_param_test.cpp b/test/redfish-core/include/utils/query_param_test.cpp
new file mode 100644
index 0000000000..51f0b7101f
--- /dev/null
+++ b/test/redfish-core/include/utils/query_param_test.cpp
@@ -0,0 +1,709 @@
+#include "bmcweb_config.h"
+
+#include "utils/query_param.hpp"
+
+#include <boost/system/result.hpp>
+#include <boost/url/url_view.hpp>
+#include <nlohmann/json.hpp>
+
+#include <new>
+#include <span>
+
+#include <gmock/gmock.h> // IWYU pragma: keep
+#include <gtest/gtest.h> // IWYU pragma: keep
+
+// IWYU pragma: no_include <gtest/gtest-message.h>
+// IWYU pragma: no_include <gtest/gtest-test-part.h>
+// IWYU pragma: no_include "gtest/gtest_pred_impl.h"
+// IWYU pragma: no_include <boost/url/impl/url_view.hpp>
+// IWYU pragma: no_include <gmock/gmock-matchers.h>
+// IWYU pragma: no_include <gtest/gtest-matchers.h>
+
+namespace redfish::query_param
+{
+namespace
+{
+
+using ::testing::UnorderedElementsAre;
+
+TEST(Delegate, OnlyPositive)
+{
+ Query query{
+ .isOnly = true,
+ };
+ QueryCapabilities capabilities{
+ .canDelegateOnly = true,
+ };
+ Query delegated = delegate(capabilities, query);
+ EXPECT_TRUE(delegated.isOnly);
+ EXPECT_FALSE(query.isOnly);
+}
+
+TEST(Delegate, ExpandPositive)
+{
+ Query query{
+ .isOnly = false,
+ .expandLevel = 5,
+ .expandType = ExpandType::Both,
+ };
+ QueryCapabilities capabilities{
+ .canDelegateExpandLevel = 3,
+ };
+ Query delegated = delegate(capabilities, query);
+ EXPECT_FALSE(delegated.isOnly);
+ EXPECT_EQ(delegated.expandLevel, capabilities.canDelegateExpandLevel);
+ EXPECT_EQ(delegated.expandType, ExpandType::Both);
+ EXPECT_EQ(query.expandLevel, 2);
+}
+
+TEST(Delegate, OnlyNegative)
+{
+ Query query{
+ .isOnly = true,
+ };
+ QueryCapabilities capabilities{
+ .canDelegateOnly = false,
+ };
+ Query delegated = delegate(capabilities, query);
+ EXPECT_FALSE(delegated.isOnly);
+ EXPECT_EQ(query.isOnly, true);
+}
+
+TEST(Delegate, ExpandNegative)
+{
+ Query query{
+ .isOnly = false,
+ .expandType = ExpandType::None,
+ };
+ Query delegated = delegate(QueryCapabilities{}, query);
+ EXPECT_EQ(delegated.expandType, ExpandType::None);
+}
+
+TEST(Delegate, TopNegative)
+{
+ Query query{
+ .top = 42,
+ };
+ Query delegated = delegate(QueryCapabilities{}, query);
+ EXPECT_EQ(delegated.top, std::nullopt);
+ EXPECT_EQ(query.top, 42);
+}
+
+TEST(Delegate, TopPositive)
+{
+ Query query{
+ .top = 42,
+ };
+ QueryCapabilities capabilities{
+ .canDelegateTop = true,
+ };
+ Query delegated = delegate(capabilities, query);
+ EXPECT_EQ(delegated.top, 42);
+ EXPECT_EQ(query.top, std::nullopt);
+}
+
+TEST(Delegate, SkipNegative)
+{
+ Query query{
+ .skip = 42,
+ };
+ Query delegated = delegate(QueryCapabilities{}, query);
+ EXPECT_EQ(delegated.skip, std::nullopt);
+ EXPECT_EQ(query.skip, 42);
+}
+
+TEST(Delegate, SkipPositive)
+{
+ Query query{
+ .skip = 42,
+ };
+ QueryCapabilities capabilities{
+ .canDelegateSkip = true,
+ };
+ Query delegated = delegate(capabilities, query);
+ EXPECT_EQ(delegated.skip, 42);
+ EXPECT_EQ(query.skip, 0);
+}
+
+TEST(FormatQueryForExpand, NoSubQueryWhenQueryIsEmpty)
+{
+ EXPECT_EQ(formatQueryForExpand(Query{}), "");
+}
+
+TEST(FormatQueryForExpand, NoSubQueryWhenExpandLevelsLeOne)
+{
+ EXPECT_EQ(formatQueryForExpand(
+ Query{.expandLevel = 1, .expandType = ExpandType::Both}),
+ "");
+ EXPECT_EQ(formatQueryForExpand(Query{.expandType = ExpandType::Links}), "");
+ EXPECT_EQ(formatQueryForExpand(Query{.expandType = ExpandType::NotLinks}),
+ "");
+}
+
+TEST(FormatQueryForExpand, NoSubQueryWhenExpandTypeIsNone)
+{
+ EXPECT_EQ(formatQueryForExpand(
+ Query{.expandLevel = 2, .expandType = ExpandType::None}),
+ "");
+}
+
+TEST(FormatQueryForExpand, DelegatedSubQueriesHaveSameTypeAndOneLessLevels)
+{
+ EXPECT_EQ(formatQueryForExpand(
+ Query{.expandLevel = 3, .expandType = ExpandType::Both}),
+ "?$expand=*($levels=2)");
+ EXPECT_EQ(formatQueryForExpand(
+ Query{.expandLevel = 4, .expandType = ExpandType::Links}),
+ "?$expand=~($levels=3)");
+ EXPECT_EQ(formatQueryForExpand(
+ Query{.expandLevel = 2, .expandType = ExpandType::NotLinks}),
+ "?$expand=.($levels=1)");
+}
+
+TEST(IsSelectedPropertyAllowed, NotAllowedCharactersReturnsFalse)
+{
+ EXPECT_FALSE(isSelectedPropertyAllowed("?"));
+ EXPECT_FALSE(isSelectedPropertyAllowed("!"));
+ EXPECT_FALSE(isSelectedPropertyAllowed("-"));
+ EXPECT_FALSE(isSelectedPropertyAllowed("/"));
+}
+
+TEST(IsSelectedPropertyAllowed, EmptyStringReturnsFalse)
+{
+ EXPECT_FALSE(isSelectedPropertyAllowed(""));
+}
+
+TEST(IsSelectedPropertyAllowed, TooLongStringReturnsFalse)
+{
+ std::string strUnderTest = "ab";
+ // 2^10
+ for (int i = 0; i < 10; ++i)
+ {
+ strUnderTest += strUnderTest;
+ }
+ EXPECT_FALSE(isSelectedPropertyAllowed(strUnderTest));
+}
+
+TEST(IsSelectedPropertyAllowed, ValidPropertReturnsTrue)
+{
+ EXPECT_TRUE(isSelectedPropertyAllowed("Chassis"));
+ EXPECT_TRUE(isSelectedPropertyAllowed("@odata.type"));
+ EXPECT_TRUE(isSelectedPropertyAllowed("#ComputerSystem.Reset"));
+ EXPECT_TRUE(isSelectedPropertyAllowed(
+ "BootSourceOverrideTarget@Redfish.AllowableValues"));
+}
+
+TEST(GetSelectParam, EmptyValueReturnsError)
+{
+ Query query;
+ EXPECT_FALSE(getSelectParam("", query));
+}
+
+TEST(GetSelectParam, EmptyPropertyReturnsError)
+{
+ Query query;
+ EXPECT_FALSE(getSelectParam(",", query));
+ EXPECT_FALSE(getSelectParam(",,", query));
+}
+
+TEST(GetSelectParam, InvalidPathPropertyReturnsError)
+{
+ Query query;
+ EXPECT_FALSE(getSelectParam("\0,\0", query));
+ EXPECT_FALSE(getSelectParam("%%%", query));
+}
+
+TEST(GetSelectParam, TrieNodesRespectAllProperties)
+{
+ Query query;
+ ASSERT_TRUE(getSelectParam("foo/bar,bar", query));
+ ASSERT_FALSE(query.selectTrie.root.empty());
+
+ const SelectTrieNode* child = query.selectTrie.root.find("foo");
+ ASSERT_NE(child, nullptr);
+ EXPECT_FALSE(child->isSelected());
+ ASSERT_NE(child->find("bar"), nullptr);
+ EXPECT_TRUE(child->find("bar")->isSelected());
+
+ ASSERT_NE(query.selectTrie.root.find("bar"), nullptr);
+ EXPECT_TRUE(query.selectTrie.root.find("bar")->isSelected());
+}
+
+SelectTrie getTrie(std::span<std::string_view> properties)
+{
+ SelectTrie trie;
+ for (auto const& property : properties)
+ {
+ EXPECT_TRUE(trie.insertNode(property));
+ }
+ return trie;
+}
+
+TEST(RecursiveSelect, ExpectedKeysAreSelectInSimpleObject)
+{
+ std::vector<std::string_view> properties = {"SelectMe"};
+ SelectTrie trie = getTrie(properties);
+ nlohmann::json root = R"({"SelectMe" : "foo", "OmitMe" : "bar"})"_json;
+ nlohmann::json expected = R"({"SelectMe" : "foo"})"_json;
+ recursiveSelect(root, trie.root);
+ EXPECT_EQ(root, expected);
+}
+
+TEST(RecursiveSelect, ExpectedKeysAreSelectInNestedObject)
+{
+ std::vector<std::string_view> properties = {
+ "SelectMe", "Prefix0/ExplicitSelectMe", "Prefix1", "Prefix2",
+ "Prefix4/ExplicitSelectMe"};
+ SelectTrie trie = getTrie(properties);
+ nlohmann::json root = R"(
+{
+ "SelectMe":[
+ "foo"
+ ],
+ "OmitMe":"bar",
+ "Prefix0":{
+ "ExplicitSelectMe":"123",
+ "OmitMe":"456"
+ },
+ "Prefix1":{
+ "ImplicitSelectMe":"123"
+ },
+ "Prefix2":[
+ {
+ "ImplicitSelectMe":"123"
+ }
+ ],
+ "Prefix3":[
+ "OmitMe"
+ ],
+ "Prefix4":[
+ {
+ "ExplicitSelectMe":"123",
+ "OmitMe": "456"
+ }
+ ]
+}
+)"_json;
+ nlohmann::json expected = R"(
+{
+ "SelectMe":[
+ "foo"
+ ],
+ "Prefix0":{
+ "ExplicitSelectMe":"123"
+ },
+ "Prefix1":{
+ "ImplicitSelectMe":"123"
+ },
+ "Prefix2":[
+ {
+ "ImplicitSelectMe":"123"
+ }
+ ],
+ "Prefix4":[
+ {
+ "ExplicitSelectMe":"123"
+ }
+ ]
+}
+)"_json;
+ recursiveSelect(root, trie.root);
+ EXPECT_EQ(root, expected);
+}
+
+TEST(RecursiveSelect, ReservedPropertiesAreSelected)
+{
+ nlohmann::json root = R"(
+{
+ "OmitMe":"bar",
+ "@odata.id":1,
+ "@odata.type":2,
+ "@odata.context":3,
+ "@odata.etag":4,
+ "Prefix1":{
+ "OmitMe":"bar",
+ "@odata.id":1,
+ "ExplicitSelectMe": 1
+ },
+ "Prefix2":[1, 2, 3],
+ "Prefix3":[
+ {
+ "OmitMe":"bar",
+ "@odata.id":1,
+ "ExplicitSelectMe": 1
+ }
+ ]
+}
+)"_json;
+ nlohmann::json expected = R"(
+{
+ "@odata.id":1,
+ "@odata.type":2,
+ "@odata.context":3,
+ "@odata.etag":4,
+ "Prefix1":{
+ "@odata.id":1,
+ "ExplicitSelectMe": 1
+ },
+ "Prefix3":[
+ {
+ "@odata.id":1,
+ "ExplicitSelectMe": 1
+ }
+ ]
+}
+)"_json;
+ auto ret = boost::urls::parse_relative_ref(
+ "/redfish/v1?$select=Prefix1/ExplicitSelectMe,Prefix3/ExplicitSelectMe");
+ ASSERT_TRUE(ret);
+ crow::Response res;
+ std::optional<Query> query = parseParameters(ret->params(), res);
+
+ ASSERT_NE(query, std::nullopt);
+ recursiveSelect(root, query->selectTrie.root);
+ EXPECT_EQ(root, expected);
+}
+
+TEST(PropogateErrorCode, 500IsWorst)
+{
+ constexpr std::array<unsigned, 7> codes = {100, 200, 300, 400,
+ 401, 500, 501};
+ for (auto code : codes)
+ {
+ EXPECT_EQ(propogateErrorCode(500, code), 500);
+ EXPECT_EQ(propogateErrorCode(code, 500), 500);
+ }
+}
+
+TEST(PropogateErrorCode, 5xxAreWorseThanOthers)
+{
+ constexpr std::array<unsigned, 7> codes = {100, 200, 300, 400,
+ 401, 501, 502};
+ for (auto code : codes)
+ {
+ EXPECT_EQ(propogateErrorCode(code, 505), 505);
+ EXPECT_EQ(propogateErrorCode(505, code), 505);
+ }
+ EXPECT_EQ(propogateErrorCode(502, 501), 502);
+ EXPECT_EQ(propogateErrorCode(501, 502), 502);
+ EXPECT_EQ(propogateErrorCode(503, 502), 503);
+}
+
+TEST(PropogateErrorCode, 401IsWorseThanOthers)
+{
+ constexpr std::array<unsigned, 7> codes = {100, 200, 300, 400, 401};
+ for (auto code : codes)
+ {
+ EXPECT_EQ(propogateErrorCode(code, 401), 401);
+ EXPECT_EQ(propogateErrorCode(401, code), 401);
+ }
+}
+
+TEST(PropogateErrorCode, 4xxIsWorseThanOthers)
+{
+ constexpr std::array<unsigned, 7> codes = {100, 200, 300, 400, 402};
+ for (auto code : codes)
+ {
+ EXPECT_EQ(propogateErrorCode(code, 405), 405);
+ EXPECT_EQ(propogateErrorCode(405, code), 405);
+ }
+ EXPECT_EQ(propogateErrorCode(400, 402), 402);
+ EXPECT_EQ(propogateErrorCode(402, 403), 403);
+ EXPECT_EQ(propogateErrorCode(403, 402), 403);
+}
+
+TEST(PropogateError, IntermediateNoErrorMessageMakesNoChange)
+{
+ crow::Response intermediate;
+ intermediate.result(boost::beast::http::status::ok);
+
+ crow::Response finalRes;
+ finalRes.result(boost::beast::http::status::ok);
+ propogateError(finalRes, intermediate);
+ EXPECT_EQ(finalRes.result(), boost::beast::http::status::ok);
+ EXPECT_EQ(finalRes.jsonValue.find("error"), finalRes.jsonValue.end());
+}
+
+TEST(PropogateError, ErrorsArePropergatedWithErrorInRoot)
+{
+ nlohmann::json root = R"(
+{
+ "@odata.type": "#Message.v1_1_1.Message",
+ "Message": "The request failed due to an internal service error. The service is still operational.",
+ "MessageArgs": [],
+ "MessageId": "Base.1.13.0.InternalError",
+ "MessageSeverity": "Critical",
+ "Resolution": "Resubmit the request. If the problem persists, consider resetting the service."
+}
+)"_json;
+ crow::Response intermediate;
+ intermediate.result(boost::beast::http::status::internal_server_error);
+ intermediate.jsonValue = root;
+
+ crow::Response final;
+ final.result(boost::beast::http::status::ok);
+
+ propogateError(final, intermediate);
+
+ EXPECT_EQ(final.jsonValue["error"]["code"].get<std::string>(),
+ "Base.1.13.0.InternalError");
+ EXPECT_EQ(
+ final.jsonValue["error"]["message"].get<std::string>(),
+ "The request failed due to an internal service error. The service is still operational.");
+ EXPECT_EQ(intermediate.jsonValue, R"({})"_json);
+ EXPECT_EQ(final.result(),
+ boost::beast::http::status::internal_server_error);
+}
+
+TEST(PropogateError, ErrorsArePropergatedWithErrorCode)
+{
+ crow::Response intermediate;
+ intermediate.result(boost::beast::http::status::internal_server_error);
+
+ nlohmann::json error = R"(
+{
+ "error": {
+ "@Message.ExtendedInfo": [],
+ "code": "Base.1.13.0.InternalError",
+ "message": "The request failed due to an internal service error. The service is still operational."
+ }
+}
+)"_json;
+ nlohmann::json extendedInfo = R"(
+{
+ "@odata.type": "#Message.v1_1_1.Message",
+ "Message": "The request failed due to an internal service error. The service is still operational.",
+ "MessageArgs": [],
+ "MessageId": "Base.1.13.0.InternalError",
+ "MessageSeverity": "Critical",
+ "Resolution": "Resubmit the request. If the problem persists, consider resetting the service."
+}
+)"_json;
+
+ for (int i = 0; i < 10; ++i)
+ {
+ error["error"][messages::messageAnnotation].push_back(extendedInfo);
+ }
+ intermediate.jsonValue = error;
+ crow::Response final;
+ final.result(boost::beast::http::status::ok);
+
+ propogateError(final, intermediate);
+ EXPECT_EQ(final.jsonValue["error"][messages::messageAnnotation],
+ error["error"][messages::messageAnnotation]);
+ std::string errorCode = messages::messageVersionPrefix;
+ errorCode += "GeneralError";
+ std::string errorMessage =
+ "A general error has occurred. See Resolution for "
+ "information on how to resolve the error.";
+ EXPECT_EQ(final.jsonValue["error"]["code"].get<std::string>(), errorCode);
+ EXPECT_EQ(final.jsonValue["error"]["message"].get<std::string>(),
+ errorMessage);
+ EXPECT_EQ(intermediate.jsonValue, R"({})"_json);
+ EXPECT_EQ(final.result(),
+ boost::beast::http::status::internal_server_error);
+}
+
+TEST(QueryParams, ParseParametersOnly)
+{
+ auto ret = boost::urls::parse_relative_ref("/redfish/v1?only");
+ ASSERT_TRUE(ret);
+
+ crow::Response res;
+ std::optional<Query> query = parseParameters(ret->params(), res);
+ ASSERT_TRUE(query != std::nullopt);
+ EXPECT_TRUE(query->isOnly);
+}
+
+TEST(QueryParams, ParseParametersExpand)
+{
+ auto ret = boost::urls::parse_relative_ref("/redfish/v1?$expand=*");
+ ASSERT_TRUE(ret);
+
+ crow::Response res;
+
+ std::optional<Query> query = parseParameters(ret->params(), res);
+ if constexpr (bmcwebInsecureEnableQueryParams)
+ {
+ ASSERT_NE(query, std::nullopt);
+ EXPECT_TRUE(query->expandType ==
+ redfish::query_param::ExpandType::Both);
+ }
+ else
+ {
+ ASSERT_EQ(query, std::nullopt);
+ }
+}
+
+TEST(QueryParams, ParseParametersTop)
+{
+ auto ret = boost::urls::parse_relative_ref("/redfish/v1?$top=1");
+ ASSERT_TRUE(ret);
+
+ crow::Response res;
+
+ std::optional<Query> query = parseParameters(ret->params(), res);
+ ASSERT_TRUE(query != std::nullopt);
+ EXPECT_EQ(query->top, 1);
+}
+
+TEST(QueryParams, ParseParametersTopOutOfRangeNegative)
+{
+ auto ret = boost::urls::parse_relative_ref("/redfish/v1?$top=-1");
+ ASSERT_TRUE(ret);
+
+ crow::Response res;
+
+ std::optional<Query> query = parseParameters(ret->params(), res);
+ ASSERT_TRUE(query == std::nullopt);
+}
+
+TEST(QueryParams, ParseParametersTopOutOfRangePositive)
+{
+ auto ret = boost::urls::parse_relative_ref("/redfish/v1?$top=1001");
+ ASSERT_TRUE(ret);
+
+ crow::Response res;
+
+ std::optional<Query> query = parseParameters(ret->params(), res);
+ ASSERT_TRUE(query == std::nullopt);
+}
+
+TEST(QueryParams, ParseParametersSkip)
+{
+ auto ret = boost::urls::parse_relative_ref("/redfish/v1?$skip=1");
+ ASSERT_TRUE(ret);
+
+ crow::Response res;
+
+ std::optional<Query> query = parseParameters(ret->params(), res);
+ ASSERT_TRUE(query != std::nullopt);
+ EXPECT_EQ(query->skip, 1);
+}
+TEST(QueryParams, ParseParametersSkipOutOfRange)
+{
+ auto ret = boost::urls::parse_relative_ref(
+ "/redfish/v1?$skip=99999999999999999999");
+ ASSERT_TRUE(ret);
+
+ crow::Response res;
+
+ std::optional<Query> query = parseParameters(ret->params(), res);
+ ASSERT_EQ(query, std::nullopt);
+}
+
+TEST(QueryParams, ParseParametersUnexpectedGetsIgnored)
+{
+ auto ret = boost::urls::parse_relative_ref("/redfish/v1?unexpected_param");
+ ASSERT_TRUE(ret);
+
+ crow::Response res;
+
+ std::optional<Query> query = parseParameters(ret->params(), res);
+ ASSERT_TRUE(query != std::nullopt);
+}
+
+TEST(QueryParams, ParseParametersUnexpectedDollarGetsError)
+{
+ auto ret = boost::urls::parse_relative_ref("/redfish/v1?$unexpected_param");
+ ASSERT_TRUE(ret);
+
+ crow::Response res;
+
+ std::optional<Query> query = parseParameters(ret->params(), res);
+ ASSERT_TRUE(query == std::nullopt);
+ EXPECT_EQ(res.result(), boost::beast::http::status::not_implemented);
+}
+
+TEST(QueryParams, GetExpandType)
+{
+ Query query{};
+
+ EXPECT_FALSE(getExpandType("", query));
+ EXPECT_FALSE(getExpandType(".(", query));
+ EXPECT_FALSE(getExpandType(".()", query));
+ EXPECT_FALSE(getExpandType(".($levels=1", query));
+
+ EXPECT_TRUE(getExpandType("*", query));
+ EXPECT_EQ(query.expandType, ExpandType::Both);
+ EXPECT_TRUE(getExpandType(".", query));
+ EXPECT_EQ(query.expandType, ExpandType::NotLinks);
+ EXPECT_TRUE(getExpandType("~", query));
+ EXPECT_EQ(query.expandType, ExpandType::Links);
+
+ // Per redfish specification, level defaults to 1
+ EXPECT_TRUE(getExpandType(".", query));
+ EXPECT_EQ(query.expandLevel, 1);
+
+ EXPECT_TRUE(getExpandType(".($levels=42)", query));
+ EXPECT_EQ(query.expandLevel, 42);
+
+ // Overflow
+ EXPECT_FALSE(getExpandType(".($levels=256)", query));
+
+ // Negative
+ EXPECT_FALSE(getExpandType(".($levels=-1)", query));
+
+ // No number
+ EXPECT_FALSE(getExpandType(".($levels=a)", query));
+}
+
+TEST(QueryParams, FindNavigationReferencesNonLink)
+{
+ using nlohmann::json;
+
+ json singleTreeNode = R"({"Foo" : {"@odata.id": "/foobar"}})"_json;
+
+ // Parsing as the root should net one entry
+ EXPECT_THAT(findNavigationReferences(ExpandType::Both, singleTreeNode),
+ UnorderedElementsAre(
+ ExpandNode{json::json_pointer("/Foo"), "/foobar"}));
+
+ // Parsing in Non-hyperlinks mode should net one entry
+ EXPECT_THAT(findNavigationReferences(ExpandType::NotLinks, singleTreeNode),
+ UnorderedElementsAre(
+ ExpandNode{json::json_pointer("/Foo"), "/foobar"}));
+
+ // Searching for not types should return empty set
+ EXPECT_TRUE(
+ findNavigationReferences(ExpandType::None, singleTreeNode).empty());
+
+ // Searching for hyperlinks only should return empty set
+ EXPECT_TRUE(
+ findNavigationReferences(ExpandType::Links, singleTreeNode).empty());
+
+ json multiTreeNodes =
+ R"({"Links": {"@odata.id": "/links"}, "Foo" : {"@odata.id": "/foobar"}})"_json;
+ // Should still find Foo
+ EXPECT_THAT(findNavigationReferences(ExpandType::NotLinks, multiTreeNodes),
+ UnorderedElementsAre(
+ ExpandNode{json::json_pointer("/Foo"), "/foobar"}));
+}
+
+TEST(QueryParams, FindNavigationReferencesLink)
+{
+ using nlohmann::json;
+
+ json singleLinkNode =
+ R"({"Links" : {"Sessions": {"@odata.id": "/foobar"}}})"_json;
+
+ // Parsing as the root should net one entry
+ EXPECT_THAT(findNavigationReferences(ExpandType::Both, singleLinkNode),
+ UnorderedElementsAre(ExpandNode{
+ json::json_pointer("/Links/Sessions"), "/foobar"}));
+ // Parsing in hyperlinks mode should net one entry
+ EXPECT_THAT(findNavigationReferences(ExpandType::Links, singleLinkNode),
+ UnorderedElementsAre(ExpandNode{
+ json::json_pointer("/Links/Sessions"), "/foobar"}));
+
+ // Searching for not types should return empty set
+ EXPECT_TRUE(
+ findNavigationReferences(ExpandType::None, singleLinkNode).empty());
+
+ // Searching for non-hyperlinks only should return empty set
+ EXPECT_TRUE(
+ findNavigationReferences(ExpandType::NotLinks, singleLinkNode).empty());
+}
+
+} // namespace
+} // namespace redfish::query_param