Implement odata annotations ignoring

From the quoted section of the spec in the patchset, we should be
ignoring odata annotations on PATCH requests.  This commit implements a
preliminary loop through the json object, and removes the odata items
before processing begins.

Tested:

curl -vvvv --insecure --user root:0penBmc -X PATCH -d '{"@odata.etag":
"my_etag"}' https://192.168.7.2/redfish/v1/AccountService/Accounts/root
returns: Base.1.11.0.NoOperation

Redfish protocol validator now passes the REQ_PATCH_ODATA_PROPS test.

Included unit tests passing.

Signed-off-by: Ed Tanous <edtanous@google.com>
Change-Id: I62be75342681d147b8536fd122bbc793eeaa3788
diff --git a/redfish-core/include/utils/json_utils.hpp b/redfish-core/include/utils/json_utils.hpp
index b688576..317cf11 100644
--- a/redfish-core/include/utils/json_utils.hpp
+++ b/redfish-core/include/utils/json_utils.hpp
@@ -531,13 +531,27 @@
         BMCWEB_LOG_DEBUG << "Json value not readable";
         return std::nullopt;
     }
-
-    if (jsonRequest.empty())
+    nlohmann::json::object_t* object =
+        jsonRequest.get_ptr<nlohmann::json::object_t*>();
+    if (object == nullptr || object->empty())
     {
         BMCWEB_LOG_DEBUG << "Json value is empty";
         messages::emptyJSON(res);
         return std::nullopt;
     }
+    std::erase_if(*object,
+                  [](const std::pair<std::string, nlohmann::json>& item) {
+                      return item.first.starts_with("@odata.");
+                  });
+    if (object->empty())
+    {
+        //  If the update request only contains OData annotations, the service
+        //  should return the HTTP 400 Bad Request status code with the
+        //  NoOperation message from the Base Message Registry, ...
+        messages::noOperation(res);
+        return std::nullopt;
+    }
+
     return {std::move(jsonRequest)};
 }
 
diff --git a/redfish-core/ut/json_utils_test.cpp b/redfish-core/ut/json_utils_test.cpp
index 9312f1a..cf7b734 100644
--- a/redfish-core/ut/json_utils_test.cpp
+++ b/redfish-core/ut/json_utils_test.cpp
@@ -274,6 +274,34 @@
     EXPECT_FALSE(res.jsonValue.empty());
 }
 
+TEST(readJsonPatch, OdataIgnored)
+{
+    crow::Response res;
+    std::error_code ec;
+    crow::Request req({}, ec);
+    // Ignore errors intentionally
+    req.body = R"({"@odata.etag": "etag", "integer": 1})";
+
+    std::optional<int64_t> integer = 0;
+    EXPECT_TRUE(readJsonPatch(req, res, "integer", integer));
+    EXPECT_EQ(res.result(), boost::beast::http::status::ok);
+    EXPECT_TRUE(res.jsonValue.empty());
+}
+
+TEST(readJsonPatch, OnlyOdataGivesNoOperation)
+{
+    crow::Response res;
+    std::error_code ec;
+    crow::Request req({}, ec);
+    // Ignore errors intentionally
+    req.body = R"({"@odata.etag": "etag"})";
+
+    std::optional<int64_t> integer = 0;
+    EXPECT_FALSE(readJsonPatch(req, res, "integer", integer));
+    EXPECT_EQ(res.result(), boost::beast::http::status::bad_request);
+    EXPECT_FALSE(res.jsonValue.empty());
+}
+
 TEST(readJsonAction, ValidElements)
 {
     crow::Response res;