diff --git a/test/TestXStationClient.cpp b/test/TestXStationClient.cpp index aadc2a5..7de48b5 100644 --- a/test/TestXStationClient.cpp +++ b/test/TestXStationClient.cpp @@ -22,36 +22,6 @@ class XStationClientTest : public testing::Test return m_context; } - template auto runAwaitable(Awaitable &&awaitable) - { - std::exception_ptr eptr; - using result_type = typename Awaitable::value_type; - result_type result; - - boost::asio::co_spawn( - m_context, - [&]() -> boost::asio::awaitable { - try - { - result = co_await std::forward(awaitable); - } - catch (...) - { - eptr = std::current_exception(); - } - }, - boost::asio::detached); - - m_context.run(); - - if (eptr) - { - std::rethrow_exception(eptr); - } - - return result; - } - template auto runAwaitableVoid(Awaitable &&awaitable) { std::exception_ptr eptr; @@ -83,7 +53,10 @@ class XStationClientTest : public testing::Test TEST_F(XStationClientTest, XStationClient_string_constructor) { - EXPECT_NO_THROW(auto client = std::make_unique(getIoContext(), "test", "test", "demo")); + std::shared_ptr client; + EXPECT_NO_THROW(client = std::make_unique(getIoContext(), "test", "test", "demo")); + EXPECT_TRUE(client->socket == nullptr); + EXPECT_TRUE(client->stream == nullptr); } TEST_F(XStationClientTest, XStationClient_json_constructor) @@ -94,10 +67,44 @@ TEST_F(XStationClientTest, XStationClient_json_constructor) {"accountType", "demo"} }; - EXPECT_NO_THROW(auto client = std::make_unique(getIoContext(), accountCredentials)); + std::shared_ptr client; + EXPECT_NO_THROW(client = std::make_unique(getIoContext(), accountCredentials)); + EXPECT_TRUE(client->socket == nullptr); + EXPECT_TRUE(client->stream == nullptr); +} + +TEST_F(XStationClientTest, XStationClient_setupSocketConnection_exception) +{ + const boost::json::object accountCredentials = { + {"accountId", "test"}, + {"password", "test"}, + {"accountType", "demo"} + }; + + std::unique_ptr client; + EXPECT_NO_THROW(client = std::make_unique(getIoContext(), accountCredentials)); + EXPECT_THROW(runAwaitableVoid(client->setupSocketConnection()), exception::LoginFailed); + EXPECT_TRUE(client->socket != nullptr); + EXPECT_TRUE(client->stream == nullptr); +} + +TEST_F(XStationClientTest, XStationClient_setupStreamConnection_exception) +{ + const boost::json::object accountCredentials = { + {"accountId", "test"}, + {"password", "test"}, + {"accountType", "demo"} + }; + + std::unique_ptr client; + EXPECT_NO_THROW(client = std::make_unique(getIoContext(), accountCredentials)); + EXPECT_THROW(runAwaitableVoid(client->setupStreamConnection()), exception::LoginFailed); + EXPECT_TRUE(client->socket == nullptr); + // Socket is initialized, but not logged in, so no stream session ID is available, that is why no stream is created. + EXPECT_TRUE(client->stream == nullptr); } -TEST_F(XStationClientTest, XStationClient_getSocket_exception) +TEST_F(XStationClientTest, XStationClient_closeSocketConnection) { const boost::json::object accountCredentials = { {"accountId", "test"}, @@ -107,10 +114,10 @@ TEST_F(XStationClientTest, XStationClient_getSocket_exception) std::unique_ptr client; EXPECT_NO_THROW(client = std::make_unique(getIoContext(), accountCredentials)); - EXPECT_THROW(runAwaitable(client->getSocket()), exception::LoginFailed); + EXPECT_NO_THROW(runAwaitableVoid(client->closeSocketConnection())); } -TEST_F(XStationClientTest, XStationClient_getStream_exception) +TEST_F(XStationClientTest, XStationClient_closeStreamConnection) { const boost::json::object accountCredentials = { {"accountId", "test"}, @@ -120,5 +127,5 @@ TEST_F(XStationClientTest, XStationClient_getStream_exception) std::unique_ptr client; EXPECT_NO_THROW(client = std::make_unique(getIoContext(), accountCredentials)); - EXPECT_THROW(runAwaitable(client->getStream()), exception::LoginFailed); + EXPECT_NO_THROW(runAwaitableVoid(client->closeStreamConnection())); } diff --git a/xapi/XStationClient.cpp b/xapi/XStationClient.cpp index e3ff0aa..d0e142e 100644 --- a/xapi/XStationClient.cpp +++ b/xapi/XStationClient.cpp @@ -6,35 +6,57 @@ namespace xapi XStationClient::XStationClient(boost::asio::io_context &ioContext, const std::string &accountId, const std::string &password, const std::string &accountType) - : m_ioContext(ioContext), m_accountId(accountId), m_password(password), m_accountType(accountType), - m_streamSessionId("") + : socket(nullptr), stream(nullptr), m_ioContext(ioContext), m_accountId(accountId), m_password(password), + m_accountType(accountType), m_streamSessionId("") { } XStationClient::XStationClient(boost::asio::io_context &ioContext, const boost::json::object &accountCredentials) - : m_ioContext(ioContext), m_accountId(accountCredentials.at("accountId").as_string()), - m_password(accountCredentials.at("password").as_string()), - m_accountType(accountCredentials.at("accountType").as_string()), m_streamSessionId("") + : XStationClient(ioContext, + std::string(accountCredentials.at("accountId").as_string()), + std::string(accountCredentials.at("password").as_string()), + std::string(accountCredentials.at("accountType").as_string())) { } -boost::asio::awaitable> XStationClient::getSocket() +boost::asio::awaitable XStationClient::setupSocketConnection() { - auto socket = std::make_shared(m_ioContext); + socket = std::make_unique(m_ioContext); co_await socket->initSession(m_accountType); m_streamSessionId = co_await socket->login(m_accountId, m_password); - co_return socket; } -boost::asio::awaitable> XStationClient::getStream() const +boost::asio::awaitable XStationClient::setupStreamConnection() { if (m_streamSessionId.empty()) { - throw xapi::exception::LoginFailed("No stream session ID, get Socket to establish a session first"); + throw exception::LoginFailed("No stream session ID, get Socket to establish a session first"); } - auto stream = std::make_shared(m_ioContext); + stream = std::make_unique(m_ioContext); co_await stream->initSession(m_accountType, m_streamSessionId); - co_return stream; +} + +boost::asio::awaitable XStationClient::closeSocketConnection() +{ + if (socket == nullptr) + { + co_return; + } + auto result = co_await socket->logout(); + if (result["status"].as_bool() != true) + { + // If logout fails and server is not closed the connection gracefully, close it from client side. + co_await socket->closeSession(); + } +} + +boost::asio::awaitable XStationClient::closeStreamConnection() +{ + if (stream == nullptr) + { + co_return; + } + co_await stream->closeSession(); } } // namespace xapi diff --git a/xapi/XStationClient.hpp b/xapi/XStationClient.hpp index 684a58c..f59d60f 100644 --- a/xapi/XStationClient.hpp +++ b/xapi/XStationClient.hpp @@ -60,16 +60,35 @@ class XStationClient ~XStationClient() = default; /** - * @brief Retrieves a configured Socket object for the client. + * @brief Performes setup of the socket connection. * @return An awaitable std::shared_ptr object. */ - boost::asio::awaitable> getSocket(); + boost::asio::awaitable setupSocketConnection(); /** - * @brief Retrieves a configured Stream object for the client. + * @brief Performes setup of the stream connection. * @return An awaitable std::shared_ptr object. */ - boost::asio::awaitable> getStream() const; + boost::asio::awaitable setupStreamConnection(); + + /** + * @brief Closes the socket connection. + * + * Tries to logout from the server and close the connection gracefully. If the server responds with negative status, the connection + * is closed from the client side. + * + * @return An awaitable void. + */ + boost::asio::awaitable closeSocketConnection(); + + /** + * @brief Closes the stream connection. + * @return An awaitable void. + */ + boost::asio::awaitable closeStreamConnection(); + + std::unique_ptr socket; + std::unique_ptr stream; private: boost::asio::io_context &m_ioContext;