From 8cf41b1d02e33c86169469ea138b8035fd7622aa Mon Sep 17 00:00:00 2001 From: Will Sobel Date: Thu, 22 May 2025 16:03:48 -0400 Subject: [PATCH 1/7] checkpoint --- agent_lib/CMakeLists.txt | 2 ++ error.cpp | 37 ++++++++++++++++++++++++++++++ error.hpp | 49 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 88 insertions(+) create mode 100644 error.cpp create mode 100644 error.hpp diff --git a/agent_lib/CMakeLists.txt b/agent_lib/CMakeLists.txt index bf25989d4..37cfdef25 100644 --- a/agent_lib/CMakeLists.txt +++ b/agent_lib/CMakeLists.txt @@ -260,6 +260,7 @@ set(AGENT_SOURCES # src/sink/rest_sink HEADER_FILE_ONLY "${SOURCE_DIR}/sink/rest_sink/cached_file.hpp" + "${SOURCE_DIR}/sink/rest_sink/error.hpp" "${SOURCE_DIR}/sink/rest_sink/file_cache.hpp" "${SOURCE_DIR}/sink/rest_sink/parameter.hpp" "${SOURCE_DIR}/sink/rest_sink/request.hpp" @@ -274,6 +275,7 @@ set(AGENT_SOURCES # src/sink/rest_sink SOURCE_FILES_ONLY + "${SOURCE_DIR}/sink/rest_sink/error.cpp" "${SOURCE_DIR}/sink/rest_sink/file_cache.cpp" "${SOURCE_DIR}/sink/rest_sink/rest_service.cpp" "${SOURCE_DIR}/sink/rest_sink/server.cpp" diff --git a/error.cpp b/error.cpp new file mode 100644 index 000000000..ddf80c038 --- /dev/null +++ b/error.cpp @@ -0,0 +1,37 @@ +// +// Copyright Copyright 2009-2024, AMT – The Association For Manufacturing Technology (“AMT”) +// All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +#include "error.hpp" +#include "mtconnect/entity/factory.hpp" + +namespace mtconnect::sink::rest_sink { + using namespace mtconnect::entity; + using namespace std; + + entity::FactoryPtr Error::getFactory() + { + static auto error = make_shared(Requirements {{"errorCode", true}, + {"URI", true}, + {"ErrorMessage", false}}); + + return error; + } + + + +} + diff --git a/error.hpp b/error.hpp new file mode 100644 index 000000000..5fcf5f7bb --- /dev/null +++ b/error.hpp @@ -0,0 +1,49 @@ +// +// Copyright Copyright 2009-2024, AMT – The Association For Manufacturing Technology (“AMT”) +// All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +#include "mtconnect/entity/entity.hpp" + +namespace mtconnect::sink::rest_sink { + + class QueryParameter : public mtconnect::entity::Entity + { + + + }; + + class AGENT_LIB_API Error : public mtconnect::entity::Entity { + public: + Error(const std::string &name, const entity::Properties &props) + : entity::Entity(name, props) + { + } + ~Error() override = default; + + /// @brief get the static error factory + /// @return shared pointer to the factory + static entity::FactoryPtr getFactory(); + }; + + class InvalidParameterValue : public Error + { + + + }; + + +} + From ac4b129a3914bd1b57db0bb598347d9a53ba12e9 Mon Sep 17 00:00:00 2001 From: Will Sobel Date: Wed, 4 Jun 2025 22:33:52 -0400 Subject: [PATCH 2/7] Moved error.?pp into the correct position --- error.cpp => src/mtconnect/sink/rest_sink/error.cpp | 0 error.hpp => src/mtconnect/sink/rest_sink/error.hpp | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename error.cpp => src/mtconnect/sink/rest_sink/error.cpp (100%) rename error.hpp => src/mtconnect/sink/rest_sink/error.hpp (100%) diff --git a/error.cpp b/src/mtconnect/sink/rest_sink/error.cpp similarity index 100% rename from error.cpp rename to src/mtconnect/sink/rest_sink/error.cpp diff --git a/error.hpp b/src/mtconnect/sink/rest_sink/error.hpp similarity index 100% rename from error.hpp rename to src/mtconnect/sink/rest_sink/error.hpp From 1ea1d569fbe70628b6566208c486ef70849b8942 Mon Sep 17 00:00:00 2001 From: Will Sobel Date: Sun, 8 Jun 2025 17:06:20 -0400 Subject: [PATCH 3/7] Aded error entities --- src/mtconnect/entity/data_set.hpp | 2 +- src/mtconnect/sink/rest_sink/error.cpp | 105 ++++++++- src/mtconnect/sink/rest_sink/error.hpp | 242 +++++++++++++++++++-- src/mtconnect/sink/rest_sink/parameter.hpp | 3 + src/mtconnect/sink/rest_sink/request.hpp | 2 + 5 files changed, 326 insertions(+), 28 deletions(-) diff --git a/src/mtconnect/entity/data_set.hpp b/src/mtconnect/entity/data_set.hpp index 923ea9e10..95b50b89b 100644 --- a/src/mtconnect/entity/data_set.hpp +++ b/src/mtconnect/entity/data_set.hpp @@ -297,7 +297,7 @@ namespace mtconnect::entity { return std::holds_alternative(m_other) && std::get(m_other) == v; } - const DataSetValue &m_other; //! the other data set value + const DataSetValue &m_other; //! the other data set value }; inline bool DataSetEntry::same(const DataSetEntry &other) const diff --git a/src/mtconnect/sink/rest_sink/error.cpp b/src/mtconnect/sink/rest_sink/error.cpp index ddf80c038..5f870b22a 100644 --- a/src/mtconnect/sink/rest_sink/error.cpp +++ b/src/mtconnect/sink/rest_sink/error.cpp @@ -16,22 +16,105 @@ // #include "error.hpp" + #include "mtconnect/entity/factory.hpp" namespace mtconnect::sink::rest_sink { using namespace mtconnect::entity; using namespace std; - - entity::FactoryPtr Error::getFactory() + + const std::string Error::nameForCode(ErrorCode code) { - static auto error = make_shared(Requirements {{"errorCode", true}, - {"URI", true}, - {"ErrorMessage", false}}); - - return error; + switch (code) + { + case ErrorCode::ASSET_NOT_FOUND: + return "AssetNotFound"; + + case ErrorCode::INTERNAL_ERROR: + return "InternalError"; + + case ErrorCode::INVALID_REQUEST: + return "InvalidRequest"; + + case ErrorCode::INVALID_URI: + return "InvalidURI"; + + case ErrorCode::INVALID_XPATH: + return "InvalidXPath"; + + case ErrorCode::NO_DEVICE: + return "NoDevice"; + + case ErrorCode::OUT_OF_RANGE: + return "OutOfRange"; + + case ErrorCode::QUERY_ERROR: + return "QueryError"; + + case ErrorCode::TOO_MANY: + return "TooMany"; + + case ErrorCode::UNAUTHORIZED: + return "Unauthorized"; + + case ErrorCode::UNSUPPORTED: + return "UNSUPPORTED"; + + case ErrorCode::INVALID_PARAMTER_VALUE: + return "InvalidParamterValue"; + + case ErrorCode::INVALID_QUERY_PARAMETER: + return "InvalidQueryParameter"; + } + + return "InternalError"; + } + + const std::string Error::enumForCode(ErrorCode code) + { + switch (code) + { + case ErrorCode::ASSET_NOT_FOUND: + return "ASSET_NOT_FOUND"; + + case ErrorCode::INTERNAL_ERROR: + return "INTERNAL_ERROR"; + + case ErrorCode::INVALID_REQUEST: + return "INVALID_REQUEST"; + + case ErrorCode::INVALID_URI: + return "INVALID_URI"; + + case ErrorCode::INVALID_XPATH: + return "INVALID_XPATH"; + + case ErrorCode::NO_DEVICE: + return "NO_DEVICE"; + + case ErrorCode::OUT_OF_RANGE: + return "OUT_OF_RANGE"; + + case ErrorCode::QUERY_ERROR: + return "QUERY_ERROR"; + + case ErrorCode::TOO_MANY: + return "TOO_MANY"; + + case ErrorCode::UNAUTHORIZED: + return "UNAUTHORIZED"; + + case ErrorCode::UNSUPPORTED: + return "UNSUPPORTED"; + + case ErrorCode::INVALID_PARAMTER_VALUE: + return "INVALID_PARAMTER_VALUE"; + + case ErrorCode::INVALID_QUERY_PARAMETER: + return "INVALID_QUERY_PARAMETER"; + } + + return "InternalError"; } - - - -} +} // namespace mtconnect::sink::rest_sink diff --git a/src/mtconnect/sink/rest_sink/error.hpp b/src/mtconnect/sink/rest_sink/error.hpp index 5fcf5f7bb..60b5cb760 100644 --- a/src/mtconnect/sink/rest_sink/error.hpp +++ b/src/mtconnect/sink/rest_sink/error.hpp @@ -15,35 +15,245 @@ // limitations under the License. // +#pragma once + #include "mtconnect/entity/entity.hpp" +#include "mtconnect/entity/factory.hpp" namespace mtconnect::sink::rest_sink { - - class QueryParameter : public mtconnect::entity::Entity + + class AGENT_LIB_API Error : public mtconnect::entity::Entity { - - + public: + enum class ErrorCode + { + ASSET_NOT_FOUND, + INTERNAL_ERROR, + INVALID_REQUEST, + INVALID_URI, + INVALID_XPATH, + NO_DEVICE, + OUT_OF_RANGE, + QUERY_ERROR, + TOO_MANY, + UNAUTHORIZED, + UNSUPPORTED, + INVALID_PARAMTER_VALUE, + INVALID_QUERY_PARAMETER + }; + + Error(const std::string &name, const entity::Properties &props) : entity::Entity(name, props) {} + ~Error() override = default; + + /// @brief get the static error factory + /// @return shared pointer to the factory + static entity::FactoryPtr getFactory() + { + static auto error = std::make_shared( + entity::Requirements { + {"errorCode", false}, {"URI", false}, {"Request", false}, {"ErrorMessage", false}}, + [](const std::string &name, entity::Properties &props) -> entity::EntityPtr { + return std::make_shared(name, props); + }); + + return error; + } + + static const std::string nameForCode(ErrorCode code); + static const std::string enumForCode(ErrorCode code); + + static entity::EntityPtr make(const ErrorCode code, const std::string &uri, + std::optional errorMessage, + std::optional request) + { + using namespace std; + + entity::Properties props {{"errorCode", enumForCode(code)}, {"URI", uri}}; + if (errorMessage) + props["ErrorMessage"] = *errorMessage; + if (request) + props["Request"] = *request; + + return std::make_shared(nameForCode(code), props); + } + + entity::EntityPtr makeLegacy() + { + return std::make_shared("Error", + entity::Properties {{"errorCode", getProperty_("errorCode")}, + {"VALUE", getProperty_("ErrorMessage")}}); + } }; - - class AGENT_LIB_API Error : public mtconnect::entity::Entity { + + class AGENT_LIB_API QueryParameter : public entity::Entity + { public: - Error(const std::string &name, const entity::Properties &props) - : entity::Entity(name, props) + QueryParameter(const entity::Properties &props) : entity::Entity("QueryParameter", props) {} + QueryParameter(const std::string &name, const entity::Properties &props) + : entity::Entity(name, props) + {} + + /// @brief get the static error factory + /// @return shared pointer to the factory + static entity::FactoryPtr getFactory() { + static auto qp = std::make_shared( + entity::Requirements {{"name", true}, + {"Value", false}, + {"Type", false}, + {"Format", false}, + {"Minimum", entity::ValueType::INTEGER, false}, + {"Maximum", entity::ValueType::INTEGER, false}}, + [](const std::string &name, entity::Properties &props) -> entity::EntityPtr { + return std::make_shared(name, props); + }); + + return qp; } - ~Error() override = default; + + static entity::EntityPtr make(const entity::Properties &properties) + { + return std::make_shared(properties); + } + }; + + class AGENT_LIB_API InvalidParameterValue : public Error + { + public: + InvalidParameterValue(const entity::Properties &props) : Error("InvalidParameterValue", props) + {} + InvalidParameterValue(const std::string &name, const entity::Properties &props) + : Error(name, props) + {} + ~InvalidParameterValue() override = default; /// @brief get the static error factory /// @return shared pointer to the factory - static entity::FactoryPtr getFactory(); + static entity::FactoryPtr getFactory() + { + static entity::FactoryPtr factory; + if (!factory) + { + factory = std::make_shared(*Error::getFactory()); + factory->addRequirements( + entity::Requirements {{"InvalidParameterValue", entity::ValueType::ENTITY, + QueryParameter::getFactory(), true}}); + factory->setFunction( + [](const std::string &name, entity::Properties &props) -> entity::EntityPtr { + return std::make_shared(name, props); + }); + } + return factory; + } + + static entity::EntityPtr make(const std::string &uri, const std::string &name, + const std::string &value, const std::string &type, + const std::string &format, + std::optional errorMessage, + std::optional request) + { + using namespace std; + + entity::Properties props { + {"errorCode", enumForCode(Error::ErrorCode::INVALID_PARAMTER_VALUE)}, {"URI", uri}}; + if (errorMessage) + props["ErrorMessage"] = *errorMessage; + if (request) + props["Request"] = *request; + + auto qp = std::make_shared(entity::Properties { + {"name", name}, {"Type", type}, {"Format", format}, {"Value", value}}); + props["QueryParameter"] = qp; + + return std::make_shared(props); + } }; - - class InvalidParameterValue : public Error + + class AGENT_LIB_API OutOfRange : public Error { - - + public: + OutOfRange(const entity::Properties &props) : Error("OutOfRange", props) {} + OutOfRange(const std::string &name, const entity::Properties &props) : Error(name, props) {} + ~OutOfRange() override = default; + + /// @brief get the static error factory + /// @return shared pointer to the factory + static entity::FactoryPtr getFactory() + { + static entity::FactoryPtr factory; + if (!factory) + { + factory = std::make_shared(*Error::getFactory()); + factory->addRequirements(entity::Requirements { + {"QueryParameters", entity::ValueType::ENTITY, QueryParameter::getFactory(), true}}); + factory->setFunction( + [](const std::string &name, entity::Properties &props) -> entity::EntityPtr { + return std::make_shared(name, props); + }); + } + return factory; + } + + static entity::EntityPtr make(const std::string &uri, const std::string &name, int64_t value, + int64_t min, int64_t max, std::optional errorMessage, + std::optional request) + { + using namespace std; + + entity::Properties props {{"errorCode", enumForCode(Error::ErrorCode::OUT_OF_RANGE)}, + {"URI", uri}}; + if (errorMessage) + props["ErrorMessage"] = *errorMessage; + if (request) + props["Request"] = *request; + + auto qp = std::make_shared(entity::Properties { + {"name", name}, {"Minimum", min}, {"Maximum", max}, {"Value", value}}); + props["QueryParameter"] = qp; + + return std::make_shared(props); + } }; - -} + class AGENT_LIB_API AssetNotFound : public Error + { + public: + AssetNotFound(const entity::Properties &props) : Error("AssetNotFound", props) {} + AssetNotFound(const std::string &name, const entity::Properties &props) : Error(name, props) {} + ~AssetNotFound() override = default; + /// @brief get the static error factory + /// @return shared pointer to the factory + static entity::FactoryPtr getFactory() + { + static entity::FactoryPtr factory; + if (!factory) + { + factory = std::make_shared(*Error::getFactory()); + factory->addRequirements(entity::Requirements {{"AssetId", true}}); + factory->setFunction( + [](const std::string &name, entity::Properties &props) -> entity::EntityPtr { + return std::make_shared(name, props); + }); + } + return factory; + } + + static entity::EntityPtr make(const std::string &uri, const std::string &assetId, + std::optional errorMessage, + std::optional request) + { + using namespace std; + + entity::Properties props {{"errorCode", enumForCode(Error::ErrorCode::ASSET_NOT_FOUND)}, + {"URI", uri}, + {"AssetId", assetId}}; + if (errorMessage) + props["ErrorMessage"] = *errorMessage; + if (request) + props["Request"] = *request; + + return std::make_shared(props); + } + }; +} // namespace mtconnect::sink::rest_sink diff --git a/src/mtconnect/sink/rest_sink/parameter.hpp b/src/mtconnect/sink/rest_sink/parameter.hpp index cb2879c34..8e11c7006 100644 --- a/src/mtconnect/sink/rest_sink/parameter.hpp +++ b/src/mtconnect/sink/rest_sink/parameter.hpp @@ -22,6 +22,7 @@ #include #include +#include "error.hpp" #include "mtconnect/config.hpp" namespace mtconnect::sink::rest_sink { @@ -29,6 +30,8 @@ namespace mtconnect::sink::rest_sink { class AGENT_LIB_API ParameterError : public std::logic_error { using std::logic_error::logic_error; + + entity::EntityPtr m_error; }; /// @brief The parameter type for a REST request diff --git a/src/mtconnect/sink/rest_sink/request.hpp b/src/mtconnect/sink/rest_sink/request.hpp index 40eb12172..fd314488b 100644 --- a/src/mtconnect/sink/rest_sink/request.hpp +++ b/src/mtconnect/sink/rest_sink/request.hpp @@ -22,6 +22,7 @@ #include +#include "error.hpp" #include "mtconnect/config.hpp" #include "parameter.hpp" @@ -47,6 +48,7 @@ namespace mtconnect::sink::rest_sink { std::string m_contentType; std::string m_body; boost::beast::http::status m_code; + entity::EntityPtr m_error; }; class Session; From 77b09e61dfccbee2898e039797b565a6e8ad9ebe Mon Sep 17 00:00:00 2001 From: Will Sobel Date: Mon, 7 Jul 2025 15:46:59 +0200 Subject: [PATCH 4/7] checkpoint in refactoring the rest response error doc. --- src/mtconnect/entity/data_set.hpp | 24 ++++---- src/mtconnect/sink/rest_sink/error.hpp | 17 +++++- src/mtconnect/sink/rest_sink/parameter.hpp | 67 +++++++++++++++++++--- src/mtconnect/sink/rest_sink/request.hpp | 9 ++- src/mtconnect/sink/rest_sink/routing.hpp | 10 +++- 5 files changed, 101 insertions(+), 26 deletions(-) diff --git a/src/mtconnect/entity/data_set.hpp b/src/mtconnect/entity/data_set.hpp index 55e35631c..3c92193e4 100644 --- a/src/mtconnect/entity/data_set.hpp +++ b/src/mtconnect/entity/data_set.hpp @@ -124,9 +124,9 @@ namespace mtconnect::entity { return m_key == other.m_key && m_removed == other.m_removed && sameValue(other); } - std::string m_key; ///< The key of the entry - T m_value; ///< The value of the entry - bool m_removed; ///< boolean indicator if this entry is removed. + std::string m_key; //! The key of the entry + T m_value; //! The value of the entry + bool m_removed; //! boolean indicator if this entry is removed. }; /// @brief A set of data set entries @@ -171,10 +171,10 @@ namespace mtconnect::entity { /// @brief Data Set Value type enumeration enum class TabelCellType : std::uint16_t { - EMPTY = 0x0, ///< monostate for no value - STRING = 0x02, ///< string value - INTEGER = 0x3, ///< 64 bit integer - DOUBLE = 0x4 ///< double + EMPTY = 0x0, //! monostate for no value + STRING = 0x02, //! string value + INTEGER = 0x3, //! 64 bit integer + DOUBLE = 0x4 //! double }; /// @brief Table Cell value variant @@ -216,11 +216,11 @@ namespace mtconnect::entity { /// @brief Data Set Value type enumeration enum class DataSetValueType : std::uint16_t { - EMPTY = 0x0, ///< monostate for no value - TABLE_ROW = 0x01, ///< data set member for tables - STRING = 0x02, ///< string value - INTEGER = 0x3, ///< 64 bit integer - DOUBLE = 0x4 ///< double + EMPTY = 0x0, //! monostate for no value + TABLE_ROW = 0x01, //! data set member for tables + STRING = 0x02, //! string value + INTEGER = 0x3, //! 64 bit integer + DOUBLE = 0x4 //! double }; /// @brief Data set value variant diff --git a/src/mtconnect/sink/rest_sink/error.hpp b/src/mtconnect/sink/rest_sink/error.hpp index 60b5cb760..ea6827806 100644 --- a/src/mtconnect/sink/rest_sink/error.hpp +++ b/src/mtconnect/sink/rest_sink/error.hpp @@ -150,7 +150,7 @@ namespace mtconnect::sink::rest_sink { const std::string &value, const std::string &type, const std::string &format, std::optional errorMessage, - std::optional request) + std::optional request = std::nullopt) { using namespace std; @@ -256,4 +256,19 @@ namespace mtconnect::sink::rest_sink { return std::make_shared(props); } }; + + class AGENT_LIB_API RestError : public std::logic_error + { + public: + RestError(entity::EntityPtr error) : std::logic_error("REST Error"), m_error(error) {} + RestError(const char *what, entity::EntityPtr error = nullptr) : std::logic_error(what), m_error(error) {} + RestError(const std::string &what, entity::EntityPtr error = nullptr) : std::logic_error(what), m_error(error) {} + + auto getError() const { return m_error; } + void setError(entity::EntityPtr error) { m_error = error; } + + protected: + entity::EntityPtr m_error; + }; + } // namespace mtconnect::sink::rest_sink diff --git a/src/mtconnect/sink/rest_sink/parameter.hpp b/src/mtconnect/sink/rest_sink/parameter.hpp index 8e11c7006..0fcb06a89 100644 --- a/src/mtconnect/sink/rest_sink/parameter.hpp +++ b/src/mtconnect/sink/rest_sink/parameter.hpp @@ -27,11 +27,10 @@ namespace mtconnect::sink::rest_sink { /// @brief Parameter related errors thrown during interpreting a REST request - class AGENT_LIB_API ParameterError : public std::logic_error + class AGENT_LIB_API ParameterError : public RestError { - using std::logic_error::logic_error; - - entity::EntityPtr m_error; + ParameterError(entity::EntityPtr error) : RestError("Parameter Error", error) {} + using RestError::RestError; }; /// @brief The parameter type for a REST request @@ -67,15 +66,69 @@ namespace mtconnect::sink::rest_sink { {} Parameter(const Parameter &o) = default; + /// @brief to support std::set interface + bool operator<(const Parameter &o) const { return m_name < o.m_name; } + + const std::string getTypeName() const + { + switch(m_type) + { + + case NONE: + return "unknown"; + + case STRING: + return "string"; + + case INTEGER: + return "integer"; + + case UNSIGNED_INTEGER: + return "integer"; + + case DOUBLE: + return "double"; + + case BOOL: + return "boolean"; + } + + return "unknown"; + } + + const std::string getTypeFormat() const + { + switch(m_type) + { + case NONE: + return "unknown"; + + case STRING: + return "string"; + + case INTEGER: + return "in32"; + + case UNSIGNED_INTEGER: + return "uint64"; + + case DOUBLE: + return "double"; + + case BOOL: + return "bool"; + } + + return "unknown"; + } + + std::string m_name; ParameterType m_type {STRING}; /// @brief Default value if one is available ParameterValue m_default; UrlPart m_part {PATH}; - /// @brief to support std::set interface - bool operator<(const Parameter &o) const { return m_name < o.m_name; } - std::optional m_description; }; diff --git a/src/mtconnect/sink/rest_sink/request.hpp b/src/mtconnect/sink/rest_sink/request.hpp index fd314488b..e6913bf59 100644 --- a/src/mtconnect/sink/rest_sink/request.hpp +++ b/src/mtconnect/sink/rest_sink/request.hpp @@ -28,11 +28,11 @@ namespace mtconnect::sink::rest_sink { /// @brief An error that occurred during a request - class AGENT_LIB_API RequestError : public std::logic_error + class AGENT_LIB_API RequestError : public RestError { public: - /// @brief Create a simple error message related to a request - RequestError(const char *w) : std::logic_error::logic_error(w) {} + using RestError::RestError; + /// @brief Create a request error /// @param w the message /// @param body the body of the request @@ -40,7 +40,7 @@ namespace mtconnect::sink::rest_sink { /// @param code the boost status code RequestError(const char *w, const std::string &body, const std::string &type, boost::beast::http::status code) - : std::logic_error::logic_error(w), m_contentType(type), m_body(body), m_code(code) + : RestError(w), m_contentType(type), m_body(body), m_code(code) {} RequestError(const RequestError &) = default; ~RequestError() override = default; @@ -48,7 +48,6 @@ namespace mtconnect::sink::rest_sink { std::string m_contentType; std::string m_body; boost::beast::http::status m_code; - entity::EntityPtr m_error; }; class Session; diff --git a/src/mtconnect/sink/rest_sink/routing.hpp b/src/mtconnect/sink/rest_sink/routing.hpp index bfa584eeb..18fa1a2ec 100644 --- a/src/mtconnect/sink/rest_sink/routing.hpp +++ b/src/mtconnect/sink/rest_sink/routing.hpp @@ -206,7 +206,15 @@ namespace mtconnect::sink::rest_sink { catch (ParameterError &e) { std::string msg = std::string("for query parameter '") + p.m_name + "': " + e.what(); - throw ParameterError(msg); + + auto error = InvalidParameterValue::make(request->m_path, + p.m_name, + q->second, + p.getTypeName(), + p.getTypeFormat(), + msg); + + throw ParameterError(msg, error); } } else if (!std::holds_alternative(p.m_default)) From c962d907cd24550bdc7ba9c75f10a4c81ac5fbd0 Mon Sep 17 00:00:00 2001 From: Will Sobel Date: Mon, 25 Aug 2025 15:47:06 +0200 Subject: [PATCH 5/7] Refactored error handling --- src/mtconnect/printer/json_printer.cpp | 34 +--- src/mtconnect/printer/json_printer.hpp | 2 +- src/mtconnect/printer/printer.hpp | 13 +- src/mtconnect/printer/xml_printer.cpp | 23 ++- src/mtconnect/printer/xml_printer.hpp | 4 +- src/mtconnect/sink/rest_sink/error.cpp | 2 +- src/mtconnect/sink/rest_sink/error.hpp | 172 +++++++++++++++--- src/mtconnect/sink/rest_sink/parameter.hpp | 6 +- src/mtconnect/sink/rest_sink/request.hpp | 30 +-- src/mtconnect/sink/rest_sink/response.hpp | 7 +- src/mtconnect/sink/rest_sink/rest_service.cpp | 124 +++++-------- src/mtconnect/sink/rest_sink/rest_service.hpp | 39 +++- src/mtconnect/sink/rest_sink/routing.hpp | 94 +++++----- src/mtconnect/sink/rest_sink/server.hpp | 19 +- src/mtconnect/sink/rest_sink/session.hpp | 7 +- test_package/xml_printer_test.cpp | 18 +- 16 files changed, 330 insertions(+), 264 deletions(-) diff --git a/src/mtconnect/printer/json_printer.cpp b/src/mtconnect/printer/json_printer.cpp index cfb5635f3..74805e205 100644 --- a/src/mtconnect/printer/json_printer.cpp +++ b/src/mtconnect/printer/json_printer.cpp @@ -117,7 +117,7 @@ namespace mtconnect::printer { } std::string JsonPrinter::printErrors(const uint64_t instanceId, const unsigned int bufferSize, - const uint64_t nextSeq, const ProtoErrorList &list, + const uint64_t nextSeq, const entity::EntityList &list, bool pretty, const std::optional requestId) const { @@ -125,6 +125,8 @@ namespace mtconnect::printer { StringBuffer output; RenderJson(output, m_pretty || pretty, [&](auto &writer) { + entity::JsonPrinter printer(writer, m_jsonVersion); + AutoJsonObject obj(writer); { AutoJsonObject obj(writer, "MTConnectError"); @@ -136,34 +138,8 @@ namespace mtconnect::printer { m_modelChangeTime, m_validation, requestId); } { - if (m_jsonVersion > 1) - { - AutoJsonObject obj(writer, "Errors"); - { - AutoJsonArray ary(writer, "Error"); - for (auto &e : list) - { - AutoJsonObject obj(writer); - string s(e.second); - obj.AddPairs("errorCode", e.first, "value", trim(s)); - } - } - } - else - { - AutoJsonArray obj(writer, "Errors"); - { - for (auto &e : list) - { - AutoJsonObject obj(writer); - { - AutoJsonObject obj(writer, "Error"); - string s(e.second); - obj.AddPairs("errorCode", e.first, "value", trim(s)); - } - } - } - } + obj.Key("Errors"); + printer.printEntityList(list); } } }); diff --git a/src/mtconnect/printer/json_printer.hpp b/src/mtconnect/printer/json_printer.hpp index 119f77b89..1b8d76bbb 100644 --- a/src/mtconnect/printer/json_printer.hpp +++ b/src/mtconnect/printer/json_printer.hpp @@ -32,7 +32,7 @@ namespace mtconnect::printer { std::string printErrors( const uint64_t instanceId, const unsigned int bufferSize, const uint64_t nextSeq, - const ProtoErrorList &list, bool pretty = false, + const entity::EntityList &list, bool pretty = false, const std::optional requestId = std::nullopt) const override; std::string printProbe( diff --git a/src/mtconnect/printer/printer.hpp b/src/mtconnect/printer/printer.hpp index d6526a98b..22b2dae64 100644 --- a/src/mtconnect/printer/printer.hpp +++ b/src/mtconnect/printer/printer.hpp @@ -58,25 +58,28 @@ namespace mtconnect { /// @param[in] instanceId the instance id /// @param[in] bufferSize the buffer size /// @param[in] nextSeq the next sequence - /// @param[in] errorCode an error code - /// @param[in] errorText the error text + /// @param[in] error the error being reported + /// @param[in] pretty `true` if content should be pretty printed + /// @param[in] requestId optional request id to include in the header /// @return the error document virtual std::string printError( const uint64_t instanceId, const unsigned int bufferSize, const uint64_t nextSeq, - const std::string &errorCode, const std::string &errorText, bool pretty = false, + entity::EntityPtr error, bool pretty = false, const std::optional requestId = std::nullopt) const { - return printErrors(instanceId, bufferSize, nextSeq, {{errorCode, errorText}}); + return printErrors(instanceId, bufferSize, nextSeq, entity::EntityList {error}, pretty, requestId); } /// @brief Generate an MTConnect Error document with a error list /// @param[in] instanceId the instance id /// @param[in] bufferSize the buffer size /// @param[in] nextSeq the next sequence /// @param[in] list the list of errors + /// @param[in] pretty `true` if content should be pretty printed + /// @param[in] requestId optional request id to include in the header /// @return the MTConnect Error document virtual std::string printErrors( const uint64_t instanceId, const unsigned int bufferSize, const uint64_t nextSeq, - const ProtoErrorList &list, bool pretty = false, + const entity::EntityList &list, bool pretty = false, const std::optional requestId = std::nullopt) const = 0; /// @brief Generate an MTConnect Devices document /// @param[in] instanceId the instance id diff --git a/src/mtconnect/printer/xml_printer.cpp b/src/mtconnect/printer/xml_printer.cpp index c5ae680c1..4f1e5011b 100644 --- a/src/mtconnect/printer/xml_printer.cpp +++ b/src/mtconnect/printer/xml_printer.cpp @@ -30,6 +30,7 @@ #include "mtconnect/asset/cutting_tool.hpp" #include "mtconnect/device_model/composition.hpp" #include "mtconnect/device_model/configuration/configuration.hpp" +#include "mtconnect/sink/rest_sink/error.hpp" #include "mtconnect/device_model/device.hpp" #include "mtconnect/logging.hpp" #include "mtconnect/version.h" @@ -343,7 +344,7 @@ namespace mtconnect::printer { } std::string XmlPrinter::printErrors(const uint64_t instanceId, const unsigned int bufferSize, - const uint64_t nextSeq, const ProtoErrorList &list, + const uint64_t nextSeq, const entity::EntityList &list, bool pretty, const std::optional requestId) const { string ret; @@ -357,9 +358,18 @@ namespace mtconnect::printer { { AutoElement e1(writer, "Errors"); + entity::XmlPrinter printer; + + auto version = IntSchemaVersion(*m_schemaVersion); for (auto &e : list) { - addSimpleElement(writer, "Error", e.second, {{"errorCode", e.first}}); + entity::EntityPtr err { e }; + if (version < SCHEMA_VERSION(2, 6)) + { + auto re = dynamic_pointer_cast(err); + err = re->makeLegacyError(); + } + printer.print(writer, err, m_errorNsSet); } } closeElement(writer); // MTConnectError @@ -679,12 +689,9 @@ namespace mtconnect::printer { if (requestId) addAttribute(writer, "requestId", *requestId); - int major, minor; - char c; - stringstream v(*m_schemaVersion); - v >> major >> c >> minor; + auto schemaVersion = IntSchemaVersion(*m_schemaVersion); - if (major > 1 || (major == 1 && minor >= 7)) + if (schemaVersion >= SCHEMA_VERSION(1, 7)) { addAttribute(writer, "deviceModelChangeTime", m_modelChangeTime); } @@ -708,7 +715,7 @@ namespace mtconnect::printer { addAttribute(writer, "lastSequence", to_string(lastSeq)); } - if (major < 2 && aType == eDEVICES && count && !count->empty()) + if (schemaVersion < SCHEMA_VERSION(2, 0) && aType == eDEVICES && count && !count->empty()) { AutoElement ele(writer, "AssetCounts"); diff --git a/src/mtconnect/printer/xml_printer.hpp b/src/mtconnect/printer/xml_printer.hpp index 0ab050bef..33d08c0fe 100644 --- a/src/mtconnect/printer/xml_printer.hpp +++ b/src/mtconnect/printer/xml_printer.hpp @@ -45,7 +45,7 @@ namespace mtconnect { std::string printErrors( const uint64_t instanceId, const unsigned int bufferSize, const uint64_t nextSeq, - const ProtoErrorList &list, bool pretty = false, + const entity::EntityList &list, bool pretty = false, const std::optional requestId = std::nullopt) const override; std::string printProbe( @@ -64,7 +64,7 @@ namespace mtconnect { const uint64_t anInstanceId, const unsigned int bufferSize, const unsigned int assetCount, const asset::AssetList &asset, bool pretty = false, const std::optional requestId = std::nullopt) const override; - std::string mimeType() const override { return "text/xml"; } + std::string mimeType() const override { return "application/mtconnect+xml"; } /// @brief Print a single device in XML /// @param device A device poiinter diff --git a/src/mtconnect/sink/rest_sink/error.cpp b/src/mtconnect/sink/rest_sink/error.cpp index 5f870b22a..8839f8814 100644 --- a/src/mtconnect/sink/rest_sink/error.cpp +++ b/src/mtconnect/sink/rest_sink/error.cpp @@ -58,7 +58,7 @@ namespace mtconnect::sink::rest_sink { return "Unauthorized"; case ErrorCode::UNSUPPORTED: - return "UNSUPPORTED"; + return "Unsupported"; case ErrorCode::INVALID_PARAMTER_VALUE: return "InvalidParamterValue"; diff --git a/src/mtconnect/sink/rest_sink/error.hpp b/src/mtconnect/sink/rest_sink/error.hpp index ea6827806..ae83f9206 100644 --- a/src/mtconnect/sink/rest_sink/error.hpp +++ b/src/mtconnect/sink/rest_sink/error.hpp @@ -17,10 +17,15 @@ #pragma once +#include + #include "mtconnect/entity/entity.hpp" #include "mtconnect/entity/factory.hpp" +#include +#include "mtconnect/printer/printer.hpp" namespace mtconnect::sink::rest_sink { + using status = boost::beast::http::status; class AGENT_LIB_API Error : public mtconnect::entity::Entity { @@ -44,7 +49,11 @@ namespace mtconnect::sink::rest_sink { Error(const std::string &name, const entity::Properties &props) : entity::Entity(name, props) {} ~Error() override = default; - + + void setURI(const std::string &uri) { setProperty("URI", uri); } + void setRequest(const std::string &request) { setProperty("Request", request); } + void setErrorMessage(const std::string &message) { setProperty("ErrorMessage", message); } + /// @brief get the static error factory /// @return shared pointer to the factory static entity::FactoryPtr getFactory() @@ -61,14 +70,14 @@ namespace mtconnect::sink::rest_sink { static const std::string nameForCode(ErrorCode code); static const std::string enumForCode(ErrorCode code); - - static entity::EntityPtr make(const ErrorCode code, const std::string &uri, - std::optional errorMessage, - std::optional request) + + static entity::EntityPtr make(const ErrorCode code, + std::optional errorMessage = std::nullopt, + std::optional request = std::nullopt) { using namespace std; - entity::Properties props {{"errorCode", enumForCode(code)}, {"URI", uri}}; + entity::Properties props {{"errorCode", enumForCode(code)}}; if (errorMessage) props["ErrorMessage"] = *errorMessage; if (request) @@ -77,14 +86,16 @@ namespace mtconnect::sink::rest_sink { return std::make_shared(nameForCode(code), props); } - entity::EntityPtr makeLegacy() + entity::EntityPtr makeLegacyError() const { return std::make_shared("Error", - entity::Properties {{"errorCode", getProperty_("errorCode")}, - {"VALUE", getProperty_("ErrorMessage")}}); + entity::Properties {{"errorCode", getProperty("errorCode")}, + {"VALUE", getProperty("ErrorMessage")}}); } }; + using ErrorPtr = std::shared_ptr; + class AGENT_LIB_API QueryParameter : public entity::Entity { public: @@ -146,16 +157,16 @@ namespace mtconnect::sink::rest_sink { return factory; } - static entity::EntityPtr make(const std::string &uri, const std::string &name, + static entity::EntityPtr make(const std::string &name, const std::string &value, const std::string &type, const std::string &format, - std::optional errorMessage, + std::optional errorMessage = std::nullopt, std::optional request = std::nullopt) { using namespace std; entity::Properties props { - {"errorCode", enumForCode(Error::ErrorCode::INVALID_PARAMTER_VALUE)}, {"URI", uri}}; + {"errorCode", enumForCode(Error::ErrorCode::INVALID_PARAMTER_VALUE)}}; if (errorMessage) props["ErrorMessage"] = *errorMessage; if (request) @@ -194,14 +205,13 @@ namespace mtconnect::sink::rest_sink { return factory; } - static entity::EntityPtr make(const std::string &uri, const std::string &name, int64_t value, - int64_t min, int64_t max, std::optional errorMessage, - std::optional request) + static entity::EntityPtr make(const std::string &name, int64_t value, + int64_t min, int64_t max, std::optional errorMessage = std::nullopt, + std::optional request = std::nullopt) { using namespace std; - entity::Properties props {{"errorCode", enumForCode(Error::ErrorCode::OUT_OF_RANGE)}, - {"URI", uri}}; + entity::Properties props {{"errorCode", enumForCode(Error::ErrorCode::OUT_OF_RANGE)}}; if (errorMessage) props["ErrorMessage"] = *errorMessage; if (request) @@ -239,14 +249,13 @@ namespace mtconnect::sink::rest_sink { return factory; } - static entity::EntityPtr make(const std::string &uri, const std::string &assetId, - std::optional errorMessage, - std::optional request) + static entity::EntityPtr make(const std::string &assetId, + std::optional errorMessage = std::nullopt, + std::optional request = std::nullopt) { using namespace std; entity::Properties props {{"errorCode", enumForCode(Error::ErrorCode::ASSET_NOT_FOUND)}, - {"URI", uri}, {"AssetId", assetId}}; if (errorMessage) props["ErrorMessage"] = *errorMessage; @@ -257,18 +266,125 @@ namespace mtconnect::sink::rest_sink { } }; - class AGENT_LIB_API RestError : public std::logic_error + class AGENT_LIB_API RestError { public: - RestError(entity::EntityPtr error) : std::logic_error("REST Error"), m_error(error) {} - RestError(const char *what, entity::EntityPtr error = nullptr) : std::logic_error(what), m_error(error) {} - RestError(const std::string &what, entity::EntityPtr error = nullptr) : std::logic_error(what), m_error(error) {} + /// @brief An exception that gets thrown during REST processing with an error and a status + /// @param error the error entity + /// @param accepts the accepted mime types, defaults to application/xml + /// @param status the HTTP status code, defaults to 400 Bad Request + /// @param format optional format for the error + RestError(entity::EntityPtr error, + std::string accepts = "application/xml", + status st = status::bad_request, + std::optional format = std::nullopt, + std::optional request = std::nullopt) : m_errors({error}), m_accepts(accepts), m_status(st), m_format(format) + { + if (request) + setRequest(*request); + } + + /// @brief An exception that gets thrown during REST processing with an error and a status + /// @param errors a list of errors + /// @param accepts the accepted mime types, defaults to application/xml + /// @param status the HTTP status code, defaults to 400 Bad Request + /// @param format optional format for the error + RestError(entity::EntityList &errors, + std::string accepts = "application/xml", + status st = status::bad_request, + std::optional format = std::nullopt, + std::optional request = std::nullopt) : m_errors(errors), + m_accepts(accepts), m_status(st), m_format(format) + { + if (request) + setRequest(*request); + } + + /// @brief An exception that gets thrown during REST processing with an error and a status + /// @param error the error entity + /// @param printer the printer to generate the error document + /// @param status the HTTP status code, defaults to 400 Bad Request + /// @param format optional format for the error + RestError(entity::EntityPtr error, + const printer::Printer* printer = nullptr, + status st = status::bad_request, + std::optional format = std::nullopt, + std::optional request = std::nullopt) : + m_errors({error}), m_status(st), m_format(format), m_printer(printer) + { + if (request) + setRequest(*request); + } + + /// @brief An exception that gets thrown during REST processing with an error and a status + /// @param errors a list of errors + /// @param printer the printer to generate the error document + /// @param status the HTTP status code, defaults to 400 Bad Request + /// @param format optional format for the error + RestError(entity::EntityList &errors, + const printer::Printer* printer = nullptr, + status st = status::bad_request, + std::optional format = std::nullopt, + std::optional request = std::nullopt) : m_errors(errors), + m_status(st), m_format(format), m_printer(printer) + { + if (request) + setRequest(*request); + } - auto getError() const { return m_error; } - void setError(entity::EntityPtr error) { m_error = error; } + + ~RestError() = default; + RestError(const RestError &o) = default; + + /// @brief set the URI for all errors + /// @param uri the URI + void setUri(const std::string &uri) + { + for (auto &e : m_errors) + e->setProperty("URI", uri); + } + + /// @brief set the Request for all errors + /// @param request the Request + void setRequest(const std::string &request) + { + m_request = request; + for (auto &e : m_errors) + e->setProperty("Request", request); + } + + const auto &getErrors() const { return m_errors; } + void setStatus(status st) { m_status = st; } + const auto &getStatus() const { return m_status; } + void setFormat(const std::string &format) { m_format = format; } + const auto &getFormat() const { return m_format; } + const auto &getAccepts() const { return m_accepts; } + const auto &getRequest() const { return m_request; } + auto getPrinter() const { return m_printer; } + + std::string what() const + { + std::stringstream ss; + for (const auto &e : m_errors) + { + ss << e->getName() << ": "; + auto message = e->maybeGet("ErrorMessage"); + if (message) + ss << *message; + ss << ", "; + } + + return ss.str(); + } + protected: - entity::EntityPtr m_error; + entity::EntityList m_errors; + std::string m_accepts { "application/xml" }; + status m_status; + std::optional m_format; + std::optional m_request; + const printer::Printer* m_printer { nullptr }; }; } // namespace mtconnect::sink::rest_sink diff --git a/src/mtconnect/sink/rest_sink/parameter.hpp b/src/mtconnect/sink/rest_sink/parameter.hpp index 0fcb06a89..0ad20f9db 100644 --- a/src/mtconnect/sink/rest_sink/parameter.hpp +++ b/src/mtconnect/sink/rest_sink/parameter.hpp @@ -21,16 +21,16 @@ #include #include #include +#include #include "error.hpp" #include "mtconnect/config.hpp" namespace mtconnect::sink::rest_sink { /// @brief Parameter related errors thrown during interpreting a REST request - class AGENT_LIB_API ParameterError : public RestError + class AGENT_LIB_API ParameterError : public std::invalid_argument { - ParameterError(entity::EntityPtr error) : RestError("Parameter Error", error) {} - using RestError::RestError; + using std::invalid_argument::invalid_argument; }; /// @brief The parameter type for a REST request diff --git a/src/mtconnect/sink/rest_sink/request.hpp b/src/mtconnect/sink/rest_sink/request.hpp index e6913bf59..8b9615210 100644 --- a/src/mtconnect/sink/rest_sink/request.hpp +++ b/src/mtconnect/sink/rest_sink/request.hpp @@ -27,29 +27,6 @@ #include "parameter.hpp" namespace mtconnect::sink::rest_sink { - /// @brief An error that occurred during a request - class AGENT_LIB_API RequestError : public RestError - { - public: - using RestError::RestError; - - /// @brief Create a request error - /// @param w the message - /// @param body the body of the request - /// @param type the request type - /// @param code the boost status code - RequestError(const char *w, const std::string &body, const std::string &type, - boost::beast::http::status code) - : RestError(w), m_contentType(type), m_body(body), m_code(code) - {} - RequestError(const RequestError &) = default; - ~RequestError() override = default; - - std::string m_contentType; - std::string m_body; - boost::beast::http::status m_code; - }; - class Session; using SessionPtr = std::shared_ptr; @@ -72,9 +49,14 @@ namespace mtconnect::sink::rest_sink { QueryMap m_query; ///< The parsed query parameters ParameterMap m_parameters; ///< The parsed path parameters + /// @name Websocket related properties + ///@{ + std::optional m_requestId; ///< Request id from websocket sub std::optional m_command; ///< Specific request from websocket + ///@} + /// @brief Find a parameter by type /// @tparam T the type of the parameter /// @param s the name of the parameter @@ -90,5 +72,5 @@ namespace mtconnect::sink::rest_sink { } }; - using RequestPtr = std::shared_ptr; + using RequestPtr = std::shared_ptr; } // namespace mtconnect::sink::rest_sink diff --git a/src/mtconnect/sink/rest_sink/response.hpp b/src/mtconnect/sink/rest_sink/response.hpp index b19fecff1..b4cd5b5e5 100644 --- a/src/mtconnect/sink/rest_sink/response.hpp +++ b/src/mtconnect/sink/rest_sink/response.hpp @@ -33,6 +33,7 @@ namespace mtconnect { class Printer; } namespace sink::rest_sink { + class RestService; using status = boost::beast::http::status; /// @brief A response for a simple request request returning some content @@ -51,11 +52,7 @@ namespace mtconnect { /// @param[in] file the file Response(status status, CachedFilePtr file) : m_status(status), m_mimeType(file->m_mimeType), m_expires(0), m_file(file) - {} - /// @brief Creates a response with an error - /// @param[in] e the error - Response(RequestError &e) : m_status(e.m_code), m_body(e.m_body), m_mimeType(e.m_contentType) - {} + {} status m_status; ///< The return status std::string m_body; ///< The body of the response diff --git a/src/mtconnect/sink/rest_sink/rest_service.cpp b/src/mtconnect/sink/rest_sink/rest_service.cpp index f4b9bea7a..5124f8c40 100644 --- a/src/mtconnect/sink/rest_sink/rest_service.cpp +++ b/src/mtconnect/sink/rest_sink/rest_service.cpp @@ -26,6 +26,7 @@ #include "mtconnect/pipeline/timestamp_extractor.hpp" #include "mtconnect/printer/xml_printer.hpp" #include "server.hpp" +#include "error.hpp" namespace asio = boost::asio; using namespace std; @@ -50,6 +51,9 @@ namespace mtconnect { m_options(options), m_logStreamData(GetOption(options, config::LogStreams).value_or(false)) { + using placeholders::_1; + using placeholders::_2; + auto maxSize = ConvertFileSize(options, mtconnect::configuration::MaxCachedFileSize, 20 * 1024); auto compressSize = @@ -65,16 +69,7 @@ namespace mtconnect { loadHttpHeaders(config); m_server = make_unique(context, m_options); - m_server->setErrorFunction( - [this](SessionPtr session, rest_sink::status st, const string &msg) { - if (m_sinkContract) - { - auto printer = m_sinkContract->getPrinter("xml"); - auto doc = printError(printer, "INVALID_REQUEST", msg); - ResponsePtr resp = std::make_unique(st, doc, printer->mimeType()); - session->writeFailureResponse(std::move(resp)); - } - }); + m_server->setErrorFunction(boost::bind(&RestService::writeErrorResponse, this, _1, _2)); auto xmlPrinter = dynamic_cast(m_sinkContract->getPrinter("xml")); @@ -562,13 +557,8 @@ namespace mtconnect { } else { - auto format = request->parameter("format"); - auto printer = getPrinter(request->m_accepts, format); - auto error = printError(printer, "INVALID_REQUEST", "No asset given"); - - respond(session, - make_unique(rest_sink::status::bad_request, error, printer->mimeType()), - request->m_requestId); + auto error = Error::make(Error::ErrorCode::INVALID_REQUEST, "No asset given"); + throw RestError(error, request->m_accepts, rest_sink::status::bad_request); } return true; }; @@ -1105,14 +1095,13 @@ namespace mtconnect { return end; } - catch (RequestError &re) + catch (RestError &re) { LOG(error) << asyncResponse->m_session->getRemote().address() << ": Error processing request: " << re.what(); if (asyncResponse->m_session) { - ResponsePtr resp = std::make_unique(re); - asyncResponse->m_session->writeResponse(std::move(resp)); + writeErrorResponse(asyncResponse->m_session, re); asyncResponse->m_session->close(); } } @@ -1237,14 +1226,13 @@ namespace mtconnect { asyncResponse->getRequestId()); } } - catch (RequestError &re) + catch (RestError &re) { LOG(error) << asyncResponse->m_session->getRemote().address() << ": Error processing request: " << re.what(); if (asyncResponse->m_session) { - ResponsePtr resp = std::make_unique(re); - asyncResponse->m_session->writeResponse(std::move(resp)); + writeErrorResponse(asyncResponse->m_session, re); asyncResponse->m_session->close(); } } @@ -1296,15 +1284,11 @@ namespace mtconnect { AssetList list; if (m_sinkContract->getAssetStorage()->getAssets(list, ids) == 0) { - stringstream str; - str << "Cannot find asset for asset Ids: "; + entity::EntityList errors; for (auto &id : ids) - str << id << ", "; - - auto message = str.str().substr(0, str.str().size() - 2); - return make_unique( - status::not_found, printError(printer, "ASSET_NOT_FOUND", message, pretty, requestId), - printer->mimeType()); + errors.emplace_back(AssetNotFound::make(id, "Cannot find asset: " + id)); + throw RestError(errors, printer, status::not_found, + std::nullopt, requestId); } else { @@ -1333,20 +1317,18 @@ namespace mtconnect { auto ap = m_loopback->receiveAsset(dev, asset, uuid, type, nullopt, errors); if (!ap || errors.size() > 0 || (type && ap->getType() != *type)) { - ProtoErrorList errorResp; + entity::EntityList errorList; + if (!ap) - errorResp.emplace_back("INVALID_REQUEST", "Could not parse Asset."); + errorList.emplace_back(Error::make(Error::ErrorCode::INVALID_REQUEST, "Could not parse Asset.")); else - errorResp.emplace_back("INVALID_REQUEST", "Asset parsed with errors."); + errorList.emplace_back(Error::make(Error::ErrorCode::INVALID_REQUEST, "Asset parsed with errors.")); for (auto &e : errors) { - errorResp.emplace_back("INVALID_REQUEST", e->what()); + errorList.emplace_back(Error::make(Error::ErrorCode::INVALID_REQUEST, e->what())); } - return make_unique( - status::bad_request, - printer->printErrors(m_instanceId, m_sinkContract->getCircularBuffer().getBufferSize(), - m_sinkContract->getCircularBuffer().getSequence(), errorResp), - printer->mimeType()); + + throw RestError(errorList, printer); } AssetList list {ap}; @@ -1379,9 +1361,10 @@ namespace mtconnect { } else { - return make_unique(status::not_found, - printError(printer, "ASSET_NOT_FOUND", "Cannot find assets"), - printer->mimeType()); + entity::EntityList errors; + for (auto &id : ids) + errors.emplace_back(AssetNotFound::make(id, "Cannot find asset: " + id)); + throw RestError(errors, printer, status::not_found); } } @@ -1393,9 +1376,7 @@ namespace mtconnect { if (m_sinkContract->getAssetStorage()->getAssets(list, std::numeric_limits().max(), true, device, type) == 0) { - return make_unique(status::not_found, - printError(printer, "ASSET_NOT_FOUND", "Cannot find assets"), - printer->mimeType()); + throw RestError(AssetNotFound::make("", "No assets to delete"), printer, status::not_found); } else { @@ -1434,13 +1415,13 @@ namespace mtconnect { auto dev = checkDevice(printer, device); - ProtoErrorList errorResp; + entity::EntityList errors; for (auto &qp : observations) { auto di = dev->getDeviceDataItem(qp.first); if (di == nullptr) { - errorResp.emplace_back("BAD_REQUEST", "Cannot find data item: " + qp.first); + errors.emplace_back(Error::make(Error::ErrorCode::INVALID_REQUEST, "Cannot find data item: " + qp.first)); } else { @@ -1459,17 +1440,13 @@ namespace mtconnect { } } - if (errorResp.empty()) + if (errors.empty()) { return make_unique(status::ok, "", "text/xml"); } else { - return make_unique( - status::not_found, - printer->printErrors(m_instanceId, m_sinkContract->getCircularBuffer().getBufferSize(), - m_sinkContract->getCircularBuffer().getSequence(), errorResp), - printer->mimeType()); + throw RestError(errors, printer); } } @@ -1492,19 +1469,6 @@ namespace mtconnect { return "xml"; } - string RestService::printError(const Printer *printer, const string &errorCode, - const string &text, bool pretty, - const std::optional &requestId) const - { - LOG(debug) << "Returning error " << errorCode << ": " << text; - if (printer) - return printer->printError( - m_instanceId, m_sinkContract->getCircularBuffer().getBufferSize(), - m_sinkContract->getCircularBuffer().getSequence(), errorCode, text, pretty, requestId); - else - return errorCode + ": " + text; - } - // ----------------------------------------------- // Validation methods // ----------------------------------------------- @@ -1517,22 +1481,22 @@ namespace mtconnect { { stringstream str; str << '\'' << param << '\'' << " must be greater than " << min; - throw RequestError(str.str().c_str(), printError(printer, "OUT_OF_RANGE", str.str()), - printer->mimeType(), status::bad_request); + auto error = OutOfRange::make(param, value, min, max, str.str()); + throw RestError(error, printer); } if (value >= max) { stringstream str; str << '\'' << param << '\'' << " must be less than " << max; - throw RequestError(str.str().c_str(), printError(printer, "OUT_OF_RANGE", str.str()), - printer->mimeType(), status::bad_request); + auto error = OutOfRange::make(param, value, min, max, str.str()); + throw RestError(error, printer); } if (notZero && value == 0) { stringstream str; str << '\'' << param << '\'' << " must not be zero(0)"; - throw RequestError(str.str().c_str(), printError(printer, "OUT_OF_RANGE", str.str()), - printer->mimeType(), status::bad_request); + auto error = OutOfRange::make(param, value, min, max, str.str()); + throw RestError(error, printer); } } @@ -1546,15 +1510,17 @@ namespace mtconnect { } catch (exception &e) { - throw RequestError(e.what(), printError(printer, "INVALID_XPATH", e.what()), - printer->mimeType(), status::bad_request); + + string msg = "The path could not be parsed. Invalid syntax: "s + e.what(); + auto error = Error::make(Error::ErrorCode::INVALID_XPATH, printer->mimeType(), msg); + throw RestError(error, printer); } if (filter.empty()) { string msg = "The path could not be parsed. Invalid syntax: " + *path; - throw RequestError(msg.c_str(), printError(printer, "INVALID_XPATH", msg), - printer->mimeType(), status::bad_request); + auto error = Error::make(Error::ErrorCode::INVALID_XPATH, printer->mimeType(), msg); + throw RestError(error, printer); } } @@ -1564,8 +1530,8 @@ namespace mtconnect { if (!dev) { string msg("Could not find the device '" + uuid + "'"); - throw RequestError(msg.c_str(), printError(printer, "NO_DEVICE", msg), printer->mimeType(), - status::not_found); + auto error = Error::make(Error::ErrorCode::NO_DEVICE, printer->mimeType(), msg); + throw RestError(error, printer, status::not_found); } return dev; diff --git a/src/mtconnect/sink/rest_sink/rest_service.hpp b/src/mtconnect/sink/rest_sink/rest_service.hpp index f58c7df28..9b96c5816 100644 --- a/src/mtconnect/sink/rest_sink/rest_service.hpp +++ b/src/mtconnect/sink/rest_sink/rest_service.hpp @@ -280,15 +280,6 @@ namespace mtconnect { return printer; } - /// @brief Generate an MTConnect Error document - /// @param printer printer to generate error - /// @param errorCode an error code - /// @param text descriptive error text - /// @return MTConnect Error document - std::string printError(const printer::Printer *printer, const std::string &errorCode, - const std::string &text, bool pretty = false, - const std::optional &requestId = std::nullopt) const; - /// @name For testing only ///@{ auto instanceId() const { return m_instanceId; } @@ -296,6 +287,36 @@ namespace mtconnect { ///@} protected: + /// @brief Write an error response to the session + /// + /// This produces an MTConnect Error document using the error information and writes it the session. + /// Uses the accepts and format information from the error to determine the printer to use. + /// @param session the session to write to + /// @param error the error to write + void writeErrorResponse(SessionPtr session, const RestError &error) + { + LOG(debug) << "Returning error: " << error.what(); + + if (m_sinkContract) + { + const auto *prnt = error.getPrinter(); + if (!prnt) + { + prnt = getPrinter(error.getAccepts(), + error.getFormat()); + } + auto body = prnt->printErrors(m_instanceId, + m_sinkContract->getCircularBuffer().getBufferSize(), + m_sinkContract->getCircularBuffer().getSequence(), + error.getErrors(), false, error.getRequest()); + + ResponsePtr resp = std::make_unique(error.getStatus(), body, + prnt->mimeType()); + + session->writeFailureResponse(std::move(resp)); + } + } + // Configuration void loadNamespace(const boost::property_tree::ptree &tree, const char *namespaceType, printer::XmlPrinter *xmlPrinter, NamespaceFunction callback); diff --git a/src/mtconnect/sink/rest_sink/routing.hpp b/src/mtconnect/sink/rest_sink/routing.hpp index 18fa1a2ec..6cddc3e57 100644 --- a/src/mtconnect/sink/rest_sink/routing.hpp +++ b/src/mtconnect/sink/rest_sink/routing.hpp @@ -167,71 +167,65 @@ namespace mtconnect::sink::rest_sink { /// @return `true` if the request was matched bool matches(SessionPtr session, RequestPtr request) { - try + if (!request->m_command) { - if (!request->m_command) + request->m_parameters.clear(); + std::smatch m; + if (m_verb == request->m_verb && std::regex_match(request->m_path, m, m_pattern)) { - request->m_parameters.clear(); - std::smatch m; - if (m_verb == request->m_verb && std::regex_match(request->m_path, m, m_pattern)) + auto s = m.begin(); + s++; + for (auto &p : m_pathParameters) { - auto s = m.begin(); - s++; - for (auto &p : m_pathParameters) + if (s != m.end()) { - if (s != m.end()) - { - ParameterValue v(s->str()); - request->m_parameters.emplace(make_pair(p.m_name, v)); - s++; - } + ParameterValue v(s->str()); + request->m_parameters.emplace(make_pair(p.m_name, v)); + s++; } } - else - { - return false; - } } + else + { + return false; + } + } - for (auto &p : m_queryParameters) + entity::EntityList errors; + for (auto &p : m_queryParameters) + { + auto q = request->m_query.find(p.m_name); + if (q != request->m_query.end()) { - auto q = request->m_query.find(p.m_name); - if (q != request->m_query.end()) + try { - try - { - auto v = convertValue(q->second, p.m_type); - request->m_parameters.emplace(make_pair(p.m_name, v)); - } - catch (ParameterError &e) - { - std::string msg = std::string("for query parameter '") + p.m_name + "': " + e.what(); - - auto error = InvalidParameterValue::make(request->m_path, - p.m_name, - q->second, - p.getTypeName(), - p.getTypeFormat(), - msg); - - throw ParameterError(msg, error); - } + auto v = convertValue(q->second, p.m_type); + request->m_parameters.emplace(make_pair(p.m_name, v)); } - else if (!std::holds_alternative(p.m_default)) + catch (ParameterError &e) { - request->m_parameters.emplace(make_pair(p.m_name, p.m_default)); + std::string msg = std::string("for query parameter '") + p.m_name + "': " + e.what(); + + LOG(warning) << "Parameter error: " << msg; + auto error = InvalidParameterValue::make(request->m_path, + p.m_name, + q->second, + p.getTypeName(), + p.getTypeFormat(), + msg); + errors.emplace_back(error); } } - return m_function(session, request); - } - - catch (ParameterError &e) - { - LOG(debug) << "Pattern error: " << e.what(); - throw e; + else if (!std::holds_alternative(p.m_default)) + { + request->m_parameters.emplace(make_pair(p.m_name, p.m_default)); + } } - - return false; + + if (!errors.empty()) + throw RestError(errors, request->m_accepts); + else + return m_function(session, request); } /// @brief check if this is related to a swagger API diff --git a/src/mtconnect/sink/rest_sink/server.hpp b/src/mtconnect/sink/rest_sink/server.hpp index 740efab83..834afdb9e 100644 --- a/src/mtconnect/sink/rest_sink/server.hpp +++ b/src/mtconnect/sink/rest_sink/server.hpp @@ -75,8 +75,8 @@ namespace mtconnect::sink::rest_sink { if (fields) setHttpHeaders(*fields); - m_errorFunction = [](SessionPtr session, status st, const std::string &msg) { - ResponsePtr response = std::make_unique(st, msg, "text/plain"); + m_errorFunction = [](SessionPtr session, const RestError &error) { + ResponsePtr response = std::make_unique(error.getStatus(), "Error occurred for request", "text/plain"); session->writeFailureResponse(std::move(response)); return true; }; @@ -189,18 +189,11 @@ namespace mtconnect::sink::rest_sink { session->fail(boost::beast::http::status::not_found, txt.str()); } } - catch (RequestError &re) + catch (RestError &re) { - LOG(error) << session->getRemote().address() << ": Error processing request: " << re.what(); - ResponsePtr resp = std::make_unique(re); - session->writeResponse(std::move(resp)); - } - catch (ParameterError &pe) - { - std::stringstream txt; - txt << session->getRemote().address() << ": Parameter Error: " << pe.what(); - LOG(error) << txt.str(); - session->fail(boost::beast::http::status::not_found, txt.str()); + LOG(error) << session->getRemote().address() << ": Error processing request: " << request->m_path; + re.setUri(request->m_path); + m_errorFunction(session, re); } catch (std::logic_error &le) { diff --git a/src/mtconnect/sink/rest_sink/session.hpp b/src/mtconnect/sink/rest_sink/session.hpp index c49a26586..d430a3de9 100644 --- a/src/mtconnect/sink/rest_sink/session.hpp +++ b/src/mtconnect/sink/rest_sink/session.hpp @@ -27,6 +27,7 @@ #include "mtconnect/config.hpp" #include "mtconnect/observation/change_observer.hpp" #include "routing.hpp" +#include "error.hpp" namespace mtconnect::sink::rest_sink { struct Response; @@ -34,7 +35,7 @@ namespace mtconnect::sink::rest_sink { class Session; using SessionPtr = std::shared_ptr; using ErrorFunction = - std::function; + std::function; using Dispatch = std::function; using Complete = std::function; @@ -93,7 +94,9 @@ namespace mtconnect::sink::rest_sink { } else { - m_errorFunction(shared_from_this(), status, message); + auto error = Error::make(Error::ErrorCode::INTERNAL_ERROR, message); + RestError re(error, "application/xml", status); + m_errorFunction(shared_from_this(), re); } } diff --git a/test_package/xml_printer_test.cpp b/test_package/xml_printer_test.cpp index feba4c780..2fe637432 100644 --- a/test_package/xml_printer_test.cpp +++ b/test_package/xml_printer_test.cpp @@ -26,6 +26,7 @@ #include "mtconnect/observation/observation.hpp" #include "mtconnect/parser/xml_parser.hpp" #include "mtconnect/printer//xml_printer.hpp" +#include "mtconnect/sink/rest_sink/error.hpp" #include "mtconnect/utilities.hpp" #include "test_utilities.hpp" @@ -36,6 +37,7 @@ using namespace mtconnect::observation; using namespace mtconnect::entity; using namespace mtconnect::printer; using namespace mtconnect::parser; +using namespace mtconnect::sink::rest_sink; // main int main(int argc, char *argv[]) @@ -106,12 +108,14 @@ ObservationPtr XmlPrinterTest::addEventToCheckpoint(Checkpoint &checkpoint, cons TEST_F(XmlPrinterTest, PrintError) { m_printer->setSenderName("MachineXXX"); - PARSE_XML(m_printer->printError(123, 9999, 1, "ERROR_CODE", "ERROR TEXT!")); + + auto error = Error::make(Error::ErrorCode::INVALID_REQUEST, "ERROR TEXT!"); + PARSE_XML(m_printer->printError(123, 9999, 1, error, true)); ASSERT_XML_PATH_EQUAL(doc, "//m:Header@instanceId", "123"); ASSERT_XML_PATH_EQUAL(doc, "//m:Header@bufferSize", "9999"); ASSERT_XML_PATH_EQUAL(doc, "//m:Header@sender", "MachineXXX"); - ASSERT_XML_PATH_EQUAL(doc, "//m:Error@errorCode", "ERROR_CODE"); + ASSERT_XML_PATH_EQUAL(doc, "//m:Error@errorCode", "INVALID_REQUEST"); ASSERT_XML_PATH_EQUAL(doc, "//m:Error", "ERROR TEXT!"); } @@ -373,7 +377,9 @@ TEST_F(XmlPrinterTest, ChangeErrorNamespace) // Error { - PARSE_XML(m_printer->printError(123, 9999, 1, "ERROR_CODE", "ERROR TEXT!")); + auto error = Error::make(Error::ErrorCode::INVALID_REQUEST, "ERROR_TEXT"); + PARSE_XML(m_printer->printError(123, 9999, 1, error, true)); + ASSERT_XML_PATH_EQUAL(doc, "/m:MTConnectError@schemaLocation", "urn:mtconnect.org:MTConnectError:1.2 " "http://schemas.mtconnect.org/schemas/" @@ -384,7 +390,8 @@ TEST_F(XmlPrinterTest, ChangeErrorNamespace) m_printer->addErrorNamespace("urn:machine.com:MachineError:1.3", "http://www.machine.com/schemas/MachineError_1.3.xsd", "e"); - PARSE_XML(m_printer->printError(123, 9999, 1, "ERROR_CODE", "ERROR TEXT!")); + auto error = Error::make(Error::ErrorCode::INVALID_REQUEST, "ERROR_TEXT"); + PARSE_XML(m_printer->printError(123, 9999, 1, error, true)); ASSERT_XML_PATH_EQUAL( doc, "/m:MTConnectError@schemaLocation", @@ -848,7 +855,8 @@ TEST_F(XmlPrinterTest, ErrorStyle) { m_printer->setErrorStyle("/styles/Error.xsl"); - PARSE_XML(m_printer->printError(123, 9999, 1, "ERROR_CODE", "ERROR TEXT!")); + auto error = Error::make(Error::ErrorCode::INVALID_REQUEST, "ERROR_TEXT"); + PARSE_XML(m_printer->printError(123, 9999, 1, error, true)); xmlNodePtr pi = doc->children; ASSERT_EQ(string("xml-stylesheet"), string((const char *)pi->name)); From 089d38b77c5b98d017511d2fb6cc8034f2f420bc Mon Sep 17 00:00:00 2001 From: Will Sobel Date: Mon, 25 Aug 2025 17:54:46 +0200 Subject: [PATCH 6/7] Added tests for json and xml error document generation --- src/mtconnect/printer/json_printer.cpp | 17 +++- test_package/json_printer_error_test.cpp | 104 +++++++++++++++++++++-- test_package/xml_printer_test.cpp | 56 +++++++++++- 3 files changed, 168 insertions(+), 9 deletions(-) diff --git a/src/mtconnect/printer/json_printer.cpp b/src/mtconnect/printer/json_printer.cpp index 74805e205..3c28a0a45 100644 --- a/src/mtconnect/printer/json_printer.cpp +++ b/src/mtconnect/printer/json_printer.cpp @@ -39,6 +39,7 @@ #include "mtconnect/logging.hpp" #include "mtconnect/printer/json_printer_helper.hpp" #include "mtconnect/version.h" +#include "mtconnect/sink/rest_sink/error.hpp" using namespace std; @@ -122,6 +123,7 @@ namespace mtconnect::printer { const std::optional requestId) const { defaultSchemaVersion(); + auto version = IntSchemaVersion(*m_schemaVersion); StringBuffer output; RenderJson(output, m_pretty || pretty, [&](auto &writer) { @@ -138,8 +140,19 @@ namespace mtconnect::printer { m_modelChangeTime, m_validation, requestId); } { - obj.Key("Errors"); - printer.printEntityList(list); + obj.Key("Errors"); + entity::EntityList errors; + if (version < SCHEMA_VERSION(2, 6)) + { + for (auto &err : list) + { + auto re = dynamic_pointer_cast(err); + errors.emplace_back(re->makeLegacyError()); + } + } + else + errors = list; + printer.printEntityList(errors); } } }); diff --git a/test_package/json_printer_error_test.cpp b/test_package/json_printer_error_test.cpp index f4a2a7291..bc50ab1ea 100644 --- a/test_package/json_printer_error_test.cpp +++ b/test_package/json_printer_error_test.cpp @@ -33,10 +33,13 @@ #include "mtconnect/device_model/device.hpp" #include "mtconnect/observation/observation.hpp" #include "mtconnect/printer//json_printer.hpp" +#include "mtconnect/sink/rest_sink/error.hpp" #include "mtconnect/utilities.hpp" using namespace std; using namespace mtconnect; +using namespace mtconnect::sink::rest_sink; + using json = nlohmann::json; // main @@ -54,19 +57,108 @@ class JsonPrinterErrorTest : public testing::Test std::unique_ptr m_printer; }; -TEST_F(JsonPrinterErrorTest, PrintError) +TEST_F(JsonPrinterErrorTest, should_print_legacy_error) { - auto doc = m_printer->printError(12345u, 1024u, 56u, "BAD_BAD", "Never do that again"); + auto error = Error::make(Error::ErrorCode::INVALID_REQUEST, "ERROR TEXT!"); + auto doc = m_printer->printError(123, 9999, 1, error, true); + // cout << doc << endl; auto jdoc = json::parse(doc); auto it = jdoc.begin(); ASSERT_EQ(string("MTConnectError"), it.key()); - ASSERT_EQ(12345, jdoc.at("/MTConnectError/Header/instanceId"_json_pointer).get()); - ASSERT_EQ(1024, jdoc.at("/MTConnectError/Header/bufferSize"_json_pointer).get()); + ASSERT_EQ(123, jdoc.at("/MTConnectError/Header/instanceId"_json_pointer).get()); + ASSERT_EQ(9999, jdoc.at("/MTConnectError/Header/bufferSize"_json_pointer).get()); ASSERT_EQ(false, jdoc.at("/MTConnectError/Header/testIndicator"_json_pointer).get()); - ASSERT_EQ(string("BAD_BAD"), + ASSERT_EQ(string("INVALID_REQUEST"), jdoc.at("/MTConnectError/Errors/0/Error/errorCode"_json_pointer).get()); - ASSERT_EQ(string("Never do that again"), + ASSERT_EQ(string("ERROR TEXT!"), jdoc.at("/MTConnectError/Errors/0/Error/value"_json_pointer).get()); } + +TEST_F(JsonPrinterErrorTest, should_print_error_with_2_6_invalid_request) +{ + m_printer->setSchemaVersion("2.6"); + m_printer->setSenderName("MachineXXX"); + + auto error = Error::make(Error::ErrorCode::INVALID_REQUEST, "ERROR TEXT!"); + auto doc = m_printer->printError(123, 9999, 1, error, true); + + // cout << doc << endl; + auto jdoc = json::parse(doc); + auto it = jdoc.begin(); + ASSERT_EQ(string("MTConnectError"), it.key()); + ASSERT_EQ(123, jdoc.at("/MTConnectError/Header/instanceId"_json_pointer).get()); + ASSERT_EQ(9999, jdoc.at("/MTConnectError/Header/bufferSize"_json_pointer).get()); + ASSERT_EQ(false, jdoc.at("/MTConnectError/Header/testIndicator"_json_pointer).get()); + + ASSERT_EQ(string("INVALID_REQUEST"), + jdoc.at("/MTConnectError/Errors/0/InvalidRequest/errorCode"_json_pointer).get()); + ASSERT_EQ(string("ERROR TEXT!"), + jdoc.at("/MTConnectError/Errors/0/InvalidRequest/ErrorMessage"_json_pointer).get()); + +} + +TEST_F(JsonPrinterErrorTest, should_print_error_with_2_6_invalid_parameter_value) +{ + m_printer->setSchemaVersion("2.6"); + m_printer->setSenderName("MachineXXX"); + + auto error = InvalidParameterValue::make("interval", "XXX", "integer", "int64", "Bad Value"); + auto doc = m_printer->printError(123, 9999, 1, error, true); + + // cout << doc << endl; + auto jdoc = json::parse(doc); + auto it = jdoc.begin(); + ASSERT_EQ(string("MTConnectError"), it.key()); + ASSERT_EQ(123, jdoc.at("/MTConnectError/Header/instanceId"_json_pointer).get()); + ASSERT_EQ(9999, jdoc.at("/MTConnectError/Header/bufferSize"_json_pointer).get()); + ASSERT_EQ(false, jdoc.at("/MTConnectError/Header/testIndicator"_json_pointer).get()); + + ASSERT_EQ(string("INVALID_PARAMTER_VALUE"), + jdoc.at("/MTConnectError/Errors/0/InvalidParameterValue/errorCode"_json_pointer).get()); + ASSERT_EQ(string("Bad Value"), + jdoc.at("/MTConnectError/Errors/0/InvalidParameterValue/ErrorMessage"_json_pointer).get()); + ASSERT_EQ(string("interval"), + jdoc.at("/MTConnectError/Errors/0/InvalidParameterValue/QueryParameter/name"_json_pointer).get()); + ASSERT_EQ(string("XXX"), + jdoc.at("/MTConnectError/Errors/0/InvalidParameterValue/QueryParameter/Value"_json_pointer).get()); + ASSERT_EQ(string("integer"), + jdoc.at("/MTConnectError/Errors/0/InvalidParameterValue/QueryParameter/Type"_json_pointer).get()); + ASSERT_EQ(string("int64"), + jdoc.at("/MTConnectError/Errors/0/InvalidParameterValue/QueryParameter/Format"_json_pointer).get()); + + +} + +TEST_F(JsonPrinterErrorTest, should_print_error_with_2_6_out_of_range) +{ + m_printer->setSchemaVersion("2.6"); + m_printer->setSenderName("MachineXXX"); + + auto error = OutOfRange::make("from", 9999999, 10904772, 12907777, "Bad Value"); + auto doc = m_printer->printError(123, 9999, 1, error, true); + + // cout << doc << endl; + auto jdoc = json::parse(doc); + auto it = jdoc.begin(); + ASSERT_EQ(string("MTConnectError"), it.key()); + ASSERT_EQ(123, jdoc.at("/MTConnectError/Header/instanceId"_json_pointer).get()); + ASSERT_EQ(9999, jdoc.at("/MTConnectError/Header/bufferSize"_json_pointer).get()); + ASSERT_EQ(false, jdoc.at("/MTConnectError/Header/testIndicator"_json_pointer).get()); + + ASSERT_EQ(string("OUT_OF_RANGE"), + jdoc.at("/MTConnectError/Errors/0/OutOfRange/errorCode"_json_pointer).get()); + ASSERT_EQ(string("Bad Value"), + jdoc.at("/MTConnectError/Errors/0/OutOfRange/ErrorMessage"_json_pointer).get()); + ASSERT_EQ(string("from"), + jdoc.at("/MTConnectError/Errors/0/OutOfRange/QueryParameter/name"_json_pointer).get()); + ASSERT_EQ(9999999, + jdoc.at("/MTConnectError/Errors/0/OutOfRange/QueryParameter/Value"_json_pointer).get()); + ASSERT_EQ(10904772, + jdoc.at("/MTConnectError/Errors/0/OutOfRange/QueryParameter/Minimum"_json_pointer).get()); + ASSERT_EQ(12907777, + jdoc.at("/MTConnectError/Errors/0/OutOfRange/QueryParameter/Maximum"_json_pointer).get()); + + +} diff --git a/test_package/xml_printer_test.cpp b/test_package/xml_printer_test.cpp index 2fe637432..640b13e1c 100644 --- a/test_package/xml_printer_test.cpp +++ b/test_package/xml_printer_test.cpp @@ -105,7 +105,7 @@ ObservationPtr XmlPrinterTest::addEventToCheckpoint(Checkpoint &checkpoint, cons return event; } -TEST_F(XmlPrinterTest, PrintError) +TEST_F(XmlPrinterTest, should_print_legacy_error) { m_printer->setSenderName("MachineXXX"); @@ -119,6 +119,60 @@ TEST_F(XmlPrinterTest, PrintError) ASSERT_XML_PATH_EQUAL(doc, "//m:Error", "ERROR TEXT!"); } +TEST_F(XmlPrinterTest, should_print_error_with_2_6_invalid_request) +{ + m_printer->setSchemaVersion("2.6"); + m_printer->setSenderName("MachineXXX"); + + auto error = Error::make(Error::ErrorCode::INVALID_REQUEST, "ERROR TEXT!"); + PARSE_XML(m_printer->printError(123, 9999, 1, error, true)); + + ASSERT_XML_PATH_EQUAL(doc, "//m:Header@instanceId", "123"); + ASSERT_XML_PATH_EQUAL(doc, "//m:Header@bufferSize", "9999"); + ASSERT_XML_PATH_EQUAL(doc, "//m:Header@sender", "MachineXXX"); + ASSERT_XML_PATH_EQUAL(doc, "//m:Errors/m:InvalidRequest@errorCode", "INVALID_REQUEST"); + ASSERT_XML_PATH_EQUAL(doc, "//m:Errors/m:InvalidRequest/m:ErrorMessage", "ERROR TEXT!"); +} + +TEST_F(XmlPrinterTest, should_print_error_with_2_6_invalid_parameter_value) +{ + m_printer->setSchemaVersion("2.6"); + m_printer->setSenderName("MachineXXX"); + + auto error = InvalidParameterValue::make("interval", "XXX", "integer", "int64", "Bad Value"); + PARSE_XML(m_printer->printError(123, 9999, 1, error, true)); + + ASSERT_XML_PATH_EQUAL(doc, "//m:Header@instanceId", "123"); + ASSERT_XML_PATH_EQUAL(doc, "//m:Header@bufferSize", "9999"); + ASSERT_XML_PATH_EQUAL(doc, "//m:Header@sender", "MachineXXX"); + ASSERT_XML_PATH_EQUAL(doc, "//m:Errors/m:InvalidParameterValue@errorCode", "INVALID_PARAMTER_VALUE"); + ASSERT_XML_PATH_EQUAL(doc, "//m:Errors/m:InvalidParameterValue/m:ErrorMessage", "Bad Value"); + ASSERT_XML_PATH_EQUAL(doc, "//m:Errors/m:InvalidParameterValue/m:QueryParameter@name", "interval"); + ASSERT_XML_PATH_EQUAL(doc, "//m:Errors/m:InvalidParameterValue/m:QueryParameter/m:Value", "XXX"); + ASSERT_XML_PATH_EQUAL(doc, "//m:Errors/m:InvalidParameterValue/m:QueryParameter/m:Type", "integer"); + ASSERT_XML_PATH_EQUAL(doc, "//m:Errors/m:InvalidParameterValue/m:QueryParameter/m:Format", "int64"); +} + +TEST_F(XmlPrinterTest, should_print_error_with_2_6_out_of_range) +{ + m_printer->setSchemaVersion("2.6"); + m_printer->setSenderName("MachineXXX"); + + auto error = OutOfRange::make("from", 9999999, 10904772, 12907777, "Bad Value"); + PARSE_XML(m_printer->printError(123, 9999, 1, error, true)); + + ASSERT_XML_PATH_EQUAL(doc, "//m:Header@instanceId", "123"); + ASSERT_XML_PATH_EQUAL(doc, "//m:Header@bufferSize", "9999"); + ASSERT_XML_PATH_EQUAL(doc, "//m:Header@sender", "MachineXXX"); + ASSERT_XML_PATH_EQUAL(doc, "//m:Errors/m:OutOfRange@errorCode", "OUT_OF_RANGE"); + ASSERT_XML_PATH_EQUAL(doc, "//m:Errors/m:OutOfRange/m:ErrorMessage", "Bad Value"); + ASSERT_XML_PATH_EQUAL(doc, "//m:Errors/m:OutOfRange/m:QueryParameter@name", "from"); + ASSERT_XML_PATH_EQUAL(doc, "//m:Errors/m:OutOfRange/m:QueryParameter/m:Value", "9999999"); + ASSERT_XML_PATH_EQUAL(doc, "//m:Errors/m:OutOfRange/m:QueryParameter/m:Minimum", "10904772"); + ASSERT_XML_PATH_EQUAL(doc, "//m:Errors/m:OutOfRange/m:QueryParameter/m:Maximum", "12907777"); +} + + TEST_F(XmlPrinterTest, PrintProbe) { m_printer->setSenderName("MachineXXX"); From b7b0813759ac96583ccd24a0c297106ae45f0c37 Mon Sep 17 00:00:00 2001 From: Will Sobel Date: Tue, 26 Aug 2025 13:22:33 +0200 Subject: [PATCH 7/7] initial version of 2.6 error handling with tests --- CMakeLists.txt | 4 +- src/mtconnect/printer/json_printer.cpp | 2 +- src/mtconnect/printer/json_printer.hpp | 2 +- src/mtconnect/printer/printer.hpp | 7 +- src/mtconnect/printer/xml_printer.cpp | 6 +- src/mtconnect/sink/rest_sink/error.cpp | 8 +- src/mtconnect/sink/rest_sink/error.hpp | 92 ++-- src/mtconnect/sink/rest_sink/parameter.hpp | 38 +- src/mtconnect/sink/rest_sink/request.hpp | 25 +- src/mtconnect/sink/rest_sink/response.hpp | 2 +- src/mtconnect/sink/rest_sink/rest_service.cpp | 44 +- src/mtconnect/sink/rest_sink/rest_service.hpp | 25 +- src/mtconnect/sink/rest_sink/routing.hpp | 14 +- src/mtconnect/sink/rest_sink/server.hpp | 16 +- src/mtconnect/sink/rest_sink/session.hpp | 5 +- test_package/agent_test.cpp | 467 ++++++++++++++++-- test_package/http_server_test.cpp | 33 +- test_package/json_printer_error_test.cpp | 76 +-- test_package/routing_test.cpp | 76 ++- test_package/xml_printer_test.cpp | 27 +- 20 files changed, 715 insertions(+), 254 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 4914f4024..2c43d7527 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,8 +1,8 @@ # The version number. set(AGENT_VERSION_MAJOR 2) -set(AGENT_VERSION_MINOR 5) +set(AGENT_VERSION_MINOR 6) set(AGENT_VERSION_PATCH 0) -set(AGENT_VERSION_BUILD 11) +set(AGENT_VERSION_BUILD 0) set(AGENT_VERSION_RC "") # This minimum version is to support Visual Studio 2019 and C++ feature checking and FetchContent diff --git a/src/mtconnect/printer/json_printer.cpp b/src/mtconnect/printer/json_printer.cpp index 3c28a0a45..4dc7266cd 100644 --- a/src/mtconnect/printer/json_printer.cpp +++ b/src/mtconnect/printer/json_printer.cpp @@ -38,8 +38,8 @@ #include "mtconnect/entity/json_printer.hpp" #include "mtconnect/logging.hpp" #include "mtconnect/printer/json_printer_helper.hpp" -#include "mtconnect/version.h" #include "mtconnect/sink/rest_sink/error.hpp" +#include "mtconnect/version.h" using namespace std; diff --git a/src/mtconnect/printer/json_printer.hpp b/src/mtconnect/printer/json_printer.hpp index 1b8d76bbb..c4c1cd902 100644 --- a/src/mtconnect/printer/json_printer.hpp +++ b/src/mtconnect/printer/json_printer.hpp @@ -32,7 +32,7 @@ namespace mtconnect::printer { std::string printErrors( const uint64_t instanceId, const unsigned int bufferSize, const uint64_t nextSeq, - const entity::EntityList &list, bool pretty = false, + const entity::EntityList &list, bool pretty = false, const std::optional requestId = std::nullopt) const override; std::string printProbe( diff --git a/src/mtconnect/printer/printer.hpp b/src/mtconnect/printer/printer.hpp index 22b2dae64..914b8e701 100644 --- a/src/mtconnect/printer/printer.hpp +++ b/src/mtconnect/printer/printer.hpp @@ -64,10 +64,11 @@ namespace mtconnect { /// @return the error document virtual std::string printError( const uint64_t instanceId, const unsigned int bufferSize, const uint64_t nextSeq, - entity::EntityPtr error, bool pretty = false, + entity::EntityPtr error, bool pretty = false, const std::optional requestId = std::nullopt) const { - return printErrors(instanceId, bufferSize, nextSeq, entity::EntityList {error}, pretty, requestId); + return printErrors(instanceId, bufferSize, nextSeq, entity::EntityList {error}, pretty, + requestId); } /// @brief Generate an MTConnect Error document with a error list /// @param[in] instanceId the instance id @@ -79,7 +80,7 @@ namespace mtconnect { /// @return the MTConnect Error document virtual std::string printErrors( const uint64_t instanceId, const unsigned int bufferSize, const uint64_t nextSeq, - const entity::EntityList &list, bool pretty = false, + const entity::EntityList &list, bool pretty = false, const std::optional requestId = std::nullopt) const = 0; /// @brief Generate an MTConnect Devices document /// @param[in] instanceId the instance id diff --git a/src/mtconnect/printer/xml_printer.cpp b/src/mtconnect/printer/xml_printer.cpp index 4f1e5011b..e57629818 100644 --- a/src/mtconnect/printer/xml_printer.cpp +++ b/src/mtconnect/printer/xml_printer.cpp @@ -30,9 +30,9 @@ #include "mtconnect/asset/cutting_tool.hpp" #include "mtconnect/device_model/composition.hpp" #include "mtconnect/device_model/configuration/configuration.hpp" -#include "mtconnect/sink/rest_sink/error.hpp" #include "mtconnect/device_model/device.hpp" #include "mtconnect/logging.hpp" +#include "mtconnect/sink/rest_sink/error.hpp" #include "mtconnect/version.h" #include "xml_printer.hpp" @@ -359,11 +359,11 @@ namespace mtconnect::printer { { AutoElement e1(writer, "Errors"); entity::XmlPrinter printer; - + auto version = IntSchemaVersion(*m_schemaVersion); for (auto &e : list) { - entity::EntityPtr err { e }; + entity::EntityPtr err {e}; if (version < SCHEMA_VERSION(2, 6)) { auto re = dynamic_pointer_cast(err); diff --git a/src/mtconnect/sink/rest_sink/error.cpp b/src/mtconnect/sink/rest_sink/error.cpp index 8839f8814..2183594ab 100644 --- a/src/mtconnect/sink/rest_sink/error.cpp +++ b/src/mtconnect/sink/rest_sink/error.cpp @@ -60,8 +60,8 @@ namespace mtconnect::sink::rest_sink { case ErrorCode::UNSUPPORTED: return "Unsupported"; - case ErrorCode::INVALID_PARAMTER_VALUE: - return "InvalidParamterValue"; + case ErrorCode::INVALID_PARAMETER_VALUE: + return "InvalidParameterValue"; case ErrorCode::INVALID_QUERY_PARAMETER: return "InvalidQueryParameter"; @@ -107,8 +107,8 @@ namespace mtconnect::sink::rest_sink { case ErrorCode::UNSUPPORTED: return "UNSUPPORTED"; - case ErrorCode::INVALID_PARAMTER_VALUE: - return "INVALID_PARAMTER_VALUE"; + case ErrorCode::INVALID_PARAMETER_VALUE: + return "INVALID_PARAMETER_VALUE"; case ErrorCode::INVALID_QUERY_PARAMETER: return "INVALID_QUERY_PARAMETER"; diff --git a/src/mtconnect/sink/rest_sink/error.hpp b/src/mtconnect/sink/rest_sink/error.hpp index ae83f9206..32b9be5a4 100644 --- a/src/mtconnect/sink/rest_sink/error.hpp +++ b/src/mtconnect/sink/rest_sink/error.hpp @@ -17,11 +17,12 @@ #pragma once +#include + #include #include "mtconnect/entity/entity.hpp" #include "mtconnect/entity/factory.hpp" -#include #include "mtconnect/printer/printer.hpp" namespace mtconnect::sink::rest_sink { @@ -43,17 +44,17 @@ namespace mtconnect::sink::rest_sink { TOO_MANY, UNAUTHORIZED, UNSUPPORTED, - INVALID_PARAMTER_VALUE, + INVALID_PARAMETER_VALUE, INVALID_QUERY_PARAMETER }; Error(const std::string &name, const entity::Properties &props) : entity::Entity(name, props) {} ~Error() override = default; - + void setURI(const std::string &uri) { setProperty("URI", uri); } void setRequest(const std::string &request) { setProperty("Request", request); } void setErrorMessage(const std::string &message) { setProperty("ErrorMessage", message); } - + /// @brief get the static error factory /// @return shared pointer to the factory static entity::FactoryPtr getFactory() @@ -70,7 +71,7 @@ namespace mtconnect::sink::rest_sink { static const std::string nameForCode(ErrorCode code); static const std::string enumForCode(ErrorCode code); - + static entity::EntityPtr make(const ErrorCode code, std::optional errorMessage = std::nullopt, std::optional request = std::nullopt) @@ -95,7 +96,7 @@ namespace mtconnect::sink::rest_sink { }; using ErrorPtr = std::shared_ptr; - + class AGENT_LIB_API QueryParameter : public entity::Entity { public: @@ -157,16 +158,15 @@ namespace mtconnect::sink::rest_sink { return factory; } - static entity::EntityPtr make(const std::string &name, - const std::string &value, const std::string &type, - const std::string &format, - std::optional errorMessage = std::nullopt, + static entity::EntityPtr make(const std::string &name, const std::string &value, + const std::string &type, const std::string &format, + std::optional errorMessage = std::nullopt, std::optional request = std::nullopt) { using namespace std; entity::Properties props { - {"errorCode", enumForCode(Error::ErrorCode::INVALID_PARAMTER_VALUE)}}; + {"errorCode", enumForCode(Error::ErrorCode::INVALID_PARAMETER_VALUE)}}; if (errorMessage) props["ErrorMessage"] = *errorMessage; if (request) @@ -205,8 +205,8 @@ namespace mtconnect::sink::rest_sink { return factory; } - static entity::EntityPtr make(const std::string &name, int64_t value, - int64_t min, int64_t max, std::optional errorMessage = std::nullopt, + static entity::EntityPtr make(const std::string &name, int64_t value, int64_t min, int64_t max, + std::optional errorMessage = std::nullopt, std::optional request = std::nullopt) { using namespace std; @@ -250,8 +250,8 @@ namespace mtconnect::sink::rest_sink { } static entity::EntityPtr make(const std::string &assetId, - std::optional errorMessage = std::nullopt, - std::optional request = std::nullopt) + std::optional errorMessage = std::nullopt, + std::optional request = std::nullopt) { using namespace std; @@ -265,7 +265,7 @@ namespace mtconnect::sink::rest_sink { return std::make_shared(props); } }; - + class AGENT_LIB_API RestError { public: @@ -274,27 +274,24 @@ namespace mtconnect::sink::rest_sink { /// @param accepts the accepted mime types, defaults to application/xml /// @param status the HTTP status code, defaults to 400 Bad Request /// @param format optional format for the error - RestError(entity::EntityPtr error, - std::string accepts = "application/xml", - status st = status::bad_request, - std::optional format = std::nullopt, - std::optional request = std::nullopt) : m_errors({error}), m_accepts(accepts), m_status(st), m_format(format) + RestError(entity::EntityPtr error, std::string accepts = "application/xml", + status st = status::bad_request, std::optional format = std::nullopt, + std::optional request = std::nullopt) + : m_errors({error}), m_accepts(accepts), m_status(st), m_format(format) { if (request) setRequest(*request); } - + /// @brief An exception that gets thrown during REST processing with an error and a status /// @param errors a list of errors /// @param accepts the accepted mime types, defaults to application/xml /// @param status the HTTP status code, defaults to 400 Bad Request /// @param format optional format for the error - RestError(entity::EntityList &errors, - std::string accepts = "application/xml", - status st = status::bad_request, - std::optional format = std::nullopt, - std::optional request = std::nullopt) : m_errors(errors), - m_accepts(accepts), m_status(st), m_format(format) + RestError(entity::EntityList &errors, std::string accepts = "application/xml", + status st = status::bad_request, std::optional format = std::nullopt, + std::optional request = std::nullopt) + : m_errors(errors), m_accepts(accepts), m_status(st), m_format(format) { if (request) setRequest(*request); @@ -305,38 +302,32 @@ namespace mtconnect::sink::rest_sink { /// @param printer the printer to generate the error document /// @param status the HTTP status code, defaults to 400 Bad Request /// @param format optional format for the error - RestError(entity::EntityPtr error, - const printer::Printer* printer = nullptr, - status st = status::bad_request, - std::optional format = std::nullopt, - std::optional request = std::nullopt) : - m_errors({error}), m_status(st), m_format(format), m_printer(printer) + RestError(entity::EntityPtr error, const printer::Printer *printer = nullptr, + status st = status::bad_request, std::optional format = std::nullopt, + std::optional request = std::nullopt) + : m_errors({error}), m_status(st), m_format(format), m_printer(printer) { if (request) setRequest(*request); } - + /// @brief An exception that gets thrown during REST processing with an error and a status /// @param errors a list of errors /// @param printer the printer to generate the error document /// @param status the HTTP status code, defaults to 400 Bad Request /// @param format optional format for the error - RestError(entity::EntityList &errors, - const printer::Printer* printer = nullptr, - status st = status::bad_request, - std::optional format = std::nullopt, - std::optional request = std::nullopt) : m_errors(errors), - m_status(st), m_format(format), m_printer(printer) + RestError(entity::EntityList &errors, const printer::Printer *printer = nullptr, + status st = status::bad_request, std::optional format = std::nullopt, + std::optional request = std::nullopt) + : m_errors(errors), m_status(st), m_format(format), m_printer(printer) { if (request) setRequest(*request); } - - ~RestError() = default; RestError(const RestError &o) = default; - + /// @brief set the URI for all errors /// @param uri the URI void setUri(const std::string &uri) @@ -344,7 +335,7 @@ namespace mtconnect::sink::rest_sink { for (auto &e : m_errors) e->setProperty("URI", uri); } - + /// @brief set the Request for all errors /// @param request the Request void setRequest(const std::string &request) @@ -362,7 +353,7 @@ namespace mtconnect::sink::rest_sink { const auto &getAccepts() const { return m_accepts; } const auto &getRequest() const { return m_request; } auto getPrinter() const { return m_printer; } - + std::string what() const { std::stringstream ss; @@ -374,17 +365,18 @@ namespace mtconnect::sink::rest_sink { ss << *message; ss << ", "; } - - return ss.str(); + auto s = ss.str(); + s.erase(s.length() - 2); + return s; } protected: entity::EntityList m_errors; - std::string m_accepts { "application/xml" }; + std::string m_accepts {"application/xml"}; status m_status; std::optional m_format; std::optional m_request; - const printer::Printer* m_printer { nullptr }; + const printer::Printer *m_printer {nullptr}; }; } // namespace mtconnect::sink::rest_sink diff --git a/src/mtconnect/sink/rest_sink/parameter.hpp b/src/mtconnect/sink/rest_sink/parameter.hpp index 0ad20f9db..5677f1b85 100644 --- a/src/mtconnect/sink/rest_sink/parameter.hpp +++ b/src/mtconnect/sink/rest_sink/parameter.hpp @@ -20,8 +20,8 @@ #include #include #include -#include #include +#include #include "error.hpp" #include "mtconnect/config.hpp" @@ -68,61 +68,59 @@ namespace mtconnect::sink::rest_sink { /// @brief to support std::set interface bool operator<(const Parameter &o) const { return m_name < o.m_name; } - + const std::string getTypeName() const { - switch(m_type) + switch (m_type) { - case NONE: return "unknown"; - + case STRING: return "string"; - + case INTEGER: return "integer"; - + case UNSIGNED_INTEGER: return "integer"; - + case DOUBLE: return "double"; - + case BOOL: return "boolean"; } - + return "unknown"; } - + const std::string getTypeFormat() const { - switch(m_type) + switch (m_type) { case NONE: return "unknown"; - + case STRING: return "string"; - + case INTEGER: - return "in32"; - + return "int32"; + case UNSIGNED_INTEGER: return "uint64"; - + case DOUBLE: return "double"; - + case BOOL: return "bool"; } - + return "unknown"; } - std::string m_name; ParameterType m_type {STRING}; /// @brief Default value if one is available diff --git a/src/mtconnect/sink/rest_sink/request.hpp b/src/mtconnect/sink/rest_sink/request.hpp index 8b9615210..1d51060bb 100644 --- a/src/mtconnect/sink/rest_sink/request.hpp +++ b/src/mtconnect/sink/rest_sink/request.hpp @@ -51,7 +51,7 @@ namespace mtconnect::sink::rest_sink { /// @name Websocket related properties ///@{ - + std::optional m_requestId; ///< Request id from websocket sub std::optional m_command; ///< Specific request from websocket @@ -70,7 +70,28 @@ namespace mtconnect::sink::rest_sink { else return std::get(v->second); } + + /// @brief Get the URI for this request + /// @return the URI + std::string getUri() const + { + std::string s; + if (!m_query.empty()) + { + std::stringstream uri; + uri << m_path << '?'; + for (auto &q : m_query) + uri << q.first << '=' << q.second << '&'; + s = uri.str(); + s.erase(s.length() - 1); + } + else + { + s = m_path; + } + return s; + } }; - using RequestPtr = std::shared_ptr; + using RequestPtr = std::shared_ptr; } // namespace mtconnect::sink::rest_sink diff --git a/src/mtconnect/sink/rest_sink/response.hpp b/src/mtconnect/sink/rest_sink/response.hpp index b4cd5b5e5..2e662f8da 100644 --- a/src/mtconnect/sink/rest_sink/response.hpp +++ b/src/mtconnect/sink/rest_sink/response.hpp @@ -52,7 +52,7 @@ namespace mtconnect { /// @param[in] file the file Response(status status, CachedFilePtr file) : m_status(status), m_mimeType(file->m_mimeType), m_expires(0), m_file(file) - {} + {} status m_status; ///< The return status std::string m_body; ///< The body of the response diff --git a/src/mtconnect/sink/rest_sink/rest_service.cpp b/src/mtconnect/sink/rest_sink/rest_service.cpp index 5124f8c40..5722a4b4f 100644 --- a/src/mtconnect/sink/rest_sink/rest_service.cpp +++ b/src/mtconnect/sink/rest_sink/rest_service.cpp @@ -19,6 +19,7 @@ #include +#include "error.hpp" #include "mtconnect/configuration/config_options.hpp" #include "mtconnect/entity/xml_parser.hpp" #include "mtconnect/pipeline/shdr_token_mapper.hpp" @@ -26,7 +27,6 @@ #include "mtconnect/pipeline/timestamp_extractor.hpp" #include "mtconnect/printer/xml_printer.hpp" #include "server.hpp" -#include "error.hpp" namespace asio = boost::asio; using namespace std; @@ -53,7 +53,7 @@ namespace mtconnect { { using placeholders::_1; using placeholders::_2; - + auto maxSize = ConvertFileSize(options, mtconnect::configuration::MaxCachedFileSize, 20 * 1024); auto compressSize = @@ -1287,8 +1287,7 @@ namespace mtconnect { entity::EntityList errors; for (auto &id : ids) errors.emplace_back(AssetNotFound::make(id, "Cannot find asset: " + id)); - throw RestError(errors, printer, status::not_found, - std::nullopt, requestId); + throw RestError(errors, printer, status::not_found, std::nullopt, requestId); } else { @@ -1318,16 +1317,18 @@ namespace mtconnect { if (!ap || errors.size() > 0 || (type && ap->getType() != *type)) { entity::EntityList errorList; - + if (!ap) - errorList.emplace_back(Error::make(Error::ErrorCode::INVALID_REQUEST, "Could not parse Asset.")); + errorList.emplace_back( + Error::make(Error::ErrorCode::INVALID_REQUEST, "Could not parse Asset.")); else - errorList.emplace_back(Error::make(Error::ErrorCode::INVALID_REQUEST, "Asset parsed with errors.")); + errorList.emplace_back( + Error::make(Error::ErrorCode::INVALID_REQUEST, "Asset parsed with errors.")); for (auto &e : errors) { errorList.emplace_back(Error::make(Error::ErrorCode::INVALID_REQUEST, e->what())); } - + throw RestError(errorList, printer); } @@ -1421,7 +1422,8 @@ namespace mtconnect { auto di = dev->getDeviceDataItem(qp.first); if (di == nullptr) { - errors.emplace_back(Error::make(Error::ErrorCode::INVALID_REQUEST, "Cannot find data item: " + qp.first)); + errors.emplace_back( + Error::make(Error::ErrorCode::INVALID_REQUEST, "Cannot find data item: " + qp.first)); } else { @@ -1477,25 +1479,22 @@ namespace mtconnect { void RestService::checkRange(const Printer *printer, const T value, const T min, const T max, const string ¶m, bool notZero) const { + stringstream str; if (value <= min) { - stringstream str; str << '\'' << param << '\'' << " must be greater than " << min; - auto error = OutOfRange::make(param, value, min, max, str.str()); - throw RestError(error, printer); } - if (value >= max) + else if (value >= max) { - stringstream str; str << '\'' << param << '\'' << " must be less than " << max; - auto error = OutOfRange::make(param, value, min, max, str.str()); - throw RestError(error, printer); } - if (notZero && value == 0) + else if (notZero && value == 0) { - stringstream str; str << '\'' << param << '\'' << " must not be zero(0)"; - auto error = OutOfRange::make(param, value, min, max, str.str()); + } + if (str.tellp() > 0) + { + auto error = OutOfRange::make(param, value, min + 1, max - 1, str.str()); throw RestError(error, printer); } } @@ -1510,16 +1509,15 @@ namespace mtconnect { } catch (exception &e) { - string msg = "The path could not be parsed. Invalid syntax: "s + e.what(); - auto error = Error::make(Error::ErrorCode::INVALID_XPATH, printer->mimeType(), msg); + auto error = Error::make(Error::ErrorCode::INVALID_XPATH, msg); throw RestError(error, printer); } if (filter.empty()) { string msg = "The path could not be parsed. Invalid syntax: " + *path; - auto error = Error::make(Error::ErrorCode::INVALID_XPATH, printer->mimeType(), msg); + auto error = Error::make(Error::ErrorCode::INVALID_XPATH, msg); throw RestError(error, printer); } } @@ -1530,7 +1528,7 @@ namespace mtconnect { if (!dev) { string msg("Could not find the device '" + uuid + "'"); - auto error = Error::make(Error::ErrorCode::NO_DEVICE, printer->mimeType(), msg); + auto error = Error::make(Error::ErrorCode::NO_DEVICE, msg); throw RestError(error, printer, status::not_found); } diff --git a/src/mtconnect/sink/rest_sink/rest_service.hpp b/src/mtconnect/sink/rest_sink/rest_service.hpp index 9b96c5816..887aeb110 100644 --- a/src/mtconnect/sink/rest_sink/rest_service.hpp +++ b/src/mtconnect/sink/rest_sink/rest_service.hpp @@ -289,8 +289,9 @@ namespace mtconnect { protected: /// @brief Write an error response to the session /// - /// This produces an MTConnect Error document using the error information and writes it the session. - /// Uses the accepts and format information from the error to determine the printer to use. + /// This produces an MTConnect Error document using the error information and writes it the + /// session. Uses the accepts and format information from the error to determine the printer + /// to use. /// @param session the session to write to /// @param error the error to write void writeErrorResponse(SessionPtr session, const RestError &error) @@ -302,21 +303,19 @@ namespace mtconnect { const auto *prnt = error.getPrinter(); if (!prnt) { - prnt = getPrinter(error.getAccepts(), - error.getFormat()); + prnt = getPrinter(error.getAccepts(), error.getFormat()); } - auto body = prnt->printErrors(m_instanceId, - m_sinkContract->getCircularBuffer().getBufferSize(), - m_sinkContract->getCircularBuffer().getSequence(), - error.getErrors(), false, error.getRequest()); - - ResponsePtr resp = std::make_unique(error.getStatus(), body, - prnt->mimeType()); - + auto body = + prnt->printErrors(m_instanceId, m_sinkContract->getCircularBuffer().getBufferSize(), + m_sinkContract->getCircularBuffer().getSequence(), + error.getErrors(), false, error.getRequest()); + + ResponsePtr resp = std::make_unique(error.getStatus(), body, prnt->mimeType()); + session->writeFailureResponse(std::move(resp)); } } - + // Configuration void loadNamespace(const boost::property_tree::ptree &tree, const char *namespaceType, printer::XmlPrinter *xmlPrinter, NamespaceFunction callback); diff --git a/src/mtconnect/sink/rest_sink/routing.hpp b/src/mtconnect/sink/rest_sink/routing.hpp index 6cddc3e57..2aea117b7 100644 --- a/src/mtconnect/sink/rest_sink/routing.hpp +++ b/src/mtconnect/sink/rest_sink/routing.hpp @@ -204,15 +204,11 @@ namespace mtconnect::sink::rest_sink { } catch (ParameterError &e) { - std::string msg = std::string("for query parameter '") + p.m_name + "': " + e.what(); - + std::string msg = std::string("query parameter '") + p.m_name + "': " + e.what(); + LOG(warning) << "Parameter error: " << msg; - auto error = InvalidParameterValue::make(request->m_path, - p.m_name, - q->second, - p.getTypeName(), - p.getTypeFormat(), - msg); + auto error = InvalidParameterValue::make(p.m_name, q->second, p.getTypeName(), + p.getTypeFormat(), msg); errors.emplace_back(error); } } @@ -221,7 +217,7 @@ namespace mtconnect::sink::rest_sink { request->m_parameters.emplace(make_pair(p.m_name, p.m_default)); } } - + if (!errors.empty()) throw RestError(errors, request->m_accepts); else diff --git a/src/mtconnect/sink/rest_sink/server.hpp b/src/mtconnect/sink/rest_sink/server.hpp index 834afdb9e..8b5ee4d29 100644 --- a/src/mtconnect/sink/rest_sink/server.hpp +++ b/src/mtconnect/sink/rest_sink/server.hpp @@ -76,7 +76,8 @@ namespace mtconnect::sink::rest_sink { setHttpHeaders(*fields); m_errorFunction = [](SessionPtr session, const RestError &error) { - ResponsePtr response = std::make_unique(error.getStatus(), "Error occurred for request", "text/plain"); + ResponsePtr response = + std::make_unique(error.getStatus(), error.what(), "text/plain"); session->writeFailureResponse(std::move(response)); return true; }; @@ -186,13 +187,20 @@ namespace mtconnect::sink::rest_sink { std::stringstream txt; txt << session->getRemote().address() << ": Cannot find handler for: " << request->m_verb << " " << request->m_path; - session->fail(boost::beast::http::status::not_found, txt.str()); + auto error = Error::make(Error::ErrorCode::INVALID_URI, txt.str()); + RestError re(error, request->m_accepts, status::not_found, std::nullopt, + request->m_requestId); + re.setUri(request->getUri()); + m_errorFunction(session, re); } } catch (RestError &re) { - LOG(error) << session->getRemote().address() << ": Error processing request: " << request->m_path; - re.setUri(request->m_path); + LOG(error) << session->getRemote().address() + << ": Error processing request: " << request->m_path; + re.setUri(request->getUri()); + if (request->m_requestId) + re.setRequest(*request->m_requestId); m_errorFunction(session, re); } catch (std::logic_error &le) diff --git a/src/mtconnect/sink/rest_sink/session.hpp b/src/mtconnect/sink/rest_sink/session.hpp index d430a3de9..821d7ff88 100644 --- a/src/mtconnect/sink/rest_sink/session.hpp +++ b/src/mtconnect/sink/rest_sink/session.hpp @@ -24,18 +24,17 @@ #include #include +#include "error.hpp" #include "mtconnect/config.hpp" #include "mtconnect/observation/change_observer.hpp" #include "routing.hpp" -#include "error.hpp" namespace mtconnect::sink::rest_sink { struct Response; using ResponsePtr = std::unique_ptr; class Session; using SessionPtr = std::shared_ptr; - using ErrorFunction = - std::function; + using ErrorFunction = std::function; using Dispatch = std::function; using Complete = std::function; diff --git a/test_package/agent_test.cpp b/test_package/agent_test.cpp index 7f34a71ef..193545027 100644 --- a/test_package/agent_test.cpp +++ b/test_package/agent_test.cpp @@ -144,7 +144,7 @@ TEST_F(AgentTest, FailWithDuplicateDeviceUUID) ASSERT_THROW(agent->initialize(context), std::runtime_error); } -TEST_F(AgentTest, BadDevices) +TEST_F(AgentTest, should_return_error_for_unknown_device) { { PARSE_XML_RESPONSE("/LinuxCN/probe"); @@ -155,7 +155,22 @@ TEST_F(AgentTest, BadDevices) } } -TEST_F(AgentTest, BadXPath) +TEST_F(AgentTest, should_return_2_6_error_for_unknown_device) +{ + auto agent = m_agentTestHelper->createAgent("/samples/test_config.xml", 8, 4, "2.6", 4, false, + true, {{configuration::Validation, false}}); + + { + PARSE_XML_RESPONSE("/LinuxCN/probe"); + string message = (string) "Could not find the device 'LinuxCN'"; + ASSERT_XML_PATH_EQUAL(doc, "//m:NoDevice@errorCode", "NO_DEVICE"); + ASSERT_XML_PATH_EQUAL(doc, "//m:NoDevice/m:ErrorMessage", message.c_str()); + ASSERT_XML_PATH_EQUAL(doc, "//m:NoDevice/m:URI", "/LinuxCN/probe"); + ASSERT_EQ(status::not_found, m_agentTestHelper->session()->m_code); + } +} + +TEST_F(AgentTest, should_return_error_when_path_cannot_be_parsed) { { QueryMap query {{"path", "//////Linear"}}; @@ -183,7 +198,42 @@ TEST_F(AgentTest, BadXPath) } } -TEST_F(AgentTest, GoodPath) +TEST_F(AgentTest, should_return_2_6_error_when_path_cannot_be_parsed) +{ + m_agentTestHelper->createAgent("/samples/test_config.xml", 8, 4, "2.6", 4, false, true, + {{configuration::Validation, false}}); + + { + QueryMap query {{"path", "//////Linear"}}; + PARSE_XML_RESPONSE_QUERY("/current", query); + string message = (string) "The path could not be parsed. Invalid syntax: //////Linear"; + ASSERT_XML_PATH_EQUAL(doc, "//m:InvalidXPath@errorCode", "INVALID_XPATH"); + ASSERT_XML_PATH_EQUAL(doc, "//m:InvalidXPath/m:ErrorMessage", message.c_str()); + ASSERT_XML_PATH_EQUAL(doc, "//m:InvalidXPath/m:URI", "/current?path=//////Linear"); + } + + { + QueryMap query {{"path", "//Axes?//Linear"}}; + PARSE_XML_RESPONSE_QUERY("/current", query); + string message = (string) "The path could not be parsed. Invalid syntax: //Axes?//Linear"; + ASSERT_XML_PATH_EQUAL(doc, "//m:InvalidXPath@errorCode", "INVALID_XPATH"); + ASSERT_XML_PATH_EQUAL(doc, "//m:InvalidXPath/m:ErrorMessage", message.c_str()); + ASSERT_XML_PATH_EQUAL(doc, "//m:InvalidXPath/m:URI", "/current?path=//Axes?//Linear"); + } + + { + QueryMap query {{"path", "//Devices/Device[@name=\"I_DON'T_EXIST\""}}; + PARSE_XML_RESPONSE_QUERY("/current", query); + string message = (string) + "The path could not be parsed. Invalid syntax: //Devices/Device[@name=\"I_DON'T_EXIST\""; + ASSERT_XML_PATH_EQUAL(doc, "//m:InvalidXPath@errorCode", "INVALID_XPATH"); + ASSERT_XML_PATH_EQUAL(doc, "//m:InvalidXPath/m:ErrorMessage", message.c_str()); + ASSERT_XML_PATH_EQUAL(doc, "//m:InvalidXPath/m:URI", + "/current?path=//Devices/Device[@name=\"I_DON'T_EXIST\""); + } +} + +TEST_F(AgentTest, should_handle_a_correct_path) { { QueryMap query {{"path", "//Power"}}; @@ -205,12 +255,12 @@ TEST_F(AgentTest, GoodPath) } } -TEST_F(AgentTest, BadPath) +TEST_F(AgentTest, should_report_an_invalid_uri) { using namespace rest_sink; { PARSE_XML_RESPONSE("/bad_path"); - ASSERT_XML_PATH_EQUAL(doc, "//m:Error@errorCode", "INVALID_REQUEST"); + ASSERT_XML_PATH_EQUAL(doc, "//m:Error@errorCode", "INVALID_URI"); ASSERT_XML_PATH_EQUAL(doc, "//m:Error", "0.0.0.0: Cannot find handler for: GET /bad_path"); EXPECT_EQ(status::not_found, m_agentTestHelper->session()->m_code); EXPECT_FALSE(m_agentTestHelper->m_dispatched); @@ -218,7 +268,7 @@ TEST_F(AgentTest, BadPath) { PARSE_XML_RESPONSE("/bad/path/"); - ASSERT_XML_PATH_EQUAL(doc, "//m:Error@errorCode", "INVALID_REQUEST"); + ASSERT_XML_PATH_EQUAL(doc, "//m:Error@errorCode", "INVALID_URI"); ASSERT_XML_PATH_EQUAL(doc, "//m:Error", "0.0.0.0: Cannot find handler for: GET /bad/path/"); EXPECT_EQ(status::not_found, m_agentTestHelper->session()->m_code); EXPECT_FALSE(m_agentTestHelper->m_dispatched); @@ -226,7 +276,7 @@ TEST_F(AgentTest, BadPath) { PARSE_XML_RESPONSE("/LinuxCNC/current/blah"); - ASSERT_XML_PATH_EQUAL(doc, "//m:Error@errorCode", "INVALID_REQUEST"); + ASSERT_XML_PATH_EQUAL(doc, "//m:Error@errorCode", "INVALID_URI"); ASSERT_XML_PATH_EQUAL(doc, "//m:Error", "0.0.0.0: Cannot find handler for: GET /LinuxCNC/current/blah"); EXPECT_EQ(status::not_found, m_agentTestHelper->session()->m_code); @@ -234,7 +284,47 @@ TEST_F(AgentTest, BadPath) } } -TEST_F(AgentTest, CurrentAt) +TEST_F(AgentTest, should_report_a_2_6_invalid_uri) +{ + using namespace rest_sink; + + m_agentTestHelper->createAgent("/samples/test_config.xml", 8, 4, "2.6", 4, false, true, + {{configuration::Validation, false}}); + + { + PARSE_XML_RESPONSE("/bad_path"); + ASSERT_XML_PATH_EQUAL(doc, "//m:InvalidURI@errorCode", "INVALID_URI"); + ASSERT_XML_PATH_EQUAL(doc, "//m:InvalidURI/m:ErrorMessage", + "0.0.0.0: Cannot find handler for: GET /bad_path"); + ASSERT_XML_PATH_EQUAL(doc, "//m:InvalidURI/m:URI", "/bad_path"); + EXPECT_EQ(status::not_found, m_agentTestHelper->session()->m_code); + EXPECT_FALSE(m_agentTestHelper->m_dispatched); + } + + { + PARSE_XML_RESPONSE("/bad/path/"); + ASSERT_XML_PATH_EQUAL(doc, "//m:InvalidURI@errorCode", "INVALID_URI"); + ASSERT_XML_PATH_EQUAL(doc, "//m:InvalidURI/m:ErrorMessage", + "0.0.0.0: Cannot find handler for: GET /bad/path/"); + ASSERT_XML_PATH_EQUAL(doc, "//m:InvalidURI/m:URI", "/bad/path/"); + + EXPECT_EQ(status::not_found, m_agentTestHelper->session()->m_code); + EXPECT_FALSE(m_agentTestHelper->m_dispatched); + } + + { + PARSE_XML_RESPONSE("/LinuxCNC/current/blah"); + ASSERT_XML_PATH_EQUAL(doc, "//m:InvalidURI@errorCode", "INVALID_URI"); + ASSERT_XML_PATH_EQUAL(doc, "//m:InvalidURI/m:ErrorMessage", + "0.0.0.0: Cannot find handler for: GET /LinuxCNC/current/blah"); + ASSERT_XML_PATH_EQUAL(doc, "//m:InvalidURI/m:URI", "/LinuxCNC/current/blah"); + + EXPECT_EQ(status::not_found, m_agentTestHelper->session()->m_code); + EXPECT_FALSE(m_agentTestHelper->m_dispatched); + } +} + +TEST_F(AgentTest, should_handle_current_at) { QueryMap query; PARSE_XML_RESPONSE_QUERY("/current", query); @@ -303,7 +393,7 @@ TEST_F(AgentTest, CurrentAt) } } -TEST_F(AgentTest, CurrentAt64) +TEST_F(AgentTest, should_handle_64_bit_current_at) { QueryMap query; @@ -335,7 +425,7 @@ TEST_F(AgentTest, CurrentAt64) } } -TEST_F(AgentTest, CurrentAtOutOfRange) +TEST_F(AgentTest, should_report_out_of_range_for_current_at) { QueryMap query; @@ -373,9 +463,56 @@ TEST_F(AgentTest, CurrentAtOutOfRange) } } +TEST_F(AgentTest, should_report_2_6_out_of_range_for_current_at) +{ + m_agentTestHelper->createAgent("/samples/test_config.xml", 8, 4, "2.6", 4, false, true, + {{configuration::Validation, false}}); + + QueryMap query; + + addAdapter(); + + // Get the current position + char line[80] = {0}; + + // Add many events + for (int i = 1; i <= 200; i++) + { + sprintf(line, "2021-02-01T12:00:00Z|line|%d", i); + m_agentTestHelper->m_adapter->processData(line); + } + + auto &circ = m_agentTestHelper->getAgent()->getCircularBuffer(); + auto seq = circ.getSequence(); + + { + query["at"] = to_string(seq); + sprintf(line, "'at' must be less than %d", int32_t(seq)); + PARSE_XML_RESPONSE_QUERY("/current", query); + ASSERT_XML_PATH_EQUAL(doc, "//m:OutOfRange@errorCode", "OUT_OF_RANGE"); + ASSERT_XML_PATH_EQUAL(doc, "//m:OutOfRange/m:URI", "/current?at=252"); + ASSERT_XML_PATH_EQUAL(doc, "//m:OutOfRange/m:QueryParameter/m:Value", "252"); + ASSERT_XML_PATH_EQUAL(doc, "//m:OutOfRange/m:QueryParameter/m:Minimum", "1"); + ASSERT_XML_PATH_EQUAL(doc, "//m:OutOfRange/m:QueryParameter/m:Maximum", "251"); + } + + seq = circ.getFirstSequence() - 1; + + { + query["at"] = to_string(seq); + sprintf(line, "'at' must be greater than %d", int32_t(seq)); + PARSE_XML_RESPONSE_QUERY("/current", query); + ASSERT_XML_PATH_EQUAL(doc, "//m:OutOfRange@errorCode", "OUT_OF_RANGE"); + ASSERT_XML_PATH_EQUAL(doc, "//m:OutOfRange/m:URI", "/current?at=0"); + ASSERT_XML_PATH_EQUAL(doc, "//m:OutOfRange/m:QueryParameter/m:Value", "0"); + ASSERT_XML_PATH_EQUAL(doc, "//m:OutOfRange/m:QueryParameter/m:Minimum", "1"); + ASSERT_XML_PATH_EQUAL(doc, "//m:OutOfRange/m:QueryParameter/m:Maximum", "251"); + } +} + TEST_F(AgentTest, AddAdapter) { addAdapter(); } -TEST_F(AgentTest, FileDownload) +TEST_F(AgentTest, should_download_file) { QueryMap query; @@ -394,7 +531,7 @@ TEST_F(AgentTest, FileDownload) string::npos); } -TEST_F(AgentTest, FailedFileDownload) +TEST_F(AgentTest, should_report_not_found_when_cannot_find_file) { QueryMap query; @@ -406,13 +543,35 @@ TEST_F(AgentTest, FailedFileDownload) { PARSE_XML_RESPONSE(uri.c_str()); - ASSERT_XML_PATH_EQUAL(doc, "//m:MTConnectError/m:Errors/m:Error@errorCode", "INVALID_REQUEST"); + ASSERT_XML_PATH_EQUAL(doc, "//m:MTConnectError/m:Errors/m:Error@errorCode", "INVALID_URI"); ASSERT_XML_PATH_EQUAL(doc, "//m:MTConnectError/m:Errors/m:Error", ("0.0.0.0: Cannot find handler for: GET " + uri).c_str()); } } -TEST_F(AgentTest, Composition) +TEST_F(AgentTest, should_report_2_6_not_found_when_cannot_find_file) +{ + m_agentTestHelper->createAgent("/samples/test_config.xml", 8, 4, "2.6", 4, false, true, + {{configuration::Validation, false}}); + + QueryMap query; + + string uri("/schemas/MTConnectDevices_1.1.xsd"); + + // Register a file with the agent. + auto rest = m_agentTestHelper->getRestService(); + rest->getFileCache()->registerFile(uri, string("./BadFileName.xsd"), "1.1"); + + { + PARSE_XML_RESPONSE(uri.c_str()); + ASSERT_XML_PATH_EQUAL(doc, "//m:InvalidURI@errorCode", "INVALID_URI"); + ASSERT_XML_PATH_EQUAL(doc, "//m:InvalidURI/m:ErrorMessage", + ("0.0.0.0: Cannot find handler for: GET " + uri).c_str()); + ASSERT_XML_PATH_EQUAL(doc, "//m:InvalidURI/m:URI", "/schemas/MTConnectDevices_1.1.xsd"); + } +} + +TEST_F(AgentTest, should_include_composition_ids_in_observations) { auto agent = m_agentTestHelper->m_agent.get(); addAdapter(); @@ -437,16 +596,16 @@ TEST_F(AgentTest, Composition) } } -TEST_F(AgentTest, BadCount) +TEST_F(AgentTest, should_report_an_error_when_the_count_is_out_of_range) { auto &circ = m_agentTestHelper->getAgent()->getCircularBuffer(); int size = circ.getBufferSize() + 1; { QueryMap query {{"count", "NON_INTEGER"}}; PARSE_XML_RESPONSE_QUERY("/sample", query); - ASSERT_XML_PATH_EQUAL(doc, "//m:Error@errorCode", "INVALID_REQUEST"); + ASSERT_XML_PATH_EQUAL(doc, "//m:Error@errorCode", "INVALID_PARAMETER_VALUE"); ASSERT_XML_PATH_EQUAL(doc, "//m:Error", - "0.0.0.0: Parameter Error: for query parameter 'count': cannot convert " + "query parameter 'count': cannot convert " "string 'NON_INTEGER' to integer"); } @@ -494,7 +653,110 @@ TEST_F(AgentTest, BadCount) } } -TEST_F(AgentTest, Adapter) +TEST_F(AgentTest, should_report_a_2_6_error_when_the_count_is_out_of_range) +{ + m_agentTestHelper->createAgent("/samples/test_config.xml", 8, 4, "2.6", 4, false, true, + {{configuration::Validation, false}}); + + auto &circ = m_agentTestHelper->getAgent()->getCircularBuffer(); + int size = circ.getBufferSize() + 1; + { + QueryMap query {{"count", "NON_INTEGER"}}; + PARSE_XML_RESPONSE_QUERY("/sample", query); + ASSERT_XML_PATH_EQUAL(doc, "//m:InvalidParameterValue@errorCode", "INVALID_PARAMETER_VALUE"); + ASSERT_XML_PATH_EQUAL(doc, "//m:InvalidParameterValue/m:URI", "/sample?count=NON_INTEGER"); + ASSERT_XML_PATH_EQUAL(doc, "//m:InvalidParameterValue/m:ErrorMessage", + "query parameter 'count': cannot convert " + "string 'NON_INTEGER' to integer"); + ASSERT_XML_PATH_EQUAL(doc, "//m:InvalidParameterValue/m:QueryParameter@name", "count"); + ASSERT_XML_PATH_EQUAL(doc, "//m:InvalidParameterValue/m:QueryParameter/m:Format", "int32"); + ASSERT_XML_PATH_EQUAL(doc, "//m:InvalidParameterValue/m:QueryParameter/m:Type", "integer"); + ASSERT_XML_PATH_EQUAL(doc, "//m:InvalidParameterValue/m:QueryParameter/m:Value", "NON_INTEGER"); + } + + { + QueryMap query {{"count", "-500"}}; + PARSE_XML_RESPONSE_QUERY("/sample", query); + ASSERT_XML_PATH_EQUAL(doc, "//m:OutOfRange@errorCode", "OUT_OF_RANGE"); + string value("'count' must be greater than "); + value += to_string(-size); + ASSERT_XML_PATH_EQUAL(doc, "//m:OutOfRange/m:ErrorMessage", value.c_str()); + ASSERT_XML_PATH_EQUAL(doc, "//m:OutOfRange/m:URI", "/sample?count=-500"); + ASSERT_XML_PATH_EQUAL(doc, "//m:OutOfRange/m:QueryParameter@name", "count"); + ASSERT_XML_PATH_EQUAL(doc, "//m:OutOfRange/m:QueryParameter/m:Value", "-500"); + ASSERT_XML_PATH_EQUAL(doc, "//m:OutOfRange/m:QueryParameter/m:Maximum", + to_string(size - 1).c_str()); + ASSERT_XML_PATH_EQUAL(doc, "//m:OutOfRange/m:QueryParameter/m:Minimum", + to_string(-size + 1).c_str()); + } + + { + QueryMap query {{"count", "0"}}; + PARSE_XML_RESPONSE_QUERY("/sample", query); + ASSERT_XML_PATH_EQUAL(doc, "//m:OutOfRange@errorCode", "OUT_OF_RANGE"); + ASSERT_XML_PATH_EQUAL(doc, "//m:OutOfRange/m:ErrorMessage", "'count' must not be zero(0)"); + ASSERT_XML_PATH_EQUAL(doc, "//m:OutOfRange/m:URI", "/sample?count=0"); + ASSERT_XML_PATH_EQUAL(doc, "//m:OutOfRange/m:QueryParameter@name", "count"); + ASSERT_XML_PATH_EQUAL(doc, "//m:OutOfRange/m:QueryParameter/m:Value", "0"); + ASSERT_XML_PATH_EQUAL(doc, "//m:OutOfRange/m:QueryParameter/m:Maximum", + to_string(size - 1).c_str()); + ASSERT_XML_PATH_EQUAL(doc, "//m:OutOfRange/m:QueryParameter/m:Minimum", + to_string(-size + 1).c_str()); + } + + { + QueryMap query {{"count", "500"}}; + PARSE_XML_RESPONSE_QUERY("/sample", query); + string value("'count' must be less than "); + value += to_string(size); + + ASSERT_XML_PATH_EQUAL(doc, "//m:OutOfRange@errorCode", "OUT_OF_RANGE"); + ASSERT_XML_PATH_EQUAL(doc, "//m:OutOfRange/m:ErrorMessage", value.c_str()); + ASSERT_XML_PATH_EQUAL(doc, "//m:OutOfRange/m:URI", "/sample?count=500"); + ASSERT_XML_PATH_EQUAL(doc, "//m:OutOfRange/m:QueryParameter@name", "count"); + ASSERT_XML_PATH_EQUAL(doc, "//m:OutOfRange/m:QueryParameter/m:Value", "500"); + ASSERT_XML_PATH_EQUAL(doc, "//m:OutOfRange/m:QueryParameter/m:Maximum", + to_string(size - 1).c_str()); + ASSERT_XML_PATH_EQUAL(doc, "//m:OutOfRange/m:QueryParameter/m:Minimum", + to_string(-size + 1).c_str()); + } + + { + QueryMap query {{"count", "9999999"}}; + PARSE_XML_RESPONSE_QUERY("/sample", query); + string value("'count' must be less than "); + value += to_string(size); + + ASSERT_XML_PATH_EQUAL(doc, "//m:OutOfRange@errorCode", "OUT_OF_RANGE"); + ASSERT_XML_PATH_EQUAL(doc, "//m:OutOfRange/m:ErrorMessage", value.c_str()); + ASSERT_XML_PATH_EQUAL(doc, "//m:OutOfRange/m:URI", "/sample?count=9999999"); + ASSERT_XML_PATH_EQUAL(doc, "//m:OutOfRange/m:QueryParameter@name", "count"); + ASSERT_XML_PATH_EQUAL(doc, "//m:OutOfRange/m:QueryParameter/m:Value", "9999999"); + ASSERT_XML_PATH_EQUAL(doc, "//m:OutOfRange/m:QueryParameter/m:Maximum", + to_string(size - 1).c_str()); + ASSERT_XML_PATH_EQUAL(doc, "//m:OutOfRange/m:QueryParameter/m:Minimum", + to_string(-size + 1).c_str()); + } + + { + QueryMap query {{"count", "-9999999"}}; + PARSE_XML_RESPONSE_QUERY("/sample", query); + string value("'count' must be greater than "); + value += to_string(-size); + + ASSERT_XML_PATH_EQUAL(doc, "//m:OutOfRange@errorCode", "OUT_OF_RANGE"); + ASSERT_XML_PATH_EQUAL(doc, "//m:OutOfRange/m:ErrorMessage", value.c_str()); + ASSERT_XML_PATH_EQUAL(doc, "//m:OutOfRange/m:URI", "/sample?count=-9999999"); + ASSERT_XML_PATH_EQUAL(doc, "//m:OutOfRange/m:QueryParameter@name", "count"); + ASSERT_XML_PATH_EQUAL(doc, "//m:OutOfRange/m:QueryParameter/m:Value", "-9999999"); + ASSERT_XML_PATH_EQUAL(doc, "//m:OutOfRange/m:QueryParameter/m:Maximum", + to_string(size - 1).c_str()); + ASSERT_XML_PATH_EQUAL(doc, "//m:OutOfRange/m:QueryParameter/m:Minimum", + to_string(-size + 1).c_str()); + } +} + +TEST_F(AgentTest, should_process_addapter_data) { addAdapter(); @@ -524,7 +786,7 @@ TEST_F(AgentTest, Adapter) } } -TEST_F(AgentTest, SampleAtNextSeq) +TEST_F(AgentTest, should_get_samples_using_next_sequence) { QueryMap query; addAdapter(); @@ -705,7 +967,7 @@ TEST_F(AgentTest, should_give_empty_stream_with_no_new_samples) } } -TEST_F(AgentTest, AddToBuffer) +TEST_F(AgentTest, should_not_leak_observations_when_added_to_buffer) { auto agent = m_agentTestHelper->m_agent.get(); QueryMap query; @@ -797,7 +1059,7 @@ TEST_F(AgentTest, should_int_64_sequences_should_not_truncate_at_32_bits) #endif } -TEST_F(AgentTest, DuplicateCheck) +TEST_F(AgentTest, should_not_allow_duplicates_values) { addAdapter(); @@ -824,7 +1086,7 @@ TEST_F(AgentTest, DuplicateCheck) } } -TEST_F(AgentTest, DuplicateCheckAfterDisconnect) +TEST_F(AgentTest, should_not_duplicate_unavailable_when_disconnected) { addAdapter({{configuration::FilterDuplicates, true}}); @@ -863,7 +1125,7 @@ TEST_F(AgentTest, DuplicateCheckAfterDisconnect) } } -TEST_F(AgentTest, AutoAvailable) +TEST_F(AgentTest, should_handle_auto_available_if_adapter_option_is_set) { addAdapter({{configuration::AutoAvailable, true}}); auto agent = m_agentTestHelper->m_agent.get(); @@ -906,7 +1168,7 @@ TEST_F(AgentTest, AutoAvailable) } } -TEST_F(AgentTest, MultipleDisconnect) +TEST_F(AgentTest, should_handle_multiple_disconnnects) { addAdapter(); auto agent = m_agentTestHelper->m_agent.get(); @@ -976,7 +1238,7 @@ TEST_F(AgentTest, MultipleDisconnect) } } -TEST_F(AgentTest, IgnoreTimestamps) +TEST_F(AgentTest, should_ignore_timestamps_if_configured_to_do_so) { addAdapter(); @@ -1010,7 +1272,7 @@ TEST_F(AgentTest, InitialTimeSeriesValues) } } -TEST_F(AgentTest, DynamicCalibration) +TEST_F(AgentTest, should_support_dynamic_calibration_data) { addAdapter({{configuration::ConversionRequired, true}}); auto agent = m_agentTestHelper->getAgent(); @@ -1052,7 +1314,7 @@ TEST_F(AgentTest, DynamicCalibration) } } -TEST_F(AgentTest, FilterValues13) +TEST_F(AgentTest, should_filter_as_specified_in_1_3_test_1) { m_agentTestHelper->createAgent("/samples/filter_example_1.3.xml", 8, 4, "1.5", 25); addAdapter(); @@ -1091,7 +1353,7 @@ TEST_F(AgentTest, FilterValues13) } } -TEST_F(AgentTest, FilterValues) +TEST_F(AgentTest, should_filter_as_specified_in_1_3_test_2) { m_agentTestHelper->createAgent("/samples/filter_example_1.3.xml", 8, 4, "1.5", 25); addAdapter(); @@ -1155,7 +1417,7 @@ TEST_F(AgentTest, FilterValues) } } -TEST_F(AgentTest, TestPeriodFilterWithIgnoreTimestamps) +TEST_F(AgentTest, period_filter_should_work_with_ignore_timestamps) { // Test period filter with ignore timestamps m_agentTestHelper->createAgent("/samples/filter_example_1.3.xml", 8, 4, "1.5", 25); @@ -1186,7 +1448,7 @@ TEST_F(AgentTest, TestPeriodFilterWithIgnoreTimestamps) } } -TEST_F(AgentTest, TestPeriodFilterWithRelativeTime) +TEST_F(AgentTest, period_filter_should_work_with_relative_time) { // Test period filter with relative time m_agentTestHelper->createAgent("/samples/filter_example_1.3.xml", 8, 4, "1.5", 25); @@ -1216,7 +1478,7 @@ TEST_F(AgentTest, TestPeriodFilterWithRelativeTime) } } -TEST_F(AgentTest, ResetTriggered) +TEST_F(AgentTest, reset_triggered_should_work) { addAdapter(); @@ -1239,7 +1501,7 @@ TEST_F(AgentTest, ResetTriggered) } } -TEST_F(AgentTest, References) +TEST_F(AgentTest, should_honor_references_when_getting_current_or_sample) { using namespace device_model; @@ -1301,7 +1563,7 @@ TEST_F(AgentTest, References) } } -TEST_F(AgentTest, Discrete) +TEST_F(AgentTest, should_honor_discrete_data_items_and_not_filter_dups) { m_agentTestHelper->createAgent("/samples/discrete_example.xml"); addAdapter({{configuration::FilterDuplicates, true}}); @@ -1345,7 +1607,7 @@ TEST_F(AgentTest, Discrete) // ------------------------------ -TEST_F(AgentTest, UpcaseValues) +TEST_F(AgentTest, should_honor_upcase_values) { addAdapter({{configuration::FilterDuplicates, true}, {configuration::UpcaseDataItemValue, true}}); @@ -1365,7 +1627,7 @@ TEST_F(AgentTest, UpcaseValues) } } -TEST_F(AgentTest, ConditionSequence) +TEST_F(AgentTest, should_handle_condition_activation) { addAdapter({{configuration::FilterDuplicates, true}}); auto agent = m_agentTestHelper->getAgent(); @@ -1544,7 +1806,7 @@ TEST_F(AgentTest, ConditionSequence) } } -TEST_F(AgentTest, EmptyLastItemFromAdapter) +TEST_F(AgentTest, should_handle_empty_entry_as_last_pair_from_adapter) { addAdapter({{configuration::FilterDuplicates, true}}); auto agent = m_agentTestHelper->getAgent(); @@ -1605,7 +1867,7 @@ TEST_F(AgentTest, EmptyLastItemFromAdapter) } } -TEST_F(AgentTest, ConstantValue) +TEST_F(AgentTest, should_handle_constant_values) { addAdapter(); auto agent = m_agentTestHelper->getAgent(); @@ -1632,7 +1894,7 @@ TEST_F(AgentTest, ConstantValue) } } -TEST_F(AgentTest, BadDataItem) +TEST_F(AgentTest, should_handle_bad_data_item_from_adapter) { addAdapter(); @@ -1844,7 +2106,7 @@ TEST_F(AgentTest, should_handle_uuid_change) // ------------------------- Asset Tests --------------------------------- -TEST_F(AgentTest, AssetStorage) +TEST_F(AgentTest, should_store_assets_in_buffer) { auto agent = m_agentTestHelper->createAgent("/samples/test_config.xml", 8, 4, "1.3", 4, true); @@ -1879,7 +2141,7 @@ TEST_F(AgentTest, AssetStorage) } } -TEST_F(AgentTest, AssetBuffer) +TEST_F(AgentTest, should_handle_asset_buffer_and_buffer_limits) { auto agent = m_agentTestHelper->createAgent("/samples/test_config.xml", 8, 4, "1.3", 4, true); string body = "TEST 1"; @@ -2001,8 +2263,7 @@ TEST_F(AgentTest, AssetBuffer) { PARSE_XML_RESPONSE("/asset/P1"); ASSERT_XML_PATH_EQUAL(doc, "//m:MTConnectError/m:Errors/m:Error@errorCode", "ASSET_NOT_FOUND"); - ASSERT_XML_PATH_EQUAL(doc, "//m:MTConnectError/m:Errors/m:Error", - "Cannot find asset for asset Ids: P1"); + ASSERT_XML_PATH_EQUAL(doc, "//m:MTConnectError/m:Errors/m:Error", "Cannot find asset: P1"); } body = "TEST 6"; @@ -2051,22 +2312,53 @@ TEST_F(AgentTest, AssetBuffer) { PARSE_XML_RESPONSE("/asset/P4"); ASSERT_XML_PATH_EQUAL(doc, "//m:MTConnectError/m:Errors/m:Error@errorCode", "ASSET_NOT_FOUND"); - ASSERT_XML_PATH_EQUAL(doc, "//m:MTConnectError/m:Errors/m:Error", - "Cannot find asset for asset Ids: P4"); + ASSERT_XML_PATH_EQUAL(doc, "//m:MTConnectError/m:Errors/m:Error", "Cannot find asset: P4"); } } -TEST_F(AgentTest, AssetError) +TEST_F(AgentTest, should_report_asset_not_found_error) { { PARSE_XML_RESPONSE("/asset/123"); ASSERT_XML_PATH_EQUAL(doc, "//m:MTConnectError/m:Errors/m:Error@errorCode", "ASSET_NOT_FOUND"); - ASSERT_XML_PATH_EQUAL(doc, "//m:MTConnectError/m:Errors/m:Error", - "Cannot find asset for asset Ids: 123"); + ASSERT_XML_PATH_EQUAL(doc, "//m:MTConnectError/m:Errors/m:Error", "Cannot find asset: 123"); + } +} + +TEST_F(AgentTest, should_report_asset_not_found_2_6_error) +{ + m_agentTestHelper->createAgent("/samples/test_config.xml", 8, 4, "2.6", 4, false, true, + {{configuration::Validation, false}}); + + { + PARSE_XML_RESPONSE("/asset/123"); + ASSERT_XML_PATH_EQUAL(doc, "//m:AssetNotFound@errorCode", "ASSET_NOT_FOUND"); + ASSERT_XML_PATH_EQUAL(doc, "//m:AssetNotFound/m:ErrorMessage", "Cannot find asset: 123"); + ASSERT_XML_PATH_EQUAL(doc, "//m:AssetNotFound/m:AssetId", "123"); + ASSERT_XML_PATH_EQUAL(doc, "//m:AssetNotFound/m:URI", "/asset/123"); } } -TEST_F(AgentTest, AdapterAddAsset) +TEST_F(AgentTest, should_report_asset_not_found_2_6_error_with_multiple_assets) +{ + m_agentTestHelper->createAgent("/samples/test_config.xml", 8, 4, "2.6", 4, false, true, + {{configuration::Validation, false}}); + + { + PARSE_XML_RESPONSE("/asset/123;456"); + ASSERT_XML_PATH_EQUAL(doc, "//m:Errors/m:AssetNotFound[1]@errorCode", "ASSET_NOT_FOUND"); + ASSERT_XML_PATH_EQUAL(doc, "//m:AssetNotFound[1]/m:ErrorMessage", "Cannot find asset: 123"); + ASSERT_XML_PATH_EQUAL(doc, "//m:AssetNotFound[1]/m:AssetId", "123"); + ASSERT_XML_PATH_EQUAL(doc, "//m:AssetNotFound[1]/m:URI", "/asset/123;456"); + + ASSERT_XML_PATH_EQUAL(doc, "//m:Errors/m:AssetNotFound[2]@errorCode", "ASSET_NOT_FOUND"); + ASSERT_XML_PATH_EQUAL(doc, "//m:AssetNotFound[2]/m:ErrorMessage", "Cannot find asset: 456"); + ASSERT_XML_PATH_EQUAL(doc, "//m:AssetNotFound[2]/m:AssetId", "456"); + ASSERT_XML_PATH_EQUAL(doc, "//m:AssetNotFound[2]/m:URI", "/asset/123;456"); + } +} + +TEST_F(AgentTest, should_handle_asset_from_adapter_on_one_line) { addAdapter(); auto agent = m_agentTestHelper->getAgent(); @@ -2084,7 +2376,7 @@ TEST_F(AgentTest, AdapterAddAsset) } } -TEST_F(AgentTest, MultiLineAsset) +TEST_F(AgentTest, should_handle_multiline_asset) { addAdapter(); auto agent = m_agentTestHelper->getAgent(); @@ -2122,7 +2414,7 @@ TEST_F(AgentTest, MultiLineAsset) } } -TEST_F(AgentTest, BadAsset) +TEST_F(AgentTest, should_handle_bad_asset_from_adapter) { addAdapter(); const auto &storage = m_agentTestHelper->m_agent->getAssetStorage(); @@ -2134,7 +2426,7 @@ TEST_F(AgentTest, BadAsset) ASSERT_EQ((unsigned int)0, storage->getCount()); } -TEST_F(AgentTest, AssetRemoval) +TEST_F(AgentTest, should_handle_asset_removal_from_REST_api) { string body = "TEST 1"; QueryMap query; @@ -2228,7 +2520,7 @@ TEST_F(AgentTest, AssetRemoval) } } -TEST_F(AgentTest, AssetRemovalByAdapter) +TEST_F(AgentTest, should_handle_asset_removal_from_adapter) { addAdapter(); QueryMap query; @@ -2641,9 +2933,9 @@ TEST_F(AgentTest, interval_should_be_a_valid_integer_value) { query["interval"] = "NON_INTEGER"; PARSE_XML_RESPONSE_QUERY("/sample", query); - ASSERT_XML_PATH_EQUAL(doc, "//m:Error@errorCode", "INVALID_REQUEST"); + ASSERT_XML_PATH_EQUAL(doc, "//m:Error@errorCode", "INVALID_PARAMETER_VALUE"); ASSERT_XML_PATH_EQUAL(doc, "//m:Error", - "0.0.0.0: Parameter Error: for query parameter 'interval': cannot " + "query parameter 'interval': cannot " "convert string 'NON_INTEGER' to integer"); } @@ -2672,6 +2964,73 @@ TEST_F(AgentTest, interval_should_be_a_valid_integer_value) } } +/// @test ensure an error is returned when the interval has an invalid value using 2.6 error +/// reporting +TEST_F(AgentTest, interval_should_be_a_valid_integer_value_in_2_6) +{ + m_agentTestHelper->createAgent("/samples/test_config.xml", 8, 4, "2.6", 4, false, true, + {{configuration::Validation, false}}); + QueryMap query; + + /// - Cannot be test or a non-integer value + { + query["interval"] = "NON_INTEGER"; + PARSE_XML_RESPONSE_QUERY("/sample", query); + ASSERT_XML_PATH_EQUAL(doc, "//m:InvalidParameterValue@errorCode", "INVALID_PARAMETER_VALUE"); + ASSERT_XML_PATH_EQUAL(doc, "//m:InvalidParameterValue/m:URI", "/sample?interval=NON_INTEGER"); + ASSERT_XML_PATH_EQUAL(doc, "//m:InvalidParameterValue/m:ErrorMessage", + "query parameter 'interval': cannot convert " + "string 'NON_INTEGER' to integer"); + ASSERT_XML_PATH_EQUAL(doc, "//m:InvalidParameterValue/m:QueryParameter@name", "interval"); + ASSERT_XML_PATH_EQUAL(doc, "//m:InvalidParameterValue/m:QueryParameter/m:Format", "int32"); + ASSERT_XML_PATH_EQUAL(doc, "//m:InvalidParameterValue/m:QueryParameter/m:Type", "integer"); + ASSERT_XML_PATH_EQUAL(doc, "//m:InvalidParameterValue/m:QueryParameter/m:Value", "NON_INTEGER"); + } + + /// - Cannot be nagative + { + query["interval"] = "-123"; + PARSE_XML_RESPONSE_QUERY("/sample", query); + ASSERT_XML_PATH_EQUAL(doc, "//m:OutOfRange@errorCode", "OUT_OF_RANGE"); + ASSERT_XML_PATH_EQUAL(doc, "//m:OutOfRange/m:URI", "/sample?interval=-123"); + ASSERT_XML_PATH_EQUAL(doc, "//m:OutOfRange/m:ErrorMessage", + "'interval' must be greater than -1"); + ASSERT_XML_PATH_EQUAL(doc, "//m:OutOfRange/m:QueryParameter@name", "interval"); + ASSERT_XML_PATH_EQUAL(doc, "//m:OutOfRange/m:QueryParameter/m:Value", "-123"); + ASSERT_XML_PATH_EQUAL(doc, "//m:OutOfRange/m:QueryParameter/m:Minimum", "0"); + ASSERT_XML_PATH_EQUAL(doc, "//m:OutOfRange/m:QueryParameter/m:Maximum", "2147483646"); + } + + /// - Cannot be >= 2147483647 + { + query["interval"] = "2147483647"; + PARSE_XML_RESPONSE_QUERY("/sample", query); + ASSERT_XML_PATH_EQUAL(doc, "//m:OutOfRange@errorCode", "OUT_OF_RANGE"); + + ASSERT_XML_PATH_EQUAL(doc, "//m:OutOfRange/m:URI", "/sample?interval=2147483647"); + ASSERT_XML_PATH_EQUAL(doc, "//m:OutOfRange/m:ErrorMessage", + "'interval' must be less than 2147483647"); + ASSERT_XML_PATH_EQUAL(doc, "//m:OutOfRange/m:QueryParameter@name", "interval"); + ASSERT_XML_PATH_EQUAL(doc, "//m:OutOfRange/m:QueryParameter/m:Value", "2147483647"); + ASSERT_XML_PATH_EQUAL(doc, "//m:OutOfRange/m:QueryParameter/m:Minimum", "0"); + ASSERT_XML_PATH_EQUAL(doc, "//m:OutOfRange/m:QueryParameter/m:Maximum", "2147483646"); + } + + /// - Cannot wrap around and create a negative number was set as a int32 + { + query["interval"] = "999999999999999999"; + PARSE_XML_RESPONSE_QUERY("/sample", query); + ASSERT_XML_PATH_EQUAL(doc, "//m:OutOfRange@errorCode", "OUT_OF_RANGE"); + ASSERT_XML_PATH_EQUAL(doc, "//m:OutOfRange/m:URI", "/sample?interval=999999999999999999"); + ASSERT_XML_PATH_EQUAL(doc, "//m:OutOfRange/m:ErrorMessage", + "'interval' must be greater than -1"); + ASSERT_XML_PATH_EQUAL(doc, "//m:OutOfRange/m:QueryParameter@name", "interval"); + ASSERT_XML_PATH_EQUAL(doc, "//m:OutOfRange/m:QueryParameter/m:Value", "-1486618625"); + ASSERT_XML_PATH_EQUAL(doc, "//m:OutOfRange/m:QueryParameter/m:Minimum", "0"); + ASSERT_XML_PATH_EQUAL(doc, "//m:OutOfRange/m:QueryParameter/m:Maximum", "2147483646"); + } +} + /// @test check streaming of data every 50ms TEST_F(AgentTest, should_stream_data_with_interval) { diff --git a/test_package/http_server_test.cpp b/test_package/http_server_test.cpp index da52c633a..fa2607c58 100644 --- a/test_package/http_server_test.cpp +++ b/test_package/http_server_test.cpp @@ -277,7 +277,7 @@ class Client m_headerHandler; }; -class RestServiceTest : public testing::Test +class HttpServerTest : public testing::Test { protected: void SetUp() override @@ -324,7 +324,7 @@ class RestServiceTest : public testing::Test unique_ptr m_client; }; -TEST_F(RestServiceTest, simple_request_response) +TEST_F(HttpServerTest, simple_request_response) { weak_ptr savedSession; @@ -362,7 +362,7 @@ TEST_F(RestServiceTest, simple_request_response) ASSERT_TRUE(savedSession.expired()); } -TEST_F(RestServiceTest, request_response_with_query_parameters) +TEST_F(HttpServerTest, request_response_with_query_parameters) { auto handler = [&](SessionPtr session, RequestPtr request) -> bool { EXPECT_EQ("device1", get(request->m_parameters["device"])); @@ -397,7 +397,7 @@ TEST_F(RestServiceTest, request_response_with_query_parameters) EXPECT_EQ(200, m_client->m_status); } -TEST_F(RestServiceTest, request_put_when_put_not_allowed) +TEST_F(HttpServerTest, request_put_when_put_not_allowed) { auto probe = [&](SessionPtr session, RequestPtr request) -> bool { EXPECT_TRUE(false); @@ -413,12 +413,12 @@ TEST_F(RestServiceTest, request_put_when_put_not_allowed) ASSERT_TRUE(m_client->m_done); EXPECT_EQ(int(http::status::bad_request), m_client->m_status); EXPECT_EQ( - "PUT, POST, and DELETE are not allowed. MTConnect Agent is read only and only GET is " - "allowed.", + "InternalError: PUT, POST, and DELETE are not allowed. MTConnect Agent is read only and only " + "GET is allowed.", m_client->m_result); } -TEST_F(RestServiceTest, request_put_when_put_allowed) +TEST_F(HttpServerTest, request_put_when_put_allowed) { auto handler = [&](SessionPtr session, RequestPtr request) -> bool { EXPECT_EQ(http::verb::put, request->m_verb); @@ -442,7 +442,7 @@ TEST_F(RestServiceTest, request_put_when_put_allowed) EXPECT_EQ("Put ok", m_client->m_result); } -TEST_F(RestServiceTest, request_put_when_put_not_allowed_from_ip_address) +TEST_F(HttpServerTest, request_put_when_put_not_allowed_from_ip_address) { weak_ptr session; @@ -460,10 +460,11 @@ TEST_F(RestServiceTest, request_put_when_put_not_allowed_from_ip_address) m_client->spawnRequest(http::verb::put, "/probe"); ASSERT_TRUE(m_client->m_done); EXPECT_EQ(int(http::status::bad_request), m_client->m_status); - EXPECT_EQ("PUT, POST, and DELETE are not allowed from 127.0.0.1", m_client->m_result); + EXPECT_EQ("InternalError: PUT, POST, and DELETE are not allowed from 127.0.0.1", + m_client->m_result); } -TEST_F(RestServiceTest, request_put_when_put_allowed_from_ip_address) +TEST_F(HttpServerTest, request_put_when_put_allowed_from_ip_address) { auto handler = [&](SessionPtr session, RequestPtr request) -> bool { EXPECT_EQ(http::verb::put, request->m_verb); @@ -487,7 +488,7 @@ TEST_F(RestServiceTest, request_put_when_put_allowed_from_ip_address) EXPECT_EQ("Put ok", m_client->m_result); } -TEST_F(RestServiceTest, request_with_connect_close) +TEST_F(HttpServerTest, request_with_connect_close) { weak_ptr savedSession; @@ -516,7 +517,7 @@ TEST_F(RestServiceTest, request_with_connect_close) EXPECT_FALSE(savedSession.lock()); } -TEST_F(RestServiceTest, put_content_to_server) +TEST_F(HttpServerTest, put_content_to_server) { string body; auto handler = [&](SessionPtr session, RequestPtr request) -> bool { @@ -538,7 +539,7 @@ TEST_F(RestServiceTest, put_content_to_server) ASSERT_EQ("Body Content", body); } -TEST_F(RestServiceTest, put_content_with_put_values) +TEST_F(HttpServerTest, put_content_with_put_values) { string body, ct; auto handler = [&](SessionPtr session, RequestPtr request) -> bool { @@ -565,7 +566,7 @@ TEST_F(RestServiceTest, put_content_with_put_values) ASSERT_EQ("application/x-www-form-urlencoded", ct); } -TEST_F(RestServiceTest, streaming_response) +TEST_F(HttpServerTest, streaming_response) { struct context { @@ -648,7 +649,7 @@ TEST_F(RestServiceTest, streaming_response) ; } -TEST_F(RestServiceTest, additional_header_fields) +TEST_F(HttpServerTest, additional_header_fields) { m_server->setHttpHeaders({"Access-Control-Allow-Origin:*", "Origin:https://foo.example"}); @@ -682,7 +683,7 @@ const string KeyFile {TEST_RESOURCE_DIR "/user.key"}; const string DhFile {TEST_RESOURCE_DIR "/dh2048.pem"}; const string RootCertFile(TEST_RESOURCE_DIR "/rootca.crt"); -TEST_F(RestServiceTest, failure_when_tls_only) +TEST_F(HttpServerTest, failure_when_tls_only) { using namespace mtconnect::configuration; ConfigOptions options {{TlsCertificateChain, CertFile}, diff --git a/test_package/json_printer_error_test.cpp b/test_package/json_printer_error_test.cpp index bc50ab1ea..0fb0166f5 100644 --- a/test_package/json_printer_error_test.cpp +++ b/test_package/json_printer_error_test.cpp @@ -59,6 +59,7 @@ class JsonPrinterErrorTest : public testing::Test TEST_F(JsonPrinterErrorTest, should_print_legacy_error) { + m_printer->setSchemaVersion("2.5"); auto error = Error::make(Error::ErrorCode::INVALID_REQUEST, "ERROR TEXT!"); auto doc = m_printer->printError(123, 9999, 1, error, true); @@ -80,10 +81,10 @@ TEST_F(JsonPrinterErrorTest, should_print_error_with_2_6_invalid_request) { m_printer->setSchemaVersion("2.6"); m_printer->setSenderName("MachineXXX"); - + auto error = Error::make(Error::ErrorCode::INVALID_REQUEST, "ERROR TEXT!"); auto doc = m_printer->printError(123, 9999, 1, error, true); - + // cout << doc << endl; auto jdoc = json::parse(doc); auto it = jdoc.begin(); @@ -91,22 +92,23 @@ TEST_F(JsonPrinterErrorTest, should_print_error_with_2_6_invalid_request) ASSERT_EQ(123, jdoc.at("/MTConnectError/Header/instanceId"_json_pointer).get()); ASSERT_EQ(9999, jdoc.at("/MTConnectError/Header/bufferSize"_json_pointer).get()); ASSERT_EQ(false, jdoc.at("/MTConnectError/Header/testIndicator"_json_pointer).get()); - - ASSERT_EQ(string("INVALID_REQUEST"), - jdoc.at("/MTConnectError/Errors/0/InvalidRequest/errorCode"_json_pointer).get()); - ASSERT_EQ(string("ERROR TEXT!"), - jdoc.at("/MTConnectError/Errors/0/InvalidRequest/ErrorMessage"_json_pointer).get()); + ASSERT_EQ( + string("INVALID_REQUEST"), + jdoc.at("/MTConnectError/Errors/0/InvalidRequest/errorCode"_json_pointer).get()); + ASSERT_EQ( + string("ERROR TEXT!"), + jdoc.at("/MTConnectError/Errors/0/InvalidRequest/ErrorMessage"_json_pointer).get()); } TEST_F(JsonPrinterErrorTest, should_print_error_with_2_6_invalid_parameter_value) { m_printer->setSchemaVersion("2.6"); m_printer->setSenderName("MachineXXX"); - + auto error = InvalidParameterValue::make("interval", "XXX", "integer", "int64", "Bad Value"); auto doc = m_printer->printError(123, 9999, 1, error, true); - + // cout << doc << endl; auto jdoc = json::parse(doc); auto it = jdoc.begin(); @@ -114,31 +116,39 @@ TEST_F(JsonPrinterErrorTest, should_print_error_with_2_6_invalid_parameter_value ASSERT_EQ(123, jdoc.at("/MTConnectError/Header/instanceId"_json_pointer).get()); ASSERT_EQ(9999, jdoc.at("/MTConnectError/Header/bufferSize"_json_pointer).get()); ASSERT_EQ(false, jdoc.at("/MTConnectError/Header/testIndicator"_json_pointer).get()); - - ASSERT_EQ(string("INVALID_PARAMTER_VALUE"), - jdoc.at("/MTConnectError/Errors/0/InvalidParameterValue/errorCode"_json_pointer).get()); - ASSERT_EQ(string("Bad Value"), - jdoc.at("/MTConnectError/Errors/0/InvalidParameterValue/ErrorMessage"_json_pointer).get()); - ASSERT_EQ(string("interval"), - jdoc.at("/MTConnectError/Errors/0/InvalidParameterValue/QueryParameter/name"_json_pointer).get()); - ASSERT_EQ(string("XXX"), - jdoc.at("/MTConnectError/Errors/0/InvalidParameterValue/QueryParameter/Value"_json_pointer).get()); - ASSERT_EQ(string("integer"), - jdoc.at("/MTConnectError/Errors/0/InvalidParameterValue/QueryParameter/Type"_json_pointer).get()); - ASSERT_EQ(string("int64"), - jdoc.at("/MTConnectError/Errors/0/InvalidParameterValue/QueryParameter/Format"_json_pointer).get()); - + ASSERT_EQ(string("INVALID_PARAMETER_VALUE"), + jdoc.at("/MTConnectError/Errors/0/InvalidParameterValue/errorCode"_json_pointer) + .get()); + ASSERT_EQ(string("Bad Value"), + jdoc.at("/MTConnectError/Errors/0/InvalidParameterValue/ErrorMessage"_json_pointer) + .get()); + ASSERT_EQ( + string("interval"), + jdoc.at("/MTConnectError/Errors/0/InvalidParameterValue/QueryParameter/name"_json_pointer) + .get()); + ASSERT_EQ( + string("XXX"), + jdoc.at("/MTConnectError/Errors/0/InvalidParameterValue/QueryParameter/Value"_json_pointer) + .get()); + ASSERT_EQ( + string("integer"), + jdoc.at("/MTConnectError/Errors/0/InvalidParameterValue/QueryParameter/Type"_json_pointer) + .get()); + ASSERT_EQ( + string("int64"), + jdoc.at("/MTConnectError/Errors/0/InvalidParameterValue/QueryParameter/Format"_json_pointer) + .get()); } TEST_F(JsonPrinterErrorTest, should_print_error_with_2_6_out_of_range) { m_printer->setSchemaVersion("2.6"); m_printer->setSenderName("MachineXXX"); - + auto error = OutOfRange::make("from", 9999999, 10904772, 12907777, "Bad Value"); auto doc = m_printer->printError(123, 9999, 1, error, true); - + // cout << doc << endl; auto jdoc = json::parse(doc); auto it = jdoc.begin(); @@ -146,19 +156,21 @@ TEST_F(JsonPrinterErrorTest, should_print_error_with_2_6_out_of_range) ASSERT_EQ(123, jdoc.at("/MTConnectError/Header/instanceId"_json_pointer).get()); ASSERT_EQ(9999, jdoc.at("/MTConnectError/Header/bufferSize"_json_pointer).get()); ASSERT_EQ(false, jdoc.at("/MTConnectError/Header/testIndicator"_json_pointer).get()); - + ASSERT_EQ(string("OUT_OF_RANGE"), jdoc.at("/MTConnectError/Errors/0/OutOfRange/errorCode"_json_pointer).get()); ASSERT_EQ(string("Bad Value"), jdoc.at("/MTConnectError/Errors/0/OutOfRange/ErrorMessage"_json_pointer).get()); ASSERT_EQ(string("from"), - jdoc.at("/MTConnectError/Errors/0/OutOfRange/QueryParameter/name"_json_pointer).get()); + jdoc.at("/MTConnectError/Errors/0/OutOfRange/QueryParameter/name"_json_pointer) + .get()); ASSERT_EQ(9999999, - jdoc.at("/MTConnectError/Errors/0/OutOfRange/QueryParameter/Value"_json_pointer).get()); + jdoc.at("/MTConnectError/Errors/0/OutOfRange/QueryParameter/Value"_json_pointer) + .get()); ASSERT_EQ(10904772, - jdoc.at("/MTConnectError/Errors/0/OutOfRange/QueryParameter/Minimum"_json_pointer).get()); + jdoc.at("/MTConnectError/Errors/0/OutOfRange/QueryParameter/Minimum"_json_pointer) + .get()); ASSERT_EQ(12907777, - jdoc.at("/MTConnectError/Errors/0/OutOfRange/QueryParameter/Maximum"_json_pointer).get()); - - + jdoc.at("/MTConnectError/Errors/0/OutOfRange/QueryParameter/Maximum"_json_pointer) + .get()); } diff --git a/test_package/routing_test.cpp b/test_package/routing_test.cpp index d4457f580..478333fd5 100644 --- a/test_package/routing_test.cpp +++ b/test_package/routing_test.cpp @@ -31,6 +31,7 @@ using namespace std; using namespace mtconnect; +using namespace mtconnect::entity; using namespace mtconnect::sink::rest_sink; using verb = boost::beast::http::verb; @@ -202,7 +203,80 @@ TEST_F(RoutingTest, TestQueryParameterError) request->m_verb = verb::get; request->m_path = "/ABC123/sample"; request->m_query = {{"count", "xxx"}}; - ASSERT_THROW(r.matches(0, request), ParameterError); + ASSERT_THROW(r.matches(0, request), RestError); +} + +TEST_F(RoutingTest, should_throw_a_rest_error_with_an_invalid_parameter) +{ + Routing r(verb::get, + "/{device}/sample?from={unsigned_integer}&" + "interval={double}&count={integer:100}&" + "heartbeat={double:10000}", + m_func); + RequestPtr request = make_shared(); + request->m_verb = verb::get; + request->m_path = "/ABC123/sample"; + request->m_query = {{"count", "xxx"}}; + try + { + r.matches(0, request); + } + catch (RestError &e) + { + auto errors = e.getErrors(); + ASSERT_EQ(1, errors.size()); + auto error = dynamic_pointer_cast(errors.front()); + ASSERT_EQ("query parameter 'count': cannot convert string 'xxx' to integer", + error->get("ErrorMessage")); + auto qp = error->get("QueryParameter"); + ASSERT_TRUE(qp); + ASSERT_EQ("count", qp->get("name")); + ASSERT_EQ("xxx", qp->get("Value")); + ASSERT_EQ("integer", qp->get("Type")); + ASSERT_EQ("int32", qp->get("Format")); + } +} + +TEST_F(RoutingTest, should_throw_a_rest_error_with_multiple_invalid_parameters) +{ + Routing r(verb::get, + "/{device}/sample?from={unsigned_integer}&" + "interval={double}&count={integer:100}&" + "heartbeat={double:10000}", + m_func); + RequestPtr request = make_shared(); + request->m_verb = verb::get; + request->m_path = "/ABC123/sample"; + request->m_query = {{"count", "xxx"}, {"heartbeat", "yyy"}}; + try + { + r.matches(0, request); + } + catch (RestError &e) + { + auto errors = e.getErrors(); + ASSERT_EQ(2, errors.size()); + auto it = errors.begin(); + auto error = dynamic_pointer_cast(*it++); + ASSERT_EQ("query parameter 'count': cannot convert string 'xxx' to integer", + error->get("ErrorMessage")); + auto qp = error->get("QueryParameter"); + ASSERT_TRUE(qp); + ASSERT_EQ("count", qp->get("name")); + ASSERT_EQ("xxx", qp->get("Value")); + ASSERT_EQ("integer", qp->get("Type")); + ASSERT_EQ("int32", qp->get("Format")); + + error = dynamic_pointer_cast(*it++); + ASSERT_EQ("query parameter 'heartbeat': cannot convert string 'yyy' to double", + error->get("ErrorMessage")); + qp = error->get("QueryParameter"); + ASSERT_TRUE(qp); + ASSERT_EQ("heartbeat", qp->get("name")); + ASSERT_EQ("yyy", qp->get("Value")); + ASSERT_EQ("double", qp->get("Type")); + ASSERT_EQ("double", qp->get("Format")); + } } TEST_F(RoutingTest, RegexPatterns) diff --git a/test_package/xml_printer_test.cpp b/test_package/xml_printer_test.cpp index 640b13e1c..8cc4ea6bf 100644 --- a/test_package/xml_printer_test.cpp +++ b/test_package/xml_printer_test.cpp @@ -108,7 +108,7 @@ ObservationPtr XmlPrinterTest::addEventToCheckpoint(Checkpoint &checkpoint, cons TEST_F(XmlPrinterTest, should_print_legacy_error) { m_printer->setSenderName("MachineXXX"); - + auto error = Error::make(Error::ErrorCode::INVALID_REQUEST, "ERROR TEXT!"); PARSE_XML(m_printer->printError(123, 9999, 1, error, true)); @@ -123,10 +123,10 @@ TEST_F(XmlPrinterTest, should_print_error_with_2_6_invalid_request) { m_printer->setSchemaVersion("2.6"); m_printer->setSenderName("MachineXXX"); - + auto error = Error::make(Error::ErrorCode::INVALID_REQUEST, "ERROR TEXT!"); PARSE_XML(m_printer->printError(123, 9999, 1, error, true)); - + ASSERT_XML_PATH_EQUAL(doc, "//m:Header@instanceId", "123"); ASSERT_XML_PATH_EQUAL(doc, "//m:Header@bufferSize", "9999"); ASSERT_XML_PATH_EQUAL(doc, "//m:Header@sender", "MachineXXX"); @@ -138,29 +138,33 @@ TEST_F(XmlPrinterTest, should_print_error_with_2_6_invalid_parameter_value) { m_printer->setSchemaVersion("2.6"); m_printer->setSenderName("MachineXXX"); - + auto error = InvalidParameterValue::make("interval", "XXX", "integer", "int64", "Bad Value"); PARSE_XML(m_printer->printError(123, 9999, 1, error, true)); - + ASSERT_XML_PATH_EQUAL(doc, "//m:Header@instanceId", "123"); ASSERT_XML_PATH_EQUAL(doc, "//m:Header@bufferSize", "9999"); ASSERT_XML_PATH_EQUAL(doc, "//m:Header@sender", "MachineXXX"); - ASSERT_XML_PATH_EQUAL(doc, "//m:Errors/m:InvalidParameterValue@errorCode", "INVALID_PARAMTER_VALUE"); + ASSERT_XML_PATH_EQUAL(doc, "//m:Errors/m:InvalidParameterValue@errorCode", + "INVALID_PARAMETER_VALUE"); ASSERT_XML_PATH_EQUAL(doc, "//m:Errors/m:InvalidParameterValue/m:ErrorMessage", "Bad Value"); - ASSERT_XML_PATH_EQUAL(doc, "//m:Errors/m:InvalidParameterValue/m:QueryParameter@name", "interval"); + ASSERT_XML_PATH_EQUAL(doc, "//m:Errors/m:InvalidParameterValue/m:QueryParameter@name", + "interval"); ASSERT_XML_PATH_EQUAL(doc, "//m:Errors/m:InvalidParameterValue/m:QueryParameter/m:Value", "XXX"); - ASSERT_XML_PATH_EQUAL(doc, "//m:Errors/m:InvalidParameterValue/m:QueryParameter/m:Type", "integer"); - ASSERT_XML_PATH_EQUAL(doc, "//m:Errors/m:InvalidParameterValue/m:QueryParameter/m:Format", "int64"); + ASSERT_XML_PATH_EQUAL(doc, "//m:Errors/m:InvalidParameterValue/m:QueryParameter/m:Type", + "integer"); + ASSERT_XML_PATH_EQUAL(doc, "//m:Errors/m:InvalidParameterValue/m:QueryParameter/m:Format", + "int64"); } TEST_F(XmlPrinterTest, should_print_error_with_2_6_out_of_range) { m_printer->setSchemaVersion("2.6"); m_printer->setSenderName("MachineXXX"); - + auto error = OutOfRange::make("from", 9999999, 10904772, 12907777, "Bad Value"); PARSE_XML(m_printer->printError(123, 9999, 1, error, true)); - + ASSERT_XML_PATH_EQUAL(doc, "//m:Header@instanceId", "123"); ASSERT_XML_PATH_EQUAL(doc, "//m:Header@bufferSize", "9999"); ASSERT_XML_PATH_EQUAL(doc, "//m:Header@sender", "MachineXXX"); @@ -172,7 +176,6 @@ TEST_F(XmlPrinterTest, should_print_error_with_2_6_out_of_range) ASSERT_XML_PATH_EQUAL(doc, "//m:Errors/m:OutOfRange/m:QueryParameter/m:Maximum", "12907777"); } - TEST_F(XmlPrinterTest, PrintProbe) { m_printer->setSenderName("MachineXXX");