From e9594ec38baf3bd76dc0e3e93a1eaad39c3805e2 Mon Sep 17 00:00:00 2001 From: "Parker, Peter G" <parkerpg@ornl.gov> Date: Wed, 20 Jun 2018 10:21:17 -0400 Subject: [PATCH] Refs #22608 - Allow arbitrary querying of the ONCat API from C++. --- Framework/CMakeLists.txt | 3 +- Framework/Catalog/CMakeLists.txt | 65 +++ .../Catalog/inc/MantidCatalog/DllConfig.h | 39 ++ .../Catalog/inc/MantidCatalog/Exception.h | 50 ++ Framework/Catalog/inc/MantidCatalog/OAuth.h | 112 +++++ Framework/Catalog/inc/MantidCatalog/ONCat.h | 167 ++++++ .../Catalog/inc/MantidCatalog/ONCatEntity.h | 121 +++++ Framework/Catalog/src/OAuth.cpp | 192 +++++++ Framework/Catalog/src/ONCat.cpp | 476 ++++++++++++++++++ Framework/Catalog/src/ONCatEntity.cpp | 251 +++++++++ Framework/Catalog/test/CMakeLists.txt | 20 + Framework/Catalog/test/OAuthTest.h | 47 ++ Framework/Catalog/test/ONCatEntityTest.h | 155 ++++++ Framework/Catalog/test/ONCatTest.h | 461 +++++++++++++++++ 14 files changed, 2158 insertions(+), 1 deletion(-) create mode 100644 Framework/Catalog/CMakeLists.txt create mode 100644 Framework/Catalog/inc/MantidCatalog/DllConfig.h create mode 100644 Framework/Catalog/inc/MantidCatalog/Exception.h create mode 100644 Framework/Catalog/inc/MantidCatalog/OAuth.h create mode 100644 Framework/Catalog/inc/MantidCatalog/ONCat.h create mode 100644 Framework/Catalog/inc/MantidCatalog/ONCatEntity.h create mode 100644 Framework/Catalog/src/OAuth.cpp create mode 100644 Framework/Catalog/src/ONCat.cpp create mode 100644 Framework/Catalog/src/ONCatEntity.cpp create mode 100644 Framework/Catalog/test/CMakeLists.txt create mode 100644 Framework/Catalog/test/OAuthTest.h create mode 100644 Framework/Catalog/test/ONCatEntityTest.h create mode 100644 Framework/Catalog/test/ONCatTest.h diff --git a/Framework/CMakeLists.txt b/Framework/CMakeLists.txt index 139521f0bfc..8eb0109da65 100644 --- a/Framework/CMakeLists.txt +++ b/Framework/CMakeLists.txt @@ -116,6 +116,7 @@ add_subdirectory (Nexus) add_subdirectory (DataHandling) add_subdirectory (Algorithms) add_subdirectory (WorkflowAlgorithms) +add_subdirectory (Catalog) add_subdirectory (CurveFitting) add_subdirectory (Crystal) add_subdirectory (ICat) @@ -145,7 +146,7 @@ add_subdirectory (ScriptRepository) set ( FRAMEWORK_LIBS Kernel HistogramData Indexing Beamline Geometry API DataObjects PythonInterface DataHandling Nexus Algorithms CurveFitting ICat - Crystal MDAlgorithms WorkflowAlgorithms + Catalog Crystal MDAlgorithms WorkflowAlgorithms LiveData RemoteAlgorithms RemoteJobManagers SINQ ) diff --git a/Framework/Catalog/CMakeLists.txt b/Framework/Catalog/CMakeLists.txt new file mode 100644 index 00000000000..3967f156b93 --- /dev/null +++ b/Framework/Catalog/CMakeLists.txt @@ -0,0 +1,65 @@ +set ( SRC_FILES + src/ONCat.cpp + src/ONCatEntity.cpp + src/OAuth.cpp +) + +set ( INC_FILES + inc/MantidCatalog/DLLConfig.h + inc/MantidCatalog/Exception.h + inc/MantidCatalog/ONCat.h + inc/MantidCatalog/ONCatEntity.h + inc/MantidCatalog/OAuth.h +) + +set ( TEST_FILES + ONCatTest.h + ONCatEntityTest.h + OAuthTest.h +) + +if (COVERALLS) + foreach( loop_var ${SRC_FILES} ${INC_FILES}) + set_property(GLOBAL APPEND PROPERTY COVERAGE_SRCS "${CMAKE_CURRENT_SOURCE_DIR}/${loop_var}") + endforeach(loop_var) +endif() + +if(UNITY_BUILD) + include(UnityBuild) + enable_unity_build(Catalog SRC_FILES SRC_UNITY_IGNORE_FILES 10) +endif(UNITY_BUILD) + +# Add ssl dependency +include_directories ( ${OPENSSL_INCLUDE_DIR} ) +add_definitions ( -DWITH_OPENSSL -DWITH_NONAMESPACES ) + +# Add a precompiled header where they are supported +enable_precompiled_headers( inc/MantidCatalog/PrecompiledHeader.h SRC_FILES ) +# Add the target for this directory +add_library ( Catalog ${SRC_FILES} ${INC_FILES}) +# Set the name of the generated library +set_target_properties ( Catalog PROPERTIES OUTPUT_NAME MantidCatalog + COMPILE_DEFINITIONS IN_MANTID_CATALOG +) + +if (OSX_VERSION VERSION_GREATER 10.8) + set_target_properties(Catalog PROPERTIES INSTALL_RPATH "@loader_path/../Contents/MacOS") +elseif ( ${CMAKE_SYSTEM_NAME} STREQUAL "Linux" ) + set_target_properties(Catalog PROPERTIES INSTALL_RPATH "\$ORIGIN/../${LIB_DIR}") +endif () + +# Add to the 'Framework' group in VS +set_property ( TARGET Catalog PROPERTY FOLDER "MantidFramework" ) + +include_directories ( inc ) + +target_link_libraries ( Catalog LINK_PRIVATE ${TCMALLOC_LIBRARIES_LINKTIME} ${MANTIDLIBS} ${OPENSSL_LIBRARIES} ${JSONCPP_LIBRARIES}) + +# Add the unit tests directory +add_subdirectory ( test ) + +########################################################################### +# Installation settings +########################################################################### + +install ( TARGETS Catalog ${SYSTEM_PACKAGE_TARGET} DESTINATION ${PLUGINS_DIR} ) diff --git a/Framework/Catalog/inc/MantidCatalog/DllConfig.h b/Framework/Catalog/inc/MantidCatalog/DllConfig.h new file mode 100644 index 00000000000..2dd7225ace2 --- /dev/null +++ b/Framework/Catalog/inc/MantidCatalog/DllConfig.h @@ -0,0 +1,39 @@ +#ifndef MANTID_CATALOG_DLLCONFIG_H_ +#define MANTID_CATALOG_DLLCONFIG_H_ + +/* + This file contains the DLLExport/DLLImport linkage configuration for the + Catalog library + + Copyright © 2018 ISIS Rutherford Appleton Laboratory, NScD Oak Ridge + National Laboratory & European Spallation Source + + This file is part of Mantid. + + Mantid is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 3 of the License, or + (at your option) any later version. + + Mantid is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. + + File change history is stored at: <https://github.com/mantidproject/mantid>. + Code Documentation is available at: <http://doxygen.mantidproject.org> +*/ +#include "MantidKernel/System.h" + +#ifdef IN_MANTID_CATALOG +#define MANTID_CATALOG_DLL DLLExport +#define EXTERN_MANTID_CATALOG +#else +#define MANTID_CATALOG_DLL DLLImport +#define EXTERN_MANTID_CATALOG EXTERN_IMPORT +#endif /* IN_MANTID_CATALOG */ + +#endif // MANTID_CATALOG_DLLCONFIG_H_ diff --git a/Framework/Catalog/inc/MantidCatalog/Exception.h b/Framework/Catalog/inc/MantidCatalog/Exception.h new file mode 100644 index 00000000000..1814c4deee8 --- /dev/null +++ b/Framework/Catalog/inc/MantidCatalog/Exception.h @@ -0,0 +1,50 @@ +#ifndef MANTID_CATALOG_EXCEPTION_H_ +#define MANTID_CATALOG_EXCEPTION_H_ + +#include <stdexcept> + +namespace Mantid { +namespace Catalog { +namespace Exception { + +class CatalogError : public std::runtime_error { +public: + explicit CatalogError(const std::string & message) : + std::runtime_error(message) {} +}; + +class InvalidCredentialsError final : public CatalogError { +public: + explicit InvalidCredentialsError(const std::string & message) : + CatalogError(message) {} +}; + +class TokenRejectedError final : public CatalogError { +public: + explicit TokenRejectedError(const std::string & message) : + CatalogError(message) {} +}; + +class TokenParsingError final : public CatalogError { +public: + explicit TokenParsingError(const std::string & message) : + CatalogError(message) {} +}; + +class InvalidRefreshTokenError final : public CatalogError { +public: + explicit InvalidRefreshTokenError(const std::string & message) : + CatalogError(message) {} +}; + +class MalformedRepresentationError final : public CatalogError { +public: + explicit MalformedRepresentationError(const std::string & message) : + CatalogError(message) {} +}; + +} // namespace Exception +} // namespace Catalog +} // namespace Mantid + +#endif // MANTID_CATALOG_EXCEPTION_H_ diff --git a/Framework/Catalog/inc/MantidCatalog/OAuth.h b/Framework/Catalog/inc/MantidCatalog/OAuth.h new file mode 100644 index 00000000000..d3182a5e82e --- /dev/null +++ b/Framework/Catalog/inc/MantidCatalog/OAuth.h @@ -0,0 +1,112 @@ +#ifndef MANTID_CATALOG_OAUTH_H_ +#define MANTID_CATALOG_OAUTH_H_ + +#include "MantidCatalog/DllConfig.h" +#include "MantidKernel/make_unique.h" +#include "MantidKernel/DateAndTime.h" + +#include <boost/optional.hpp> + +namespace Mantid { +namespace Catalog { +namespace OAuth { + +using Types::Core::DateAndTime; + +/** + Classes providing basic Client Credentials / Resource Owner Credentials + OAuth functionality. + + To be used by other cataloging classes and so it should not be necessary + to use this directly anywhere else. + + @author Peter Parker + @date 2018 + + Copyright © 2017 ISIS Rutherford Appleton Laboratory, NScD Oak Ridge + National Laboratory & European Spallation Source + + This file is part of Mantid. + + Mantid is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 3 of the License, or + (at your option) any later version. + + Mantid is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. + + File change history is stored at: <https://github.com/mantidproject/mantid> + Code Documentation is available at: <http://doxygen.mantidproject.org> +*/ + +enum class OAuthFlow : uint8_t { + CLIENT_CREDENTIALS, + RESOURCE_OWNER_CREDENTIALS +}; + +class MANTID_CATALOG_DLL OAuthToken { +public: + OAuthToken() = delete; + OAuthToken( + const std::string & tokenType, + int expiresIn, + const std::string & accessToken, + const std::string & scope, + const boost::optional<std::string> & refreshToken + ); + ~OAuthToken(); + + std::string tokenType() const; + int expiresIn() const; + std::string accessToken() const; + std::string scope() const; + boost::optional<std::string> refreshToken() const; + + bool isExpired() const; + bool isExpired(const DateAndTime & currentTime) const; + + static OAuthToken fromJSONStream(std::istream & tokenStringStream); + +private: + DateAndTime m_expiresAt; + + std::string m_tokenType; + int m_expiresIn; + std::string m_accessToken; + std::string m_scope; + boost::optional<std::string> m_refreshToken; +}; + +class MANTID_CATALOG_DLL IOAuthTokenStore { +public: + virtual void setToken(const boost::optional<OAuthToken> & token) = 0; + virtual boost::optional<OAuthToken> getToken() = 0; +}; + +class MANTID_CATALOG_DLL ConfigServiceTokenStore : public IOAuthTokenStore { +public: + ConfigServiceTokenStore() = default; + ConfigServiceTokenStore & operator=( + const ConfigServiceTokenStore& other + ) = default; + ~ConfigServiceTokenStore(); + + void setToken(const boost::optional<OAuthToken> & token) override; + boost::optional<OAuthToken> getToken() override; +}; + +using IOAuthTokenStore_uptr = std::unique_ptr<IOAuthTokenStore>; +using IOAuthTokenStore_sptr = std::shared_ptr<IOAuthTokenStore>; +using ConfigServiceTokenStore_uptr = std::unique_ptr<ConfigServiceTokenStore>; + +} // namespace OAuth +} // namespace Catalog +} // namespace Mantid + +#endif /* MANTID_CATALOG_OAUTH_H_ */ diff --git a/Framework/Catalog/inc/MantidCatalog/ONCat.h b/Framework/Catalog/inc/MantidCatalog/ONCat.h new file mode 100644 index 00000000000..33a99002ef7 --- /dev/null +++ b/Framework/Catalog/inc/MantidCatalog/ONCat.h @@ -0,0 +1,167 @@ +#ifndef MANTID_CATALOG_ONCAT_H_ +#define MANTID_CATALOG_ONCAT_H_ + +#include "MantidCatalog/DllConfig.h" +#include "MantidCatalog/OAuth.h" +#include "MantidCatalog/ONCatEntity.h" +#include "MantidKernel/DateAndTime.h" +#include "MantidKernel/make_unique.h" + +#include <vector> + +#include <boost/optional.hpp> + +namespace Mantid { + +namespace Kernel { +class InternetHelper; +} + +namespace Catalog { +namespace ONCat { + +using Mantid::Catalog::OAuth::OAuthFlow; +using Mantid::Catalog::OAuth::IOAuthTokenStore_uptr; +using Mantid::Catalog::OAuth::IOAuthTokenStore_sptr; +using Types::Core::DateAndTime; + +// Here we use a vector of pairs rather than a map because we would like +// the ability to set a parameter with a given name more than once -- +// this denotes an arrayed parameter. +using QueryParameter = std::pair<std::string, std::string>; +using QueryParameters = std::vector<QueryParameter>; + +/** + The main class to be used when interacting with ONCat from C++. It can + be used to retrieve "entities" from REST-like "resources". Please refer + to the API documentation at https://oncat.ornl.gov/#/build for more + information about each resource. + + Rather than use constructors, the helper method ONCat::fromMantidSettings() + is strongly recommended. This will create an ONCat object taking into + account the authentication settings configured in a given instance of + Mantid. + + Creation of an ONCat object can be done as follows: + + auto oncat = ONCat::fromMantidSettings(); + + Once you have that, logging in either assumes that a client ID and + client secret have been added to the Mantid.local.properties file (this + essentially allows machine-to-machine authentication for a use case like + auto-reduction, and there is no explicit "login" step), or that a user + is somehow prompted for their ORNL XCAMS / UCAMS username and password, + or that you will only be accessing unauthenticated resources. If an + explicit login step is necessary it should look something like this: + + oncat.login("some_user", "a_password"); + + From then on, basic usage is as follows: + + // Get a list of the experiments for NOMAD, specifying the fields + // we are interested in as a "projection". + const auto nomadExperiments = oncat.list("api", "experiments", { + QueryParameter("facility", "SNS"), + QueryParameter("instrument", "NOM"), + QueryParameter("projection", "name"), + QueryParameter("projection", "size") + }); + + // Print out the IPTS numbers of each one. + for (const auto & experiment : nomadExperiments) { + std::cout + << *experiment.asString("name") << " has " + << *experiment.asInt("size") << " ingested datafiles."; + } + + For logged in users, no further credential prompting should be required + as part of the standard workflow, although you should be prepared for + an authenticated user to have their tokens invalidated *eventually*. + At that point, any call to the API will fail, and an error will be written + to the log asking them to login again. + + @author Peter Parker + @date 2018 + + Copyright © 2017 ISIS Rutherford Appleton Laboratory, NScD Oak Ridge + National Laboratory & European Spallation Source + + This file is part of Mantid. + + Mantid is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 3 of the License, or + (at your option) any later version. + + Mantid is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. + + File change history is stored at: <https://github.com/mantidproject/mantid> + Code Documentation is available at: <http://doxygen.mantidproject.org> +*/ +class MANTID_CATALOG_DLL ONCat { +public: + static ONCat fromMantidSettings(); + + ONCat() = delete; + ONCat(const ONCat & other); + ~ONCat(); + + bool isUserLoggedIn() const; + + void login(const std::string & username, const std::string & password); + void logout(); + + ONCatEntity retrieve( + const std::string & resourceNamespace, + const std::string & resource, + const std::string & identifier, + const QueryParameters & queryParameters); + std::vector<ONCatEntity> list( + const std::string & resourceNamespace, + const std::string & resource, + const QueryParameters & queryParameters); + + ////////////////////////////////////////////////////////////////////// + // Exposed publicly for testing purposes only. + ////////////////////////////////////////////////////////////////////// + ONCat( + const std::string & url, + IOAuthTokenStore_uptr tokenStore, + OAuthFlow flow, + const std::string & clientId, + const boost::optional<std::string> & clientSecret = boost::none + ); + void refreshTokenIfNeeded(); + void refreshTokenIfNeeded(const DateAndTime & currentTime); + void setInternetHelper( + std::unique_ptr<Mantid::Kernel::InternetHelper> internetHelper + ); + ////////////////////////////////////////////////////////////////////// + +private: + void sendAPIRequest( + const std::string & uri, + const QueryParameters & queryParameters, + std::ostream & response + ); + + std::string m_url; + IOAuthTokenStore_sptr m_tokenStore; + std::string m_clientId; + boost::optional<std::string> m_clientSecret; + + OAuthFlow m_flow; + std::unique_ptr<Mantid::Kernel::InternetHelper> m_internetHelper; +}; + +} // namespace ONCat +} // namespace Catalog +} // namespace Mantid + +#endif /* MANTID_CATALOG_ONCAT_H_ */ diff --git a/Framework/Catalog/inc/MantidCatalog/ONCatEntity.h b/Framework/Catalog/inc/MantidCatalog/ONCatEntity.h new file mode 100644 index 00000000000..aa60ceae493 --- /dev/null +++ b/Framework/Catalog/inc/MantidCatalog/ONCatEntity.h @@ -0,0 +1,121 @@ +#ifndef MANTID_CATALOG_ONCATENTITY_H_ +#define MANTID_CATALOG_ONCATENTITY_H_ + +#include "MantidCatalog/DllConfig.h" +#include "MantidKernel/make_unique.h" + +#include <vector> + +#include <boost/optional.hpp> + +namespace Json { + class Value; +} + +namespace Mantid { +namespace Catalog { +namespace ONCat { + +using Content = Json::Value; +using Content_uptr = std::unique_ptr<Content>; + +/** + A class to encapsulate the "entity" responses received from the ONCat API. + + An ONCatEntity object (or a vector of objects) can be constructed when + given an istream, which is assumed to contain JSON information as defined + in the API documentation at https://oncat.ornl.gov/#/build. + + Note that there are only two fields shared across all API entity types: + "id" and "type". Further, all other fields can be optionally disabled + through the use of "projections", and a certain subset of fields may even + be completely missing for a given file because of the dynamic nature of + metadata resulting from Data Acquisition software changes. + + For this reason, all other metadata will be retrieved in a way that + forces you to deal with the case where the field in question is not there. + There are two ways of doing this: the first is to specify a default value + to be used when a value is not present, and the second is to check for a + result on a boost::optional. + + However, if your projection is such that you *know* a field will be present + (note that most fields on API resources will always be returned as long + as they are requested as part of a projection, for example the "location" + field of the Datafile resource), then feel free to assume it will be + there and resolve the boost::optional without checking for a result. + + @author Peter Parker + @date 2018 + + Copyright © 2017 ISIS Rutherford Appleton Laboratory, NScD Oak Ridge + National Laboratory & European Spallation Source + + This file is part of Mantid. + + Mantid is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 3 of the License, or + (at your option) any later version. + + Mantid is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. + + File change history is stored at: <https://github.com/mantidproject/mantid> + Code Documentation is available at: <http://doxygen.mantidproject.org> +*/ +class MANTID_CATALOG_DLL ONCatEntity { +public: + ONCatEntity() = delete; + ONCatEntity(const ONCatEntity &); + ~ONCatEntity(); + + // These are the only two fields the ONCat API guarantees will be + // there across *all* entity types. + std::string id() const; + std::string type() const; + + // So, you can either supply a default value for when the field you + // want is not there ... + std::string asString( + const std::string & path, const std::string defaultValue) const; + int asInt(const std::string & path, int defaultValue) const; + float asFloat(const std::string & path, float defaultValue) const; + double asDouble(const std::string & path, double defaultValue) const; + bool asBool(const std::string & path, bool defaultValue) const; + + // ... or, write conditional logic around boost's optional results. + boost::optional<std::string> asString(const std::string & path) const; + boost::optional<int> asInt(const std::string & path) const; + boost::optional<float> asFloat(const std::string & path) const; + boost::optional<double> asDouble(const std::string & path) const; + boost::optional<bool> asBool(const std::string & path) const; + + std::string toString() const; + + static ONCatEntity fromJSONStream( + std::istream & streamContent); + static std::vector<ONCatEntity> vectorFromJSONStream( + std::istream & streamContent); + +private: + ONCatEntity( + const std::string & id, + const std::string & type, + Content_uptr content + ); + + std::string m_id; + std::string m_type; + Content_uptr m_content; +}; + +} // namespace ONCat +} // namespace Catalog +} // namespace Mantid + +#endif /* MANTID_CATALOG_ONCATENTITY_H_ */ diff --git a/Framework/Catalog/src/OAuth.cpp b/Framework/Catalog/src/OAuth.cpp new file mode 100644 index 00000000000..8adfd8e8258 --- /dev/null +++ b/Framework/Catalog/src/OAuth.cpp @@ -0,0 +1,192 @@ +#include "MantidCatalog/OAuth.h" +#include "MantidCatalog/Exception.h" +#include "MantidKernel/ConfigService.h" + +#include <sstream> + +#include <Poco/Net/HTMLForm.h> +#include <Poco/Net/HTTPResponse.h> + +#include <json/json.h> + +namespace Mantid { +namespace Catalog { +namespace OAuth { + +using Mantid::Catalog::Exception::TokenParsingError; + +//---------------------------------------------------------------------- +// OAuthToken +//---------------------------------------------------------------------- + +OAuthToken::OAuthToken( + const std::string & tokenType, + int expiresIn, + const std::string & accessToken, + const std::string & scope, + const boost::optional<std::string> & refreshToken +) : + m_expiresAt(DateAndTime::getCurrentTime() + static_cast<double>(expiresIn)), + m_tokenType(tokenType), + m_expiresIn(expiresIn), + m_accessToken(accessToken), + m_scope(scope), + m_refreshToken(refreshToken) {} + +OAuthToken::~OAuthToken() {} + +OAuthToken OAuthToken::fromJSONStream( + std::istream & tokenStringStream +) { + try { + Json::Value full_token; + tokenStringStream >> full_token; + + const auto tokenType = full_token["token_type"].asString(); + const auto expiresIn = + static_cast<unsigned int>(full_token["expires_in"].asUInt()); + const auto accessToken = full_token["access_token"].asString(); + const auto scope = full_token["scope"].asString(); + + const auto parsedRefreshToken = full_token["refresh_token"].asString(); + const boost::optional<std::string> refreshToken = + parsedRefreshToken == "" ? + boost::none : + boost::optional<std::string>(parsedRefreshToken); + + return OAuthToken( + tokenType, + expiresIn, + accessToken, + scope, + refreshToken + ); + } catch (...) { + throw TokenParsingError( + "Unable to parse authentication token!" + ); + } +} + +bool OAuthToken::isExpired() const { + return isExpired(DateAndTime::getCurrentTime()); +} + +bool OAuthToken::isExpired(const DateAndTime & currentTime) const { + return currentTime > m_expiresAt; +} + +std::string OAuthToken::tokenType() const { + return m_tokenType; +} + +int OAuthToken::expiresIn() const { + return m_expiresIn; +} + +std::string OAuthToken::accessToken() const { + return m_accessToken; +} + +std::string OAuthToken::scope() const { + return m_scope; +} + +boost::optional<std::string> OAuthToken::refreshToken() const { + return m_refreshToken; +} + +//---------------------------------------------------------------------- +// ConfigServiceTokenStore +//---------------------------------------------------------------------- + +namespace { + static const std::string CONFIG_PATH_BASE = "catalog.oncat.token."; +} + +ConfigServiceTokenStore::~ConfigServiceTokenStore() { + try { + // Here we attempt to persist our OAuth token to disk before the + // up-until-now only-in-memory token store is destroyed. + // + // I don't believe this is a great solution. Some things to + // consider: + // + // * We have to save the *entire* contents of the config. This may + // not be desirable. + // * Ideally we would persist on every token set. + auto & config = Mantid::Kernel::ConfigService::Instance(); + config.saveConfig(config.getUserFilename()); + } catch (...) { + // It's not the end of the world if there was an error persisting + // the token (the worst that could happen is a user has to login + // again), but it *is* the end of the world if we seg fault. + } +} + +void ConfigServiceTokenStore::setToken( + const boost::optional<OAuthToken> & token +) { + auto & config = Mantid::Kernel::ConfigService::Instance(); + + if (token) { + config.setString(CONFIG_PATH_BASE + "tokenType", token->tokenType()); + config.setString( + CONFIG_PATH_BASE + "expiresIn", + std::to_string(token->expiresIn()) + ); + config.setString(CONFIG_PATH_BASE + "accessToken", token->accessToken()); + config.setString(CONFIG_PATH_BASE + "scope", token->scope()); + config.setString( + CONFIG_PATH_BASE + "refreshToken", + token->refreshToken() ? *token->refreshToken() : std::string("") + ); + } else { + config.setString(CONFIG_PATH_BASE + "tokenType", ""); + config.setString(CONFIG_PATH_BASE + "expiresIn", ""); + config.setString(CONFIG_PATH_BASE + "accessToken", ""); + config.setString(CONFIG_PATH_BASE + "scope", ""); + config.setString(CONFIG_PATH_BASE + "refreshToken", ""); + } +} + +boost::optional<OAuthToken> ConfigServiceTokenStore::getToken() { + auto & config = Mantid::Kernel::ConfigService::Instance(); + + const auto tokenType = config.getString(CONFIG_PATH_BASE + "tokenType"); + const auto expiresIn = config.getString(CONFIG_PATH_BASE + "expiresIn"); + const auto accessToken = config.getString( + CONFIG_PATH_BASE + "accessToken" + ); + const auto scope = config.getString(CONFIG_PATH_BASE + "scope"); + const auto refreshToken = config.getString( + CONFIG_PATH_BASE + "refreshToken" + ); + + // A partially written-out token is useless and is therefore + // effectively the same as a token not having been written out at + // all. So, it's all or nothing (excluding the refresh token of + // course, which is not present for all OAuth flows). + if (tokenType == "" || expiresIn == "" || accessToken == "" || scope == "") { + return boost::none; + } + + try { + return boost::make_optional(OAuthToken( + tokenType, + std::stoi(expiresIn), + accessToken, + scope, + boost::make_optional(refreshToken != "", refreshToken) + )); + } catch (std::invalid_argument &) { + // Catching any std::stoi failures silently -- a malformed token is + // useless and may as well not be there. + } + + return boost::none; +} + +} // namespace OAuth +} // namespace Catalog +} // namespace Mantid diff --git a/Framework/Catalog/src/ONCat.cpp b/Framework/Catalog/src/ONCat.cpp new file mode 100644 index 00000000000..c0c02839f8f --- /dev/null +++ b/Framework/Catalog/src/ONCat.cpp @@ -0,0 +1,476 @@ +#include "MantidCatalog/ONCat.h" +#include "MantidCatalog/OAuth.h" +#include "MantidCatalog/Exception.h" +#include "MantidKernel/ConfigService.h" +#include "MantidKernel/Exception.h" +#include "MantidKernel/InternetHelper.h" +#include "MantidKernel/Logger.h" +#include "MantidKernel/make_unique.h" + +#include <algorithm> +#include <iostream> +#include <sstream> + +#include <boost/algorithm/string/join.hpp> + +#include <Poco/Net/HTMLForm.h> +#include <Poco/Net/HTTPResponse.h> + +namespace Mantid { +namespace Catalog { +namespace ONCat { + +using Poco::Net::HTTPResponse; + +using Mantid::Catalog::OAuth::ConfigServiceTokenStore; +using Mantid::Catalog::OAuth::OAuthToken; +using Mantid::Catalog::ONCat::ONCatEntity; +using Mantid::Catalog::Exception::CatalogError; +using Mantid::Catalog::Exception::InvalidCredentialsError; +using Mantid::Catalog::Exception::InvalidRefreshTokenError; +using Mantid::Catalog::Exception::TokenRejectedError; +using Mantid::Kernel::Exception::InternetError; + +namespace { +Mantid::Kernel::Logger g_log("ONCat"); + +static const std::string CONFIG_PATH_BASE = "catalog.oncat."; +// It could be argued that this should be read in from Facilities.xml or +// similar, but I will put this off for now as it is unclear how to +// reconcile ONCat's functionality with the current <soapendpoint> / +// <externaldownload> tags in the XML. +static const std::string DEFAULT_ONCAT_URL = "https://oncat.ornl.gov"; +static const std::string DEFAULT_CLIENT_ID = + "d16ea847-41ce-4b30-9167-40298588e755"; +} + +/** + * Constructs an ONCat object based on various settings gathered from + * the ConfigService. + * + * The resulting object will work with resources that require authorization + * in one of two possible modes: + * + * User Login Mode (Default) + * ------------------------- + * + * Users must log in with their UCAMS / XCAMS credentials before calls + * to the ONCat API can be made. This mode should work "out of the box" + * (requires no changes to config files), and is the default mode of + * operation. User access to API information is governed by the same + * LDAP instance that controls file system access, so users should only + * see the experiment data they are allowed to see. + * + * This mode uses the "Resource Owner Credentials" OAuth flow. + * + * Machine-to-Machine Mode + * ----------------------- + * + * No user login is necessary, but for it to be enabled a client ID and + * secret must exist in the ConfigService. Recommended practice would + * be to add the following two entries to the Mantid.local.properties + * file on the machine to be given access, using the credentials issued + * by the ONCat administrator for the values: + * + * catalog.oncat.client_id = "[CLIENT ID]" + * catalog.oncat.client_secret = "[CLIENT SECRET]" + * + * API read access is completely unrestricted in this mode, which is + * intended for autoreduction use cases or similar. + * + * This mode uses the "Client Credentials" OAuth flow. + * + * @return The constructed ONCat object. + */ +ONCat ONCat::fromMantidSettings() { + auto & config = Mantid::Kernel::ConfigService::Instance(); + const auto client_id = config.getString(CONFIG_PATH_BASE + "client_id"); + const auto client_secret = + config.getString(CONFIG_PATH_BASE + "client_secret"); + const bool hasClientCredentials = client_id != "" && client_secret != ""; + + if (hasClientCredentials) { + g_log.debug() + << "Found client credentials in Mantid.local.properties. " + << "No user login required." + << std::endl; + } else { + g_log.debug() + << "Could not find client credentials in Mantid.local.properties. " + << "Falling back to default -- user login required." + << std::endl; + } + + return ONCat( + DEFAULT_ONCAT_URL, + Mantid::Kernel::make_unique<ConfigServiceTokenStore>(), + hasClientCredentials + ? OAuthFlow::CLIENT_CREDENTIALS + : OAuthFlow::RESOURCE_OWNER_CREDENTIALS, + hasClientCredentials ? client_id : DEFAULT_CLIENT_ID, + boost::make_optional(hasClientCredentials, client_secret) + ); +} + +ONCat::ONCat( + const std::string & url, + IOAuthTokenStore_uptr tokenStore, + OAuthFlow flow, + const std::string & clientId, + const boost::optional<std::string> & clientSecret +) : + m_url(url), + m_tokenStore(std::move(tokenStore)), + m_clientId(clientId), + m_clientSecret(clientSecret), + + m_flow(flow), + m_internetHelper(new Mantid::Kernel::InternetHelper()) {} + +ONCat::ONCat(const ONCat & other) : + m_url(other.m_url), + m_tokenStore(other.m_tokenStore), + m_clientId(other.m_clientId), + m_clientSecret(other.m_clientSecret), + + m_flow(other.m_flow), + m_internetHelper(new Mantid::Kernel::InternetHelper()) {} + +ONCat::~ONCat() {} + +/** + * Whether or not a user is currently logged in. (Not relevant when + * using machine-to-machine authentication as part of the Client + * Credentials flow, and not required when accessing unauthenticated + * parts of the API.) + * + * Something to bear in mind is that the term "logged in" is used quite + * loosely here. In an OAuth context it roughly equates to, "there is a + * token stored locally", which is not quite the same thing. This may + * sound strange, but consider the following: + * + * - Tokens expire after a given amount of time, at which point they can + * be "refreshed". A successful token refresh happens behind the + * scenes without the user even knowing it took place. + * + * - While it is possible to tell when a token needs to be refreshed, + * token refreshes are not always successful. If they fail then the + * client must prompt the user to enter their credentials again. + * + * - There is no way for the client to know whether or not the refresh + * will be successful ahead of time (i.e., whether a token has been + * revoked server-side), since the OAuth spec provides no mechanism to + * check the validity of a refresh token. + * + * - Tokens can be revoked at any time with absolutely no notice as part + * of standard OAuth practice. Also, only a limited number of tokens + * can exist for each unique client / user combination at any one + * time. + * + * Hopefully it is clear that working with OAuth client-side requires + * you to use an almost-Pythonic "ask for forgiveness rather than for + * permission" strategy -- i.e., code as if locally-stored tokens can be + * refreshed, but be ready to prompt the user for their credentials if + * the refresh fails. + * + * Some useful links with related information: + * + * - http://qr.ae/TUTke2 (quora.com) + * - https://stackoverflow.com/a/30826806/778572 + * + * @param true if a user is "logged in", else false. + */ +bool ONCat::isUserLoggedIn() const { + if (m_flow == OAuthFlow::CLIENT_CREDENTIALS) { + // No users are ever authenticated as part of this flow. + return false; + } + + return m_tokenStore->getToken(); +} + +void ONCat::logout() { + // Currently, ONCat OAuth does *not* allow clients to revoke tokens + // that are no longer needed (though this is defined in the OAtuh + // spec). A "logout", then, is simply throwing away whatever token we + // previously stored client-side. + m_tokenStore->setToken(boost::none); + g_log.debug() << "Logging out." << std::endl;; +} + +/** + * Log in as part of the Resource Ownder Credentials flow so that + * authenticated resources may be accessed on behalf of a user. + * + * @param username : The XCAMS / UCAMS ID of the user. + * @param password : The XCAMS / UCAMS password of the user. + * + * @exception Mantid::Catalog::Exception::InvalidCredentialsError : + * Thrown when the given credentials are not valid. + */ +void ONCat::login( + const std::string & username, const std::string & password +) { + if (m_flow != OAuthFlow::RESOURCE_OWNER_CREDENTIALS) { + g_log.warning() + << "Unexpected usage detected! " + << "Logging in with user credentials in not required (and is not " + << "supported) when machine-to-machine credentials are being used." + << std::endl;; + return; + } + + Poco::Net::HTMLForm form(Poco::Net::HTMLForm::ENCODING_MULTIPART); + form.set("username", username); + form.set("password", password); + form.set("client_id", m_clientId); + if (m_clientSecret) { + form.set("client_secret", m_clientSecret.get()); + } + form.set("grant_type", "password"); + + m_internetHelper->setBody(form); + + try { + std::stringstream ss; + + const int statusCode = m_internetHelper->sendRequest( + m_url + "/oauth/token", ss + ); + + if (statusCode == HTTPResponse::HTTP_OK) { + m_tokenStore->setToken(OAuthToken::fromJSONStream(ss)); + } + + g_log.debug() << "Login was successful!" << std::endl;; + } catch (InternetError & ie) { + if (ie.errorCode() == HTTPResponse::HTTP_UNAUTHORIZED) { + throw InvalidCredentialsError( + "Invalid UCAMS / XCAMS credentials used for ONCat login." + ); + } + throw CatalogError(ie.what()); + } +} + +/** + * Retrieve a single entity from the given resource (in the given namespace) of + * ONCat's API. + * + * Please see https://oncat.ornl.gov/#/build for more information about the + * currently-available resources, and what query parameters they allow. + * + * @param identifier : + * The ID or name that uniquely identifies the entity. + * @param resourceNamespace : + * The "namespace" of the resource. The most common, "core" resources all + * belong to the "api" namespace. + * @param resource : + * The name of the resource to retrieve the entity from. I.e., "Datafile" + * entities can be retrieved from the "datafiles" resource. + * @param queryParameters : + * The name-value-pair query-string parameters. + * + * @return The response from the API in the form on an ONCatEntity object. + * + * @exception Mantid::Catalog::Exception::CatalogError + */ +ONCatEntity ONCat::retrieve( + const std::string & resourceNamespace, + const std::string & resource, + const std::string & identifier, + const QueryParameters & queryParameters +) { + const auto uri = + m_url + "/" + resourceNamespace + "/" + resource + "/" + identifier; + std::stringstream ss; + + sendAPIRequest(uri, queryParameters, ss); + + return ONCatEntity::fromJSONStream(ss); +} + +/** + * Retrieve a collection of entities from the given resource (in the given + * namespace) of ONCat's API. + * + * Please see retrieve documentation for more info. + */ +std::vector<ONCatEntity> ONCat::list( + const std::string & resourceNamespace, + const std::string & resource, + const QueryParameters & queryParameters +) { + const auto uri = + m_url + "/" + resourceNamespace + "/" + resource; + std::stringstream ss; + + sendAPIRequest(uri, queryParameters, ss); + + return ONCatEntity::vectorFromJSONStream(ss); +} + +/** + * Refresh the current token if it has expired (and if it actually exists). + * + * To be called behind-the-scenes before each API query, so that we know + * our tokens are up-to-date before being used. + * + * @exception Mantid::Catalog::Exception::InvalidCredentialsError : + * Thrown when the provider decides the current token cannot be refreshed. + */ +void ONCat::refreshTokenIfNeeded() { + refreshTokenIfNeeded(DateAndTime::getCurrentTime()); +} + +/** + * See overloaded method. + * + * @param currentTime : Used in testing to specify a different time. + * + * @exception Mantid::Catalog::Exception::InvalidRefreshTokenError : + * Thrown when the provider decides the current token cannot be refreshed. + */ +void ONCat::refreshTokenIfNeeded(const DateAndTime & currentTime) { + const auto currentToken = m_tokenStore->getToken(); + + if (m_flow == OAuthFlow::CLIENT_CREDENTIALS) { + if (currentToken && !currentToken->isExpired(currentTime)) { return; } + + Poco::Net::HTMLForm form(Poco::Net::HTMLForm::ENCODING_MULTIPART); + form.set("client_id", m_clientId); + if (m_clientSecret) { + form.set("client_secret", m_clientSecret.get()); + } + form.set("grant_type", "client_credentials"); + + m_internetHelper->reset(); + m_internetHelper->setBody(form); + + try { + std::stringstream ss; + + const int statusCode = m_internetHelper->sendRequest( + m_url + "/oauth/token", ss + ); + + if (statusCode == HTTPResponse::HTTP_OK) { + m_tokenStore->setToken(OAuthToken::fromJSONStream(ss)); + } + g_log.debug() << "Token successfully refreshed." << std::endl; + } catch (InternetError & ie) { + throw CatalogError(ie.what()); + } + } else if (m_flow == OAuthFlow::RESOURCE_OWNER_CREDENTIALS) { + if (!currentToken) { return; } + if (!currentToken->isExpired(currentTime)) { return; } + const auto currentRefreshToken = currentToken->refreshToken(); + if (!currentRefreshToken) { return; } + + Poco::Net::HTMLForm form(Poco::Net::HTMLForm::ENCODING_MULTIPART); + form.set("client_id", m_clientId); + if (m_clientSecret) { + form.set("client_secret", m_clientSecret.get()); + } + form.set("grant_type", "refresh_token"); + form.set("refresh_token", currentRefreshToken.get()); + + m_internetHelper->reset(); + m_internetHelper->setBody(form); + + try { + std::stringstream ss; + + const int statusCode = m_internetHelper->sendRequest( + m_url + "/oauth/token", ss + ); + + if (statusCode == HTTPResponse::HTTP_OK) { + m_tokenStore->setToken(OAuthToken::fromJSONStream(ss)); + } + g_log.debug() << "Token successfully refreshed." << std::endl; + } catch (InternetError & ie) { + if (ie.errorCode() == HTTPResponse::HTTP_UNAUTHORIZED) { + // As per OAuth spec, when a refresh token is no longer valid, we + // can consider ourselves logged out. + logout(); + throw InvalidRefreshTokenError( + "You have been logged out. Please login again." + ); + } + throw CatalogError(ie.what()); + } + } +} + +void ONCat::setInternetHelper( + std::unique_ptr<Mantid::Kernel::InternetHelper> internetHelper +) { + m_internetHelper = std::move(internetHelper); +} + +void ONCat::sendAPIRequest( + const std::string & uri, + const QueryParameters & queryParameters, + std::ostream & response +) { + refreshTokenIfNeeded(); + + const auto tokenType = m_tokenStore->getToken()->tokenType(); + const auto accessToken = m_tokenStore->getToken()->accessToken(); + + m_internetHelper->clearHeaders(); + m_internetHelper->setMethod("GET"); + m_internetHelper->addHeader( + "Authorization", tokenType + " " + accessToken + ); + + std::vector<std::string> queryStringParts(queryParameters.size()); + std::transform( + queryParameters.begin(), queryParameters.end(), + queryStringParts.begin(), + [](const QueryParameter & queryParameter) -> std::string { + return queryParameter.first + "=" + queryParameter.second; + } + ); + const auto queryString = boost::algorithm::join(queryStringParts, "&"); + const auto url = queryString.size() == 0 ? uri : uri + "?" + queryString; + + g_log.debug() + << "About to make a call to the following ONCat URL: " << url; + + try { + m_internetHelper->sendRequest(url, response); + } catch (InternetError & ie) { + if ( + ie.errorCode() == HTTPResponse::HTTP_UNAUTHORIZED + ) { + std::string errorMessage; + switch(m_flow) { + case OAuthFlow::RESOURCE_OWNER_CREDENTIALS : + errorMessage = "You have been logged out. Please login again."; + break; + case OAuthFlow::CLIENT_CREDENTIALS : + errorMessage = + "The stored OAuth token appears to be invalid. " + "There are a few cases where this might be expected, but in " + "principle this should rarely happen. " + "Please try again and if the problem persists contact the " + "ONCat administrator at oncat-support@ornl.gov."; + break; + } + // The ONCat API does *not* leak information in the case where a + // resource exists but a user is not allowed access -- a 404 would + // always be returned instead. So, if we ever get a 401, it is + // because our locally-stored token is no longer valid and we + // should log out. + logout(); + throw TokenRejectedError(errorMessage); + } + throw CatalogError(ie.what()); + } +} + +} // namespace ONCat +} // namespace Catalog +} // namespace Mantid diff --git a/Framework/Catalog/src/ONCatEntity.cpp b/Framework/Catalog/src/ONCatEntity.cpp new file mode 100644 index 00000000000..862cb6ff6a5 --- /dev/null +++ b/Framework/Catalog/src/ONCatEntity.cpp @@ -0,0 +1,251 @@ +#include "MantidCatalog/Exception.h" +#include "MantidCatalog/ONCatEntity.h" +#include "MantidKernel/StringTokenizer.h" + +#include <iostream> +#include <sstream> +#include <json/json.h> + +namespace Mantid { +namespace Catalog { +namespace ONCat { + +using Mantid::Kernel::StringTokenizer; +using Mantid::Kernel::make_unique; +using Mantid::Catalog::Exception::MalformedRepresentationError; + +//---------------------------------------------------------------------- +// Anonymous Helpers +//---------------------------------------------------------------------- +namespace { + +class ContentError : public std::runtime_error { +public: + explicit ContentError(const std::string & message) : + std::runtime_error(message) {} +}; + +Content getNestedContent( + const Content & content, const std::string & path +) { + const auto pathTokens = StringTokenizer( + path, ".", Mantid::Kernel::StringTokenizer::TOK_TRIM + ); + + auto currentNode = content; + + // Use the path tokens to drill down through the JSON nodes. + for ( + auto pathToken = pathTokens.cbegin(); + pathToken != pathTokens.cend(); ++pathToken + ) { + if (!currentNode.isMember(*pathToken)) { + throw ContentError(""); + } + currentNode = currentNode[*pathToken]; + } + + return currentNode; +} + +template<typename T> +T getNestedContentValueAsType( + const Content & content, const std::string & path +); +template<> std::string getNestedContentValueAsType( + const Content & content, const std::string & path +) { return getNestedContent(content, path).asString(); } +template<> int getNestedContentValueAsType( + const Content & content, const std::string & path +) { return getNestedContent(content, path).asInt(); } +template<> float getNestedContentValueAsType( + const Content & content, const std::string & path +) { return getNestedContent(content, path).asFloat(); } +template<> double getNestedContentValueAsType( + const Content & content, const std::string & path +) { return getNestedContent(content, path).asDouble(); } +template<> bool getNestedContentValueAsType( + const Content & content, const std::string & path +) { return getNestedContent(content, path).asBool(); } + +template<typename T> +T getNestedContentValueElseDefault( + const Content & content, const std::string & path, T defaultValue +) { + try { + return getNestedContentValueAsType<T>(content, path); + } catch (ContentError & ce) { + return defaultValue; + } +} + +template<typename T> +boost::optional<T> getNestedContentValueIfPresent( + const Content & content, const std::string & path +) { + try { + return boost::make_optional(getNestedContentValueAsType<T>(content, path)); + } catch (ContentError & ce) { + return boost::none; + } +} + +} + +//---------------------------------------------------------------------- +// ONCatEntity +//---------------------------------------------------------------------- + +ONCatEntity::ONCatEntity( + const std::string & id, + const std::string & type, + Content_uptr content +) : + m_id(id), + m_type(type), + m_content(std::move(content)) {} + +ONCatEntity::ONCatEntity(const ONCatEntity & other) : + m_id(other.m_id), + m_type(other.m_type), + m_content(make_unique<Content>(*other.m_content)) {} + +ONCatEntity::~ONCatEntity() {} + +std::string ONCatEntity::id() const { + return m_id; +} + +std::string ONCatEntity::type() const { + return m_type; +} + +std::string ONCatEntity::asString( + const std::string & path, const std::string defaultValue +) const { + return getNestedContentValueElseDefault(*m_content, path, defaultValue); +} + +int ONCatEntity::asInt( + const std::string & path, int defaultValue +) const { + return getNestedContentValueElseDefault(*m_content, path, defaultValue); +} + +float ONCatEntity::asFloat( + const std::string & path, float defaultValue +) const { + return getNestedContentValueElseDefault(*m_content, path, defaultValue); +} + +double ONCatEntity::asDouble( + const std::string & path, double defaultValue +) const { + return getNestedContentValueElseDefault(*m_content, path, defaultValue); +} + +bool ONCatEntity::asBool( + const std::string & path, bool defaultValue +) const { + return getNestedContentValueElseDefault(*m_content, path, defaultValue); +} + +boost::optional<std::string> ONCatEntity::asString( + const std::string & path +) const { + return getNestedContentValueIfPresent<std::string>(*m_content, path); +} + +boost::optional<int> ONCatEntity::asInt( + const std::string & path +) const { + return getNestedContentValueIfPresent<int>(*m_content, path); +} + +boost::optional<float> ONCatEntity::asFloat( + const std::string & path +) const { + return getNestedContentValueIfPresent<float>(*m_content, path); +} + +boost::optional<double> ONCatEntity::asDouble( + const std::string & path +) const { + return getNestedContentValueIfPresent<double>(*m_content, path); +} + +boost::optional<bool> ONCatEntity::asBool( + const std::string & path +) const { + return getNestedContentValueIfPresent<bool>(*m_content, path); +} + +std::string ONCatEntity::toString() const { + return m_content->toStyledString(); +} + +ONCatEntity ONCatEntity::fromJSONStream( + std::istream & streamContent +) { + auto content = make_unique<Content>(); + + try { + streamContent >> *content; + } catch (Json::Exception & je) { + throw MalformedRepresentationError(je.what()); + } + + const auto id = content->get("id", "").asString(); + const auto type = content->get("type", "").asString(); + + if (id == "" || type == "") { + throw MalformedRepresentationError( + "Expected \"id\" and \"type\" attributes from ONCat API, but these " + "were not found." + ); + } + + return ONCatEntity(id, type, std::move(content)); +} + +std::vector<ONCatEntity> ONCatEntity::vectorFromJSONStream( + std::istream & streamContent +) { + auto content = make_unique<Content>(); + + try { + streamContent >> *content; + } catch (Json::Exception & je) { + throw MalformedRepresentationError(je.what()); + } + + if (!content->isArray()) { + throw MalformedRepresentationError( + "Expected JSON representation to be an array of entities." + ); + } + + std::vector<ONCatEntity> entities; + + for (const auto & subContent : *content) { + const auto id = subContent.get("id", "").asString(); + const auto type = subContent.get("type", "").asString(); + + if (id == "" || type == "") { + throw MalformedRepresentationError( + "Expected \"id\" and \"type\" attributes from ONCat API, but these " + "were not found." + ); + } + + entities.push_back( + ONCatEntity(id, type, make_unique<Content>(subContent)) + ); + } + + return entities; +} + +} // namespace ONCat +} // namespace Catalog +} // namespace Mantid diff --git a/Framework/Catalog/test/CMakeLists.txt b/Framework/Catalog/test/CMakeLists.txt new file mode 100644 index 00000000000..9057de7df13 --- /dev/null +++ b/Framework/Catalog/test/CMakeLists.txt @@ -0,0 +1,20 @@ +if ( CXXTEST_FOUND ) + include_directories ( SYSTEM ${CXXTEST_INCLUDE_DIR} ) + + include_directories ( ../../TestHelpers/inc ) + # This variable is used within the cxxtest_add_test macro to build this helper class into the test executable. + # It will go out of scope at the end of this file so doesn't need un-setting + # set ( TESTHELPER_SRCS CatalogICatTestHelper.cpp + # ../../TestHelpers/src/TearDownWorld.cpp + # ) + + # The actual test suite + cxxtest_add_test ( CatalogTest ${TEST_FILES} ) + target_link_libraries( CatalogTest LINK_PRIVATE ${TCMALLOC_LIBRARIES_LINKTIME} ${MANTIDLIBS} + Catalog + ) + add_dependencies ( FrameworkTests CatalogTest ) + + # Add to the 'FrameworkTests' group in VS + set_property ( TARGET CatalogTest PROPERTY FOLDER "UnitTests" ) +endif () diff --git a/Framework/Catalog/test/OAuthTest.h b/Framework/Catalog/test/OAuthTest.h new file mode 100644 index 00000000000..ec326739a3e --- /dev/null +++ b/Framework/Catalog/test/OAuthTest.h @@ -0,0 +1,47 @@ +#ifndef MANTID_CATALOG_OAUTHTEST_H_ +#define MANTID_CATALOG_OAUTHTEST_H_ + +#include <cxxtest/TestSuite.h> + +#include "MantidCatalog/Exception.h" +#include "MantidCatalog/OAuth.h" +#include "MantidKernel/DateAndTime.h" + +using Mantid::Catalog::OAuth::OAuthToken; +using Mantid::Catalog::OAuth::IOAuthTokenStore; +using Mantid::Catalog::OAuth::IOAuthTokenStore_uptr; +using Mantid::Types::Core::DateAndTime; + +class OAuthTest : public CxxTest::TestSuite { +public: + // CxxTest boilerplate. + static OAuthTest *createSuite() { return new OAuthTest(); } + static void destroySuite(OAuthTest *suite) { delete suite; } + + void test_oauth_token_from_json_stream() { + std::stringstream tokenStringSteam; + tokenStringSteam << std::string( + "{\"token_type\": \"Bearer\", \"expires_in\": 3600, " + "\"access_token\": \"2KSL5aEnLvIudMHIjc7LcBWBCfxOHZ\", " + "\"scope\": \"api:read data:read settings:read\", " + "\"refresh_token\": \"eZEiz7LbgFrkL5ZHv7R4ck9gOzXexb\"}" + ); + const auto oauthToken = OAuthToken::fromJSONStream(tokenStringSteam); + TS_ASSERT_EQUALS(oauthToken.tokenType(), std::string("Bearer")); + TS_ASSERT_EQUALS(oauthToken.expiresIn(), 3600); + TS_ASSERT_EQUALS( + oauthToken.accessToken(), + std::string("2KSL5aEnLvIudMHIjc7LcBWBCfxOHZ") + ); + TS_ASSERT_EQUALS( + oauthToken.scope(), std::string("api:read data:read settings:read")); + TS_ASSERT_EQUALS( + oauthToken.refreshToken(), std::string("eZEiz7LbgFrkL5ZHv7R4ck9gOzXexb") + ); + + TS_ASSERT(!oauthToken.isExpired()); + TS_ASSERT(oauthToken.isExpired(DateAndTime::getCurrentTime() + 3601.0)); + } +}; + +#endif /* MANTID_CATALOG_OAUTHTEST_H_ */ diff --git a/Framework/Catalog/test/ONCatEntityTest.h b/Framework/Catalog/test/ONCatEntityTest.h new file mode 100644 index 00000000000..54f8582632b --- /dev/null +++ b/Framework/Catalog/test/ONCatEntityTest.h @@ -0,0 +1,155 @@ +#ifndef MANTID_CATALOG_ONCATENTITYTEST_H_ +#define MANTID_CATALOG_ONCATENTITYTEST_H_ + +#include <cxxtest/TestSuite.h> + +#include "MantidCatalog/Exception.h" +#include "MantidCatalog/ONCatEntity.h" + +using Mantid::Catalog::Exception::MalformedRepresentationError; +using Mantid::Catalog::ONCat::ONCatEntity; + +class ONCatEntityTest : public CxxTest::TestSuite { +public: + // CxxTest boilerplate. + static ONCatEntityTest *createSuite() { + return new ONCatEntityTest(); + } + static void destroySuite(ONCatEntityTest *suite) { delete suite; } + + void test_basic_attributes() { + std::string dummyRepresentation = + "{" + " \"id\": \"3fa1d522-f1b8-4134-a56b-b61f24d20510\"," + " \"type\": \"dummy\"" + "}"; + + std::stringstream ss; + ss << dummyRepresentation; + + auto dummy = ONCatEntity::fromJSONStream(ss); + + TS_ASSERT_EQUALS( + dummy.id(), std::string("3fa1d522-f1b8-4134-a56b-b61f24d20510") + ); + TS_ASSERT_EQUALS(dummy.type(), std::string("dummy")); + } + + void test_basic_attributes_of_entity_vector() { + std::string dummiesRepresentation = + "[" + " {" + " \"id\": \"3fa1d522-f1b8-4134-a56b-b61f24d20510\"," + " \"type\": \"dummy\"" + " }," + " {" + " \"id\": \"4b1dec2a-0f15-416d-8d23-e08901ac4634\"," + " \"type\": \"dummy\"" + " }" + "]"; + + std::stringstream ss; + ss << dummiesRepresentation; + + auto dummies = ONCatEntity::vectorFromJSONStream(ss); + + TS_ASSERT_EQUALS(dummies.size(), 2); + + TS_ASSERT_EQUALS( + dummies[0].id(), std::string("3fa1d522-f1b8-4134-a56b-b61f24d20510") + ); + TS_ASSERT_EQUALS(dummies[0].type(), std::string("dummy")); + + TS_ASSERT_EQUALS( + dummies[1].id(), std::string("4b1dec2a-0f15-416d-8d23-e08901ac4634") + ); + TS_ASSERT_EQUALS(dummies[1].type(), std::string("dummy")); + } + + void test_throws_on_malformed_json() { + std::string malformedRepresentation = + "{" + " \"id\": \"3fa1d522-f1b8-4134-a56b-b61f24d20510\"," + " \"type\": \"dummy"; + + std::stringstream ss; + ss << malformedRepresentation; + + TS_ASSERT_THROWS( + ONCatEntity::fromJSONStream(ss), + MalformedRepresentationError + ); + } + + void test_throws_on_malformed_representation() { + std::string missingTypeRepresentation = + "{" + " \"id\": \"3fa1d522-f1b8-4134-a56b-b61f24d20510\"" + "}"; + + std::stringstream ss; + ss << missingTypeRepresentation; + + TS_ASSERT_THROWS( + ONCatEntity::fromJSONStream(ss), + MalformedRepresentationError + ); + } + + void test_nested_values_with_various_types() { + std::string dummyRepresentation = + "{" + " \"id\": \"3fa1d522-f1b8-4134-a56b-b61f24d20510\"," + " \"type\": \"dummy\"," + " \"val\": {" + " \"a\": {" + " \"string\": \"value\"," + " \"int\": 1234," + " \"float\": 1234.5," + " \"double\": 1234.5," + " \"bool\": true" + " }" + " }" + "}"; + + std::stringstream ss; + ss << dummyRepresentation; + + auto dummy = ONCatEntity::fromJSONStream(ss); + + // Note implicit calls to get, i.e. "dummy.asType("val.a.type").get()". + TS_ASSERT_EQUALS(dummy.asString("val.a.string"), std::string("value")); + TS_ASSERT_EQUALS(dummy.asInt("val.a.int"), 1234); + TS_ASSERT_EQUALS(dummy.asFloat("val.a.float"), 1234.5f); + TS_ASSERT_EQUALS(dummy.asDouble("val.a.double"), 1234.5); + TS_ASSERT_EQUALS(dummy.asBool("val.a.bool"), true); + + TS_ASSERT(!dummy.asString("a.string")); + TS_ASSERT(!dummy.asInt("a.int")); + TS_ASSERT(!dummy.asFloat("a.float")); + TS_ASSERT(!dummy.asDouble("a.double")); + TS_ASSERT(!dummy.asBool("a.bool")); + } + + void test_default_values_with_various_types() { + std::string dummyRepresentation = + "{" + " \"id\": \"3fa1d522-f1b8-4134-a56b-b61f24d20510\"," + " \"type\": \"dummy\"" + " }" + "}"; + + std::stringstream ss; + ss << dummyRepresentation; + + auto dummy = ONCatEntity::fromJSONStream(ss); + + TS_ASSERT_EQUALS(dummy.asString("a.string", "val"), std::string("val")); + TS_ASSERT_EQUALS(dummy.asInt("a.int", 1234), 1234); + TS_ASSERT_EQUALS(dummy.asFloat("a.float", 1234.5f), 1234.5f); + TS_ASSERT_EQUALS(dummy.asDouble("a.double", 1234.5), 1234.5); + TS_ASSERT_EQUALS(dummy.asBool("a.bool", true), true); + } +}; + +#endif /* MANTID_CATALOG_ONCATENTITYTEST_H_ */ diff --git a/Framework/Catalog/test/ONCatTest.h b/Framework/Catalog/test/ONCatTest.h new file mode 100644 index 00000000000..933568a8d21 --- /dev/null +++ b/Framework/Catalog/test/ONCatTest.h @@ -0,0 +1,461 @@ +#ifndef MANTID_CATALOG_ONCATTEST_H_ +#define MANTID_CATALOG_ONCATTEST_H_ + +#include <cxxtest/TestSuite.h> + +#include "MantidCatalog/Exception.h" +#include "MantidCatalog/ONCat.h" +#include "MantidKernel/DateAndTime.h" +#include "MantidKernel/Exception.h" +#include "MantidKernel/InternetHelper.h" +#include "MantidKernel/make_unique.h" + +#include <map> + +#include <Poco/Net/HTTPResponse.h> + +using Poco::Net::HTTPResponse; + +using Mantid::Catalog::Exception::InvalidCredentialsError; +using Mantid::Catalog::Exception::InvalidRefreshTokenError; +using Mantid::Catalog::Exception::TokenRejectedError; +using Mantid::Catalog::OAuth::OAuthFlow; +using Mantid::Catalog::OAuth::OAuthToken; +using Mantid::Catalog::OAuth::ConfigServiceTokenStore; +using Mantid::Catalog::OAuth::IOAuthTokenStore; +using Mantid::Catalog::OAuth::IOAuthTokenStore_uptr; +using Mantid::Catalog::ONCat::ONCat; +using Mantid::Catalog::ONCat::QueryParameter; +using Mantid::Types::Core::DateAndTime; +using Mantid::Kernel::Exception::InternetError; + +//---------------------------------------------------------------------- +// Helpers, Mocks and Variables +//---------------------------------------------------------------------- + +namespace { + +using MockResponseMap = std::map<std::string, std::pair<int, std::string>>; +using MockResponseCallCounts = std::map<std::string, unsigned int>; +using MockResponseCallMapping = std::pair<const std::basic_string<char>, unsigned int>; + +class MockONCatAPI : public Mantid::Kernel::InternetHelper { +public: + MockONCatAPI() = delete; + MockONCatAPI( + const MockResponseMap & responseMap + ) : + Mantid::Kernel::InternetHelper(), + m_responseMap(responseMap), + m_responseCallCounts() + { + for (const auto & mapping : responseMap) { + m_responseCallCounts[mapping.first] = 0; + } + } + ~MockONCatAPI() { + TS_ASSERT(allResponsesCalledOnce()); + } + + bool allResponsesCalledOnce() const { + return std::all_of( + m_responseCallCounts.cbegin(), m_responseCallCounts.cend(), + [](const MockResponseCallMapping & mapping){ + return mapping.second == 1; + } + ); + } + +protected: + int sendHTTPRequest( + const std::string &url, std::ostream & responseStream + ) override { return sendHTTPSRequest(url, responseStream); } + + int sendHTTPSRequest( + const std::string &url, std::ostream & responseStream + ) override { + auto mockResponse = m_responseMap.find(url); + + assert(mockResponse != m_responseMap.end()); + m_responseCallCounts[url] += 1; + + const auto statusCode = mockResponse->second.first; + const auto responseBody = mockResponse->second.second; + + // Approximate the behaviour of the actual helper class when a + // non-OK response is observed. + if (statusCode != HTTPResponse::HTTP_OK) { + throw InternetError(responseBody, statusCode); + } + + responseStream << responseBody; + return statusCode; + } + +private: + MockResponseMap m_responseMap; + MockResponseCallCounts m_responseCallCounts; +}; + +std::unique_ptr<MockONCatAPI> make_mock_oncat_api( + const MockResponseMap & responseMap +) { + return Mantid::Kernel::make_unique<MockONCatAPI>(responseMap); +} + +class MockTokenStore : public IOAuthTokenStore { +public: + MockTokenStore() : m_token(boost::none) {} + + void setToken(const boost::optional<OAuthToken> & token) override { + m_token = token; + } + boost::optional<OAuthToken> getToken() override { return m_token; } +private: + boost::optional<OAuthToken> m_token; +}; + +IOAuthTokenStore_uptr make_mock_token_store() { + return Mantid::Kernel::make_unique<MockTokenStore>(); +} + +IOAuthTokenStore_uptr make_mock_token_store_already_logged_in() { + auto tokenStore = Mantid::Kernel::make_unique<MockTokenStore>(); + tokenStore->setToken(OAuthToken( + "Bearer", + 3600, + "2KSL5aEnLvIudMHIjc7LcBWBCfxOHZ", + "api:read data:read settings:read", + boost::make_optional<std::string>("eZEiz7LbgFrkL5ZHv7R4ck9gOzXexb") + )); + return std::move(tokenStore); +} + +const static std::string DUMMY_URL = "https://not.a.real.url"; +const static std::string DUMMY_CLIENT_ID = + "0e527a36-297d-4cb4-8a35-84f6b11248d7"; + +} + +//---------------------------------------------------------------------- +// Tests +//---------------------------------------------------------------------- + +class ONCatTest : public CxxTest::TestSuite { +public: + // CxxTest boilerplate. + static ONCatTest *createSuite() { return new ONCatTest(); } + static void destroySuite(ONCatTest *suite) { delete suite; } + + void test_login_with_invalid_credentials_throws() { + ONCat oncat( + DUMMY_URL, + make_mock_token_store(), + OAuthFlow::RESOURCE_OWNER_CREDENTIALS, + DUMMY_CLIENT_ID + ); + + TS_ASSERT(!oncat.isUserLoggedIn()); + + oncat.setInternetHelper(make_mock_oncat_api({ + { + DUMMY_URL + "/oauth/token", std::make_pair( + HTTPResponse::HTTP_UNAUTHORIZED, + "{\"error\": \"invalid_grant\", " + "\"error_description\": \"Invalid credentials given.\"}" + ) + } + })); + + TS_ASSERT_THROWS( + oncat.login("user", "does_not_exist"), + InvalidCredentialsError + ); + TS_ASSERT(!oncat.isUserLoggedIn()); + } + + void test_login_with_valid_credentials_is_successful() { + ONCat oncat( + DUMMY_URL, + make_mock_token_store(), + OAuthFlow::RESOURCE_OWNER_CREDENTIALS, + DUMMY_CLIENT_ID + ); + + TS_ASSERT(!oncat.isUserLoggedIn()); + + oncat.setInternetHelper(make_mock_oncat_api({ + { + DUMMY_URL + "/oauth/token", std::make_pair( + HTTPResponse::HTTP_OK, + "{\"token_type\": \"Bearer\", \"expires_in\": 3600, " + "\"access_token\": \"2KSL5aEnLvIudMHIjc7LcBWBCfxOHZ\", " + "\"scope\": \"api:read data:read settings:read\", " + "\"refresh_token\": \"eZEiz7LbgFrkL5ZHv7R4ck9gOzXexb\"}" + ) + } + })); + + oncat.login("user", "does_exist"); + + TS_ASSERT(oncat.isUserLoggedIn()); + } + + void test_refreshing_token_when_needed() { + ONCat oncat( + DUMMY_URL, + make_mock_token_store(), + OAuthFlow::RESOURCE_OWNER_CREDENTIALS, + DUMMY_CLIENT_ID + ); + + oncat.setInternetHelper(make_mock_oncat_api({ + { + DUMMY_URL + "/oauth/token", std::make_pair( + HTTPResponse::HTTP_OK, + "{\"token_type\": \"Bearer\", \"expires_in\": 3600, " + "\"access_token\": \"2KSL5aEnLvIudMHIjc7LcBWBCfxOHZ\", " + "\"scope\": \"api:read data:read settings:read\", " + "\"refresh_token\": \"eZEiz7LbgFrkL5ZHv7R4ck9gOzXexb\"}" + ) + } + })); + + oncat.login("user", "does_exist"); + + TS_ASSERT(oncat.isUserLoggedIn()); + + oncat.refreshTokenIfNeeded(); + TS_ASSERT(oncat.isUserLoggedIn()); + + oncat.setInternetHelper(make_mock_oncat_api({ + { + DUMMY_URL + "/oauth/token", std::make_pair( + HTTPResponse::HTTP_OK, + "{\"token_type\": \"Bearer\", \"expires_in\": 3600, " + "\"access_token\": \"7dS7flfhsf7ShndHJSFknfskfeu789\", " + "\"scope\": \"api:read data:read settings:read\", " + "\"refresh_token\": \"sdagSDGF87dsgljerg6gdfgddfgfdg\"}" + ) + } + })); + + oncat.refreshTokenIfNeeded(DateAndTime::getCurrentTime() + 3601.0); + TS_ASSERT(oncat.isUserLoggedIn()); + } + + void test_logged_out_when_refreshing_fails() { + ONCat oncat( + DUMMY_URL, + make_mock_token_store(), + OAuthFlow::RESOURCE_OWNER_CREDENTIALS, + DUMMY_CLIENT_ID + ); + + oncat.setInternetHelper(make_mock_oncat_api({ + { + DUMMY_URL + "/oauth/token", std::make_pair( + HTTPResponse::HTTP_OK, + "{\"token_type\": \"Bearer\", \"expires_in\": 3600, " + "\"access_token\": \"2KSL5aEnLvIudMHIjc7LcBWBCfxOHZ\", " + "\"scope\": \"api:read data:read settings:read\", " + "\"refresh_token\": \"eZEiz7LbgFrkL5ZHv7R4ck9gOzXexb\"}" + ) + } + })); + + oncat.login("user", "does_exist"); + + TS_ASSERT(oncat.isUserLoggedIn()); + + oncat.refreshTokenIfNeeded(); + TS_ASSERT(oncat.isUserLoggedIn()); + + oncat.setInternetHelper(make_mock_oncat_api({ + { + DUMMY_URL + "/oauth/token", std::make_pair( + HTTPResponse::HTTP_UNAUTHORIZED, + "{\"error\": \"invalid_grant\", " + "\"error_description\": \"Bearer token not found.\"}" + ) + } + })); + + TS_ASSERT_THROWS( + oncat.refreshTokenIfNeeded(DateAndTime::getCurrentTime() + 3601.0), + InvalidRefreshTokenError + ); + + TS_ASSERT(!oncat.isUserLoggedIn()); + } + + void test_retrieve_entity() { + ONCat oncat( + DUMMY_URL, + make_mock_token_store_already_logged_in(), + OAuthFlow::RESOURCE_OWNER_CREDENTIALS, + DUMMY_CLIENT_ID + ); + + TS_ASSERT(oncat.isUserLoggedIn()); + + oncat.setInternetHelper(make_mock_oncat_api({ + { + DUMMY_URL + "/api/instruments/HB2C?facility=HFIR", std::make_pair( + HTTPResponse::HTTP_OK, + "{\"facility\": \"HFIR\"," + "\"name\": \"HB2C\"," + "\"id\": \"HB2C\"," + "\"type\": \"instrument\"}" + ) + } + })); + + const auto entity = oncat.retrieve("api", "instruments", "HB2C", { + QueryParameter("facility", "HFIR") + }); + + TS_ASSERT_EQUALS(entity.id(), std::string("HB2C")); + TS_ASSERT_EQUALS(entity.asString("name"), std::string("HB2C")); + } + + void test_list_entities() { + ONCat oncat( + DUMMY_URL, + make_mock_token_store_already_logged_in(), + OAuthFlow::RESOURCE_OWNER_CREDENTIALS, + DUMMY_CLIENT_ID + ); + + TS_ASSERT(oncat.isUserLoggedIn()); + + oncat.setInternetHelper(make_mock_oncat_api({ + { + DUMMY_URL + "/api/instruments?facility=HFIR", std::make_pair( + HTTPResponse::HTTP_OK, + "[" + " {" + " \"facility\": \"HFIR\"," + " \"name\": \"HB2C\"," + " \"id\": \"HB2C\"," + " \"type\": \"instrument\"" + " }," + " {" + " \"facility\": \"HFIR\"," + " \"name\": \"CG1D\"," + " \"id\": \"CG1D\"," + " \"type\": \"instrument\"" + " }" + "]" + ) + } + })); + + const auto entities = oncat.list("api", "instruments", { + QueryParameter("facility", "HFIR") + }); + + TS_ASSERT_EQUALS(entities.size(), 2); + TS_ASSERT_EQUALS(entities[0].id(), std::string("HB2C")); + TS_ASSERT_EQUALS(entities[0].asString("name"), std::string("HB2C")); + TS_ASSERT_EQUALS(entities[1].id(), std::string("CG1D")); + TS_ASSERT_EQUALS(entities[1].asString("name"), std::string("CG1D")); + } + + void test_send_api_request_logs_out_with_invalid_grant() { + ONCat oncat( + DUMMY_URL, + make_mock_token_store_already_logged_in(), + OAuthFlow::RESOURCE_OWNER_CREDENTIALS, + DUMMY_CLIENT_ID + ); + + TS_ASSERT(oncat.isUserLoggedIn()); + + oncat.setInternetHelper(make_mock_oncat_api({ + { + DUMMY_URL + "/api/instruments?facility=HFIR", std::make_pair( + HTTPResponse::HTTP_UNAUTHORIZED, "{}" + ) + } + })); + + TS_ASSERT_THROWS( + oncat.list("api", "instruments", {QueryParameter("facility", "HFIR")}), + TokenRejectedError + ); + TS_ASSERT(!oncat.isUserLoggedIn()); + } + + void test_client_credentials_flow_with_refresh() { + ONCat oncat( + DUMMY_URL, + make_mock_token_store(), + OAuthFlow::CLIENT_CREDENTIALS, + DUMMY_CLIENT_ID, + boost::make_optional<std::string>( + "9a2ad07a-a139-438b-8116-08c5452f96ad" + ) + ); + + oncat.setInternetHelper(make_mock_oncat_api({ + { + DUMMY_URL + "/oauth/token", std::make_pair( + HTTPResponse::HTTP_OK, + "{\"token_type\": \"Bearer\", \"expires_in\": 3600, " + "\"access_token\": \"2KSL5aEnLvIudMHIjc7LcBWBCfxOHZ\", " + "\"scope\": \"api:read data:read settings:read\"}" + ) + }, { + DUMMY_URL + "/api/instruments/HB2C?facility=HFIR", std::make_pair( + HTTPResponse::HTTP_OK, + "{\"facility\": \"HFIR\"," + "\"name\": \"HB2C\"," + "\"id\": \"HB2C\"," + "\"type\": \"instrument\"}" + ) + } + })); + + oncat.retrieve("api", "instruments", "HB2C", { + QueryParameter("facility", "HFIR") + }); + + oncat.setInternetHelper(make_mock_oncat_api({ + { + DUMMY_URL + "/oauth/token", std::make_pair( + HTTPResponse::HTTP_OK, + "{\"token_type\": \"Bearer\", \"expires_in\": 3600, " + "\"access_token\": \"987JHGFiusdvs72fAkjhsKJH32tkjk\", " + "\"scope\": \"api:read data:read settings:read\"}" + ), + } + })); + + oncat.refreshTokenIfNeeded(DateAndTime::getCurrentTime() + 3601.0); + } + + void test_config_service_token_store_roundtrip() { + ConfigServiceTokenStore tokenStore; + + const auto testToken = boost::make_optional(OAuthToken( + "Bearer", + 3600, + "2KSL5aEnLvIudMHIjc7LcBWBCfxOHZ", + "api:read data:read settings:read", + boost::make_optional<std::string>("eZEiz7LbgFrkL5ZHv7R4ck9gOzXexb") + )); + + tokenStore.setToken(testToken); + + const auto result = tokenStore.getToken(); + + TS_ASSERT_EQUALS(testToken->tokenType(), result->tokenType()); + TS_ASSERT_EQUALS(testToken->expiresIn(), result->expiresIn()); + TS_ASSERT_EQUALS(testToken->accessToken(), result->accessToken()); + TS_ASSERT_EQUALS(testToken->scope(), result->scope()); + TS_ASSERT_EQUALS(*testToken->refreshToken(), *result->refreshToken()); + } +}; + +#endif /* MANTID_CATALOG_ONCATTEST_H_ */ -- GitLab