diff --git a/.github/ISSUE_TEMPLATE/bug-report.yml b/.github/ISSUE_TEMPLATE/bug-report.yml new file mode 100644 index 0000000..d02c68b --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug-report.yml @@ -0,0 +1,54 @@ +name: Bug Report +description: Report a bug +labels: ["bug"] +body: + - type: markdown + attributes: + value: | + Thanks for taking the time to fill out this bug report! + - type: dropdown + id: version + attributes: + label: Version of xapi-cpp + description: In which version of xapi-cpp did this bug occur? + options: + - v1.0.1 + - v1.0.0 + default: 0 + validations: + required: true + - type: textarea + id: operating-system + attributes: + label: Operating system used + description: On which operating system and version did you encounter this bug? + placeholder: e.g., Windows 10, macOS Ventura, Ubuntu 20.04 + validations: + required: true + - type: textarea + id: steps + attributes: + label: Steps to reproduce + description: Detailed steps on how to trigger the bug + placeholder: please provide descrition, code snippets and so on + validations: + required: true + - type: textarea + id: expected + attributes: + label: Expected Behavior + placeholder: I expected this to happen + - type: textarea + id: error-logs + attributes: + label: Put here any logs (optional) + - type: dropdown + id: assign + attributes: + label: "Would you like to work on this issue?" + options: + - "Yes" + - type: markdown + attributes: + value: | + Thanks for reporting this issue! We will get back to you as soon as possible. diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..0086358 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1 @@ +blank_issues_enabled: true diff --git a/.github/ISSUE_TEMPLATE/new-feature.yml b/.github/ISSUE_TEMPLATE/new-feature.yml new file mode 100644 index 0000000..b4dbb27 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/new-feature.yml @@ -0,0 +1,37 @@ +name: Feature request +description: Suggest or request a new feature +labels: ["enhancement"] +assignees: ["MPogotsky"] +body: + - type: markdown + attributes: + value: | + Please fill out the sections below to properly describe the new feature you are suggesting. + - type: textarea + id: description + attributes: + label: Describe the feature + placeholder: New method in xapi client that allows to do Y + validations: + required: true + - type: textarea + id: rationale + attributes: + label: It should be implemented because + placeholder: It will allow to do Y that is needed for Z + - type: textarea + id: context + attributes: + label: Additional context + placeholder: | + Add any other context about the feature request here + - type: dropdown + id: assign + attributes: + label: "Would you like to work on this issue?" + options: + - "Yes" + - type: markdown + attributes: + value: | + Thanks for your suggestion! Let`s discuss how it can be implemented diff --git a/.github/workflows/test-xapi-cpp.yml b/.github/workflows/test-xapi-cpp.yml index f049495..e4509d6 100644 --- a/.github/workflows/test-xapi-cpp.yml +++ b/.github/workflows/test-xapi-cpp.yml @@ -2,7 +2,7 @@ name: Test xapi-cpp on: push: - branches: [ "main" ] + branches: [ "main", "develop" ] pull_request: branches: [ "main" ] @@ -41,7 +41,7 @@ jobs: run: > cmake -B ${{ steps.strings.outputs.build-output-dir }} \ -DCMAKE_CXX_COMPILER=${{ matrix.compiler }} \ - -DBUILD_TESTS=ON \ + -DCMAKE_BUILD_TYPE=Debug \ -S ${{ github.workspace }} - name: Build unit tests diff --git a/CMakeHelpers.cmake b/CMakeHelpers.cmake index c00d076..c2ef3f4 100644 --- a/CMakeHelpers.cmake +++ b/CMakeHelpers.cmake @@ -26,10 +26,11 @@ endmacro() # Helper macro to find google test library macro(helper_FIND_GTEST_LIBS) - find_package(GTest REQUIRED) - if(NOT GTest_FOUND) - message("Gtets not found. Tests won`t compile.") - else() - include_directories(${GTEST_INCLUDE_DIRS}) - endif() + include(FetchContent) + FetchContent_Declare( + googletest + URL https://github.com/google/googletest/archive/refs/heads/main.zip + ) + # Download and make gtest/gmock available + FetchContent_MakeAvailable(googletest) endmacro() diff --git a/CMakeLists.txt b/CMakeLists.txt index 7e4af38..9a2fd74 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -10,19 +10,22 @@ set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD_REQUIRED ON) set(BOOST_VERSION_REQUIRED "1.83.0") +include(${CMAKE_SOURCE_DIR}/CMakeHelpers.cmake) + # OPTIONS ==================================== -option(BUILD_TESTS "Build tests" OFF) -add_definitions(-DBUILD_TESTS) +if(NOT CMAKE_BUILD_TYPE) + set(CMAKE_BUILD_TYPE Release CACHE STRING "Choose the type of build." FORCE) +endif() +message(STATUS "CMAKE_BUILD_TYPE: ${CMAKE_BUILD_TYPE}") # XAPI ======================================= -include(${CMAKE_SOURCE_DIR}/CMakeHelpers.cmake) - helper_FIND_BOOST_LIBS() helper_FIND_OPENSSL_LIB() - add_subdirectory(xapi) # TESTS ====================================== -if(BUILD_TESTS) +if(CMAKE_BUILD_TYPE STREQUAL "Debug") + helper_FIND_GTEST_LIBS() + add_definitions(-DENABLE_TEST) add_subdirectory(test) endif() diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..1984845 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,52 @@ +# Contributing to xapi-cpp +Thank you for your interest in contributing to **xapi-cpp**! +This document outlines the guidelines for contributing, making it a smooth and collaborative experience for everyone. + +## How Can I Contribute? + +To report a bug, request a new feature, or contribute code, please use GitHub issues with the provided issue templates. You can open a new issue [here](https://github.com/MPogotsky/xapi-cpp/issues). + +## Contributing Workflow + +1. **Fork the repository** +2. **Clone the repository** +3. **Create a new branch** + + Create a new branch for your changes. Use a descriptive branch name. + ``` + git checkout -b + ``` + +4. **Make your changes** + + Implement your changes in the codebase. This might involve adding new features, fixing bugs, or improving documentation. + +5. **Test your changes!** + + Thoroughly test your changes to ensure they work as expected and don't introduce any regressions. Refer to our Testing Guidelines for specific instructions. + +6. **Commit and push your changes** + + Stage your changes and commit them with a clear and concise commit message. + + If you are solving issue from issue list, please, start you commit with #\. + + Then, push your branch to your forked repository. + ``` + git add . + git commit -m "# Fix commit message" + git push origin + ``` +7. **Create a pull request to master branch** + + Create a pull request targeting the ```main``` branch. + +8. **Wait for the pull request to be reviewed and merged** + + Your pull request will be reviewed by project maintainers. Address any feedback or requested changes. Once approved, your pull request will be merged into the main branch. + + Thank you for your contribution! + + +## Contact +Feel free to reach out via email at matsvei.pahotski@gmail.com if you need direct assistance. diff --git a/README.md b/README.md index 127d979..63a696e 100644 --- a/README.md +++ b/README.md @@ -4,9 +4,7 @@ [![license](https://img.shields.io/badge/license-MIT-blue)](https://github.com/MPogotsky/xapi-cpp/LICENSE) -The xStation5 API C++ library provides a simple and easy-to-use API for interacting with the xStation5 trading platform. With this library, you can connect to the xStation5 platform, retrieve market data, and execute trades. - -This library provides interface to work with [XTB](https://www.xtb.com) xStation5 accounts. +This library provides C++ interface to work with [XTB](https://www.xtb.com) xStation5 accounts. It can be used to connect to xStation5 platform, retrieve market data and execute trades. API documentation: @@ -42,7 +40,7 @@ Step-by-step guide to build the project using CMake. cd build ``` -3. Run CMake to configure and build the project: +3. Run CMake to configure and build Release version of the library: ```bash cmake .. @@ -79,6 +77,7 @@ Xapi supports ``find_package``, simplifying the process of linking the library t PRIVATE Boost::system Boost::json + Boost::url Xapi::Xapi ) ``` @@ -143,10 +142,10 @@ More examples can be found in [examples](examples/) folder. To build the tests, follow these steps: 1. Navigate to the `build` directory. -2. Run the following commands to configure and build the tests using CMake: +2. Run the following commands to configure debug mode and build the tests using CMake: ```bash - cmake -DBUILD_TESTS=ON .. + cmake -DCMAKE_BUILD_TYPE=Debug .. cmake --build . ``` @@ -160,14 +159,11 @@ To build the tests, follow these steps: ## Getting Help -If you have questions, issues, or need assistance with this project, you can: - -- Check the [GitHub Issues](https://github.com/MPogotsky/xapi-cpp/issues) page to report problems or check for known issues. -- Review the [Wiki](https://github.com/MPogotsky/xapi-cpp/wiki) for more detailed documentation and FAQs. +If you have questions, issues, or need assistance with this project, you can visit the [GitHub Issues](https://github.com/MPogotsky/xapi-cpp/issues) page to report problems or check for known issues. Feel free to reach out via email at matsvei.pahotski@gmail.com if you need direct assistance. ## Contributing -To contribute, please fork the repository, make your changes, and submit a pull request. All contributions are reviewed, and any useful additions are welcome! +To contribute, please fork the repository, make your changes, and submit a pull request. All contributions will be reviewed, and any useful additions are welcome. Any review or improvement suggestions you may have are greatly appreciated diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 9a137e4..852c913 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -1,4 +1,3 @@ -helper_FIND_GTEST_LIBS() enable_testing() set( SOURCES @@ -7,26 +6,21 @@ set( SOURCES TestXStationClientStream.cpp ) +add_subdirectory(mocks) add_executable( tests ${SOURCES} ) -target_compile_options(tests PRIVATE - -Wall - -Wextra - -Wpedantic - -Werror - -Wno-unused-parameter - -Wno-missing-field-initializers -) - +target_include_directories(tests PRIVATE mocks) target_link_libraries(tests PRIVATE ${GTEST_LIBRARIES} Boost::system Boost::url Boost::json Xapi - GTest::gtest_main + gtest + gtest_main + test_mocks ) include(GoogleTest) diff --git a/test/TestXStationClient.cpp b/test/TestXStationClient.cpp index 9885437..e62225a 100644 --- a/test/TestXStationClient.cpp +++ b/test/TestXStationClient.cpp @@ -1,29 +1,40 @@ +#include "MockConnection.hpp" #include "xapi/Exceptions.hpp" #include "xapi/XStationClient.hpp" #include +#include #include -using namespace xapi; +namespace xapi +{ -class XStationClientTest : public testing::Test +class XStationClientTest : public ::testing::Test { protected: + std::unique_ptr client; void SetUp() override { + client = std::make_unique(m_context, "test", "test", "demo"); + auto connection = std::make_unique(); + EXPECT_NO_THROW(client->m_connection = std::move(connection)); } void TearDown() override { + client.reset(); + } + + MockConnection &getMockedConnection() + { + return *dynamic_cast(client->m_connection.get()); } - public: boost::asio::io_context &getIoContext() { return m_context; } - template - auto runAwaitable(Awaitable &&awaitable) + template auto runAwaitable(Awaitable &&awaitable) { std::exception_ptr eptr; using result_type = typename Awaitable::value_type; @@ -53,8 +64,7 @@ class XStationClientTest : public testing::Test return result; } - template - auto runAwaitableVoid(Awaitable &&awaitable) + template auto runAwaitableVoid(Awaitable &&awaitable) { std::exception_ptr eptr; boost::asio::co_spawn( @@ -83,24 +93,25 @@ class XStationClientTest : public testing::Test boost::asio::io_context m_context; }; -const boost::json::object testAccountCredentials = { - {"accountId", "test"}, - {"password", "test"}, - {"accountType", "demo"} -}; - - -TEST_F(XStationClientTest, XStationClient_string_constructor) +TEST(XStationClientConstructorTest, string_constructor) { - EXPECT_NO_THROW(XStationClient client(getIoContext(), "test", "test", "demo")); + boost::asio::io_context ioContext; + EXPECT_NO_THROW(XStationClient client(ioContext, "test", "test", "demo")); } -TEST_F(XStationClientTest, XStationClient_json_constructor) +TEST(XStationClientConstructorTest, json_constructor) { - EXPECT_NO_THROW(XStationClient client(getIoContext(), testAccountCredentials);); + boost::asio::io_context ioContext; + const boost::json::object testAccountCredentials = { + {"accountId", "test"}, + {"password", "test"}, + {"accountType", "demo"} + }; + + EXPECT_NO_THROW(XStationClient client(ioContext, testAccountCredentials);); } -TEST_F(XStationClientTest, XStationClient_login_exception_invalid_account_type) +TEST_F(XStationClientTest, login_invalid_account_type) { const boost::json::object accountCredentials = { {"accountId", "test"}, @@ -110,218 +121,1260 @@ TEST_F(XStationClientTest, XStationClient_login_exception_invalid_account_type) XStationClient client(getIoContext(), accountCredentials); EXPECT_THROW(runAwaitableVoid(client.login()), exception::LoginFailed); + EXPECT_TRUE(client.m_streamSessionId.empty()); } -TEST_F(XStationClientTest, XStationClient_logout_exception) +TEST_F(XStationClientTest, login_account_credentials_null) +{ + const boost::json::object accountCredentials = { + {"accountId", ""}, + {"password", ""}, + {"accountType", "test"} + }; + + std::unique_ptr client; + EXPECT_NO_THROW(client = std::make_unique(getIoContext(), accountCredentials)); + EXPECT_THROW(runAwaitableVoid(client->login()), exception::LoginFailed); + EXPECT_TRUE(client->m_streamSessionId.empty()); +} + +TEST_F(XStationClientTest, login_invalid_return_from_the_server) { - XStationClient client(getIoContext(), testAccountCredentials); - EXPECT_THROW(runAwaitableVoid(client.logout()), exception::ConnectionClosed); + EXPECT_CALL(getMockedConnection(), connect(testing::_)) + .WillOnce([](const boost::url &url) -> boost::asio::awaitable { co_return; }); + + EXPECT_CALL(getMockedConnection(), makeRequest(testing::_)) + .WillOnce([](const boost::json::object &command) -> boost::asio::awaitable { co_return; }); + + EXPECT_CALL(getMockedConnection(), waitResponse()) + .WillOnce([]() -> boost::asio::awaitable { + boost::json::object response = {{"broken", true}}; + co_return response; + }); + + EXPECT_THROW(runAwaitableVoid(client->login()), exception::LoginFailed); + EXPECT_TRUE(client->m_streamSessionId.empty()); +} + +TEST_F(XStationClientTest, login_ok) +{ + const boost::json::object serverResponse = {{"status", true}, {"streamSessionId", "test"}}; + + EXPECT_CALL(getMockedConnection(), connect(testing::_)) + .WillOnce([](const boost::url &url) -> boost::asio::awaitable { co_return; }); + + EXPECT_CALL(getMockedConnection(), makeRequest(testing::_)) + .WillOnce([](const boost::json::object &command) -> boost::asio::awaitable { co_return; }); + + EXPECT_CALL(getMockedConnection(), waitResponse()) + .WillOnce([&serverResponse]() -> boost::asio::awaitable { + co_return serverResponse; + }); + + EXPECT_NO_THROW(runAwaitableVoid(client->login())); + EXPECT_TRUE(client->m_streamSessionId == "test"); +} + +TEST_F(XStationClientTest, logout_exception) +{ + const boost::json::object expectedCommand = {{"command", "logout"}}; + + EXPECT_CALL(getMockedConnection(), makeRequest(testing::_)) + .WillOnce([&expectedCommand](const boost::json::object &command) -> boost::asio::awaitable { + EXPECT_EQ(command, expectedCommand); + throw exception::ConnectionClosed("Exception"); + co_return; + } + ); + + EXPECT_THROW(runAwaitableVoid(client->logout()), exception::ConnectionClosed); +} + +TEST_F(XStationClientTest, logout_ok) +{ + const boost::json::object expectedCommand = {{"command", "logout"}}; + const boost::json::object serverResponse = {{"status", true}}; + + EXPECT_CALL(getMockedConnection(), makeRequest(testing::_)) + .WillOnce([&expectedCommand](const boost::json::object &command) -> boost::asio::awaitable { + EXPECT_EQ(command, expectedCommand); + co_return; + } + ); + + EXPECT_CALL(getMockedConnection(), waitResponse()) + .WillOnce([&serverResponse]() -> boost::asio::awaitable { + co_return serverResponse; + }); + + EXPECT_CALL(getMockedConnection(), disconnect()) + .WillOnce([]() -> boost::asio::awaitable { + co_return; + }); + + EXPECT_NO_THROW(runAwaitableVoid(client->logout())); } TEST_F(XStationClientTest, getAllSymbols_exception) { - XStationClient client(getIoContext(), testAccountCredentials); + const boost::json::object expectedCommand = {{"command", "getAllSymbols"}}; + + EXPECT_CALL(getMockedConnection(), makeRequest(testing::_)) + .WillOnce([&expectedCommand](const boost::json::object &command) -> boost::asio::awaitable { + EXPECT_EQ(command, expectedCommand); + throw exception::ConnectionClosed("Exception"); + co_return; + } + ); + boost::json::object result; - EXPECT_THROW(result = runAwaitable(client.getAllSymbols()), exception::ConnectionClosed); + EXPECT_THROW(result = runAwaitable(client->getAllSymbols()), exception::ConnectionClosed); EXPECT_TRUE(result.empty()); } +TEST_F(XStationClientTest, getAllSymbols_ok) +{ + const boost::json::object expectedCommand = {{"command", "getAllSymbols"}}; + const boost::json::object serverResponse = {{"status", true}, {"returnData", "test"}}; + + EXPECT_CALL(getMockedConnection(), makeRequest(testing::_)) + .WillOnce([&expectedCommand](const boost::json::object &command) -> boost::asio::awaitable { + EXPECT_EQ(command, expectedCommand); + co_return; + } + ); + + EXPECT_CALL(getMockedConnection(), waitResponse()) + .WillOnce([&serverResponse]() -> boost::asio::awaitable { + co_return serverResponse; + }); + + boost::json::object result; + EXPECT_NO_THROW(result = runAwaitable(client->getAllSymbols())); + EXPECT_FALSE(result.empty()); +} + TEST_F(XStationClientTest, getCalendar_exception) { - XStationClient client(getIoContext(), testAccountCredentials); + const boost::json::object expectedCommand = {{"command", "getCalendar"}}; + + EXPECT_CALL(getMockedConnection(), makeRequest(testing::_)) + .WillOnce([&expectedCommand](const boost::json::object &command) -> boost::asio::awaitable { + EXPECT_EQ(command, expectedCommand); + throw exception::ConnectionClosed("Exception"); + co_return; + } + ); + + boost::json::object result; + EXPECT_THROW(result = runAwaitable(client->getCalendar()), exception::ConnectionClosed); + EXPECT_TRUE(result.empty()); +} + +TEST_F(XStationClientTest, getCalendar_ok) +{ + const boost::json::object expectedCommand = {{"command", "getCalendar"}}; + const boost::json::object serverResponse = {{"status", true}, {"returnData", "test"}}; + + EXPECT_CALL(getMockedConnection(), makeRequest(testing::_)) + .WillOnce([&expectedCommand](const boost::json::object &command) -> boost::asio::awaitable { + EXPECT_EQ(command, expectedCommand); + co_return; + } + ); + EXPECT_CALL(getMockedConnection(), waitResponse()) + .WillOnce([&serverResponse]() -> boost::asio::awaitable { + co_return serverResponse; + }); + + boost::json::object result; + EXPECT_NO_THROW(result = runAwaitable(client->getCalendar())); + EXPECT_FALSE(result.empty()); +} + +TEST_F(XStationClientTest, getServerTime_exception) +{ + const boost::json::object expectedCommand = {{"command", "getServerTime"}}; + + EXPECT_CALL(getMockedConnection(), makeRequest(testing::_)) + .WillOnce([&expectedCommand](const boost::json::object &command) -> boost::asio::awaitable { + EXPECT_EQ(command, expectedCommand); + throw exception::ConnectionClosed("Exception"); + co_return; + } + ); + + boost::json::object result; + EXPECT_THROW(result = runAwaitable(client->getServerTime()), exception::ConnectionClosed); + EXPECT_TRUE(result.empty()); +} + +TEST_F(XStationClientTest, getServerTime_ok) +{ + const boost::json::object expectedCommand = {{"command", "getServerTime"}}; + const boost::json::object serverTimeResponse = {{"status", true}, {"returnData", "test"}}; + + EXPECT_CALL(getMockedConnection(), makeRequest(testing::_)) + .WillOnce([&expectedCommand](const boost::json::object &command) -> boost::asio::awaitable { + EXPECT_EQ(command, expectedCommand); + co_return; + } + ); + + EXPECT_CALL(getMockedConnection(), waitResponse()) + .WillOnce([&serverTimeResponse]() -> boost::asio::awaitable { + co_return serverTimeResponse; + }); + + boost::json::object result; + EXPECT_NO_THROW(result = runAwaitable(client->getServerTime())); + EXPECT_EQ(result["returnData"].as_string(), "test"); +} + +TEST_F(XStationClientTest, getTickPrices_exception) +{ + const boost::json::object expectedCommand = { + {"command", "getTickPrices"}, + {"arguments", { + {"symbols", boost::json::array({"symbol1", "symbol2"})}, + {"timestamp", 1234567890}, + {"level", 2} + }} + }; + + EXPECT_CALL(getMockedConnection(), makeRequest(testing::_)) + .WillOnce([&expectedCommand](const boost::json::object &command) -> boost::asio::awaitable { + EXPECT_EQ(command, expectedCommand); + throw exception::ConnectionClosed("Exception"); + co_return; + } + ); + + boost::json::object result; + EXPECT_THROW(result = runAwaitable(client->getTickPrices({"symbol1", "symbol2"}, 1234567890, 2)), exception::ConnectionClosed); + EXPECT_TRUE(result.empty()); +} + +TEST_F(XStationClientTest, getTickPrices_ok) +{ + const boost::json::object expectedCommand = { + {"command", "getTickPrices"}, + {"arguments", { + {"symbols", boost::json::array({"symbol1", "symbol2"})}, + {"timestamp", 1234567890}, + {"level", 2} + }} + }; + const boost::json::object serverResponse = {{"status", true}, {"returnData", "tick prices data"}}; + + EXPECT_CALL(getMockedConnection(), makeRequest(testing::_)) + .WillOnce([&expectedCommand](const boost::json::object &command) -> boost::asio::awaitable { + EXPECT_EQ(command, expectedCommand); + co_return; + } + ); + + EXPECT_CALL(getMockedConnection(), waitResponse()) + .WillOnce([&serverResponse]() -> boost::asio::awaitable { + co_return serverResponse; + }); + + boost::json::object result; + EXPECT_NO_THROW(result = runAwaitable(client->getTickPrices({"symbol1", "symbol2"}, 1234567890, 2))); + EXPECT_EQ(result["returnData"].as_string(), "tick prices data"); +} + +TEST_F(XStationClientTest, tradeTransaction_safeMode) +{ + client->setSafeMode(true); + boost::json::object result; - EXPECT_THROW(result = runAwaitable(client.getCalendar()), exception::ConnectionClosed); + EXPECT_NO_THROW(result = runAwaitable(client->tradeTransaction("EURUSD", TradeCmd::BUY, TradeType::OPEN, 1.1000f, + 1.0f, 0.0f, 0.0f, 123456, 0, 0, "Test comment"))); + + boost::json::object expectedResult; + expectedResult["status"] = false; + expectedResult["errorCode"] = "N/A"; + expectedResult["errorDescr"] = "Trading is disabled when safe=True"; + + EXPECT_EQ(result, expectedResult); +} + +TEST_F(XStationClientTest, tradeTransaction_exception) +{ + const boost::json::object expectedCommand = { + {"command", "tradeTransaction"}, + {"arguments", { + {"tradeTransInfo", { + {"cmd", 0}, + {"customComment", "test"}, + {"expiration", 0}, + {"offset", 0}, + {"order", 12345}, + {"price", 0.0}, + {"sl", 0.0}, + {"symbol", "testSymbol"}, + {"tp", 0.0}, + {"type", 0}, + {"volume", 0.0} + }} + }} + }; + + client->setSafeMode(false); + + EXPECT_CALL(getMockedConnection(), makeRequest(testing::_)) + .WillOnce([&expectedCommand](const boost::json::object &command) -> boost::asio::awaitable { + EXPECT_EQ(command, expectedCommand) << "Expected command: " << boost::json::serialize(expectedCommand) << "\n Actual command: " << boost::json::serialize(command); + throw exception::ConnectionClosed("Exception"); + co_return; + } + ); + + boost::json::object result; + EXPECT_THROW(result = runAwaitable(client->tradeTransaction("testSymbol", TradeCmd::BUY, TradeType::OPEN, \ + 0.0f, 0.0f, 0.0f, 0.0f, 12345, 0.0f, 0.0f, "test")), exception::ConnectionClosed); EXPECT_TRUE(result.empty()); } +TEST_F(XStationClientTest, tradeTransaction_ok) +{ + const boost::json::object expectedCommand = { + {"command", "tradeTransaction"}, + {"arguments", { + {"tradeTransInfo", { + {"cmd", 0}, + {"customComment", "test"}, + {"expiration", 0}, + {"offset", 0}, + {"order", 12345}, + {"price", 0.0}, + {"sl", 0.0}, + {"symbol", "testSymbol"}, + {"tp", 0.0}, + {"type", 0}, + {"volume", 0.0} + }} + }} + }; + const boost::json::object serverResponse = {{"status", true}, {"returnData", "trade result"}}; + + client->setSafeMode(false); + + EXPECT_CALL(getMockedConnection(), makeRequest(testing::_)) + .WillOnce([&expectedCommand](const boost::json::object &command) -> boost::asio::awaitable { + EXPECT_EQ(command, expectedCommand) << "Expected command: " << boost::json::serialize(expectedCommand) << "\n Actual command: " << boost::json::serialize(command); + co_return; + } + ); + + + EXPECT_CALL(getMockedConnection(), waitResponse()) + .WillOnce([&serverResponse]() -> boost::asio::awaitable { + co_return serverResponse; + }); + + boost::json::object result; + EXPECT_NO_THROW(result = runAwaitable(client->tradeTransaction("testSymbol", TradeCmd::BUY, TradeType::OPEN, \ + 0.0f, 0.0f, 0.0f, 0.0f, 12345, 0.0f, 0.0f, "test"))); + EXPECT_EQ(result["returnData"].as_string(), "trade result"); +} + +TEST_F(XStationClientTest, getChartLastRequest_ok) +{ + const std::string symbol = "AAPL"; + const int64_t start = 1633046400000; + const PeriodCode period = PeriodCode::PERIOD_H1; + + const boost::json::object expectedCommand = { + {"command", "getChartLastRequest"}, + {"arguments", { + {"info", { + {"period", static_cast(period)}, + {"start", start}, + {"symbol", symbol} + }} + }} + }; + + const boost::json::object serverResponse = { + {"status", "success"}, + {"data", {{"chartData", {}}}} + }; + + EXPECT_CALL(getMockedConnection(), makeRequest(testing::_)) + .WillOnce([&expectedCommand](const boost::json::object &command) -> boost::asio::awaitable { + EXPECT_EQ(command, expectedCommand); + co_return; + } + ); + + EXPECT_CALL(getMockedConnection(), waitResponse()) + .WillOnce([&serverResponse]() -> boost::asio::awaitable { + co_return serverResponse; + }); + + boost::json::object result; + EXPECT_NO_THROW(result = runAwaitable(client->getChartLastRequest(symbol, start, period))); + EXPECT_EQ(result["status"].as_string(), "success"); +} + TEST_F(XStationClientTest, getChartLastRequest_exception) { - XStationClient client(getIoContext(), testAccountCredentials); + const std::string symbol = "AAPL"; + const int64_t start = 1633046400000; + const PeriodCode period = PeriodCode::PERIOD_H1; + + const boost::json::object expectedCommand = { + {"command", "getChartLastRequest"}, + {"arguments", { + {"info", { + {"period", static_cast(period)}, + {"start", start}, + {"symbol", symbol} + }} + }} + }; + + EXPECT_CALL(getMockedConnection(), makeRequest(testing::_)) + .WillOnce([&expectedCommand](const boost::json::object &command) -> boost::asio::awaitable { + EXPECT_EQ(command, expectedCommand); + co_return; + } + ); + + EXPECT_CALL(getMockedConnection(), waitResponse()) + .WillOnce([]() -> boost::asio::awaitable { + throw exception::ConnectionClosed("Exception"); + }); + boost::json::object result; - EXPECT_THROW(result = runAwaitable(client.getChartLastRequest("EURUSD", 1625097600, PeriodCode::PERIOD_M1)), - exception::ConnectionClosed); + EXPECT_THROW(result = runAwaitable(client->getChartLastRequest(symbol, start, period)), exception::ConnectionClosed); EXPECT_TRUE(result.empty()); } +TEST_F(XStationClientTest, getChartRangeRequest_ok) +{ + const std::string symbol = "GOOG"; + const int64_t start = 1633046400000; + const int64_t end = 1633132800000; + const PeriodCode period = PeriodCode::PERIOD_D1; + const int ticks = 10; + + const boost::json::object expectedCommand = { + {"command", "getChartRangeRequest"}, + {"arguments", { + {"info", { + {"end", end}, + {"period", static_cast(period)}, + {"start", start}, + {"symbol", symbol}, + {"ticks", ticks} + }} + }} + }; + + const boost::json::object serverResponse = { + {"status", "success"}, + {"data", {{"chartData", {}}}} + }; + + EXPECT_CALL(getMockedConnection(), makeRequest(testing::_)) + .WillOnce([&expectedCommand](const boost::json::object &command) -> boost::asio::awaitable { + EXPECT_EQ(command, expectedCommand); + co_return; + } + ); + + EXPECT_CALL(getMockedConnection(), waitResponse()) + .WillOnce([&serverResponse]() -> boost::asio::awaitable { + co_return serverResponse; + }); + + boost::json::object result; + EXPECT_NO_THROW(result = runAwaitable(client->getChartRangeRequest(symbol, start, end, period, ticks))); + EXPECT_EQ(result["status"].as_string(), "success"); +} + TEST_F(XStationClientTest, getChartRangeRequest_exception) { - XStationClient client(getIoContext(), testAccountCredentials); + const std::string symbol = "GOOG"; + const int64_t start = 1633046400000; + const int64_t end = 1633132800000; + const PeriodCode period = PeriodCode::PERIOD_D1; + const int ticks = 10; + + EXPECT_CALL(getMockedConnection(), makeRequest(testing::_)) + .WillOnce([](const boost::json::object &command) -> boost::asio::awaitable { + co_return; + }); + + EXPECT_CALL(getMockedConnection(), waitResponse()) + .WillOnce([]() -> boost::asio::awaitable { + throw exception::ConnectionClosed("Exception"); + }); + boost::json::object result; - EXPECT_THROW( - result = runAwaitable(client.getChartRangeRequest("EURUSD", 1625097600, 1625184000, PeriodCode::PERIOD_M1, 10)), - exception::ConnectionClosed); - EXPECT_TRUE(result.empty()); + EXPECT_THROW(result = runAwaitable(client->getChartRangeRequest(symbol, start, end, period, ticks)), exception::ConnectionClosed); +} + +TEST_F(XStationClientTest, getCommissionDef_ok) +{ + const std::string symbol = "GOOG"; + const float volume = 100.0f; + + const boost::json::object expectedCommand = { + {"command", "getCommissionDef"}, + {"arguments", { + {"symbol", symbol}, + {"volume", volume} + }} + }; + + const boost::json::object serverResponse = { + {"status", "success"}, + {"commission", 5.0f} + }; + + EXPECT_CALL(getMockedConnection(), makeRequest(testing::_)) + .WillOnce([&expectedCommand](const boost::json::object &command) -> boost::asio::awaitable { + EXPECT_EQ(command, expectedCommand); + co_return; + } + ); + + EXPECT_CALL(getMockedConnection(), waitResponse()) + .WillOnce([&serverResponse]() -> boost::asio::awaitable { + co_return serverResponse; + }); + + boost::json::object result; + EXPECT_NO_THROW(result = runAwaitable(client->getCommissionDef(symbol, volume))); + EXPECT_EQ(result["status"].as_string(), "success"); + EXPECT_EQ(result["commission"].as_double(), 5.0f); } TEST_F(XStationClientTest, getCommissionDef_exception) { - XStationClient client(getIoContext(), testAccountCredentials); + const std::string symbol = "GOOG"; + const float volume = 100.0f; + + EXPECT_CALL(getMockedConnection(), makeRequest(testing::_)) + .WillOnce([](const boost::json::object &command) -> boost::asio::awaitable { + co_return; + }); + + EXPECT_CALL(getMockedConnection(), waitResponse()) + .WillOnce([]() -> boost::asio::awaitable { + throw exception::ConnectionClosed("Exception"); + }); + boost::json::object result; - EXPECT_THROW(result = runAwaitable(client.getCommissionDef("EURUSD", 1.0f)), exception::ConnectionClosed); - EXPECT_TRUE(result.empty()); + EXPECT_THROW(result = runAwaitable(client->getCommissionDef(symbol, volume)), exception::ConnectionClosed); +} + +TEST_F(XStationClientTest, getCurrentUserData_ok) +{ + const boost::json::object expectedCommand = { + {"command", "getCurrentUserData"} + }; + + const boost::json::object serverResponse = { + {"status", "success"}, + {"userData", {{"name", "John Doe"}, {"account", "12345"}}} + }; + + EXPECT_CALL(getMockedConnection(), makeRequest(testing::_)) + .WillOnce([&expectedCommand](const boost::json::object &command) -> boost::asio::awaitable { + EXPECT_EQ(command, expectedCommand); + co_return; + } + ); + + EXPECT_CALL(getMockedConnection(), waitResponse()) + .WillOnce([&serverResponse]() -> boost::asio::awaitable { + co_return serverResponse; + }); + + boost::json::object result; + EXPECT_NO_THROW(result = runAwaitable(client->getCurrentUserData())); + EXPECT_FALSE(result.empty()); } TEST_F(XStationClientTest, getCurrentUserData_exception) { - XStationClient client(getIoContext(), testAccountCredentials); + EXPECT_CALL(getMockedConnection(), makeRequest(testing::_)) + .WillOnce([](const boost::json::object &command) -> boost::asio::awaitable { + co_return; + }); + + EXPECT_CALL(getMockedConnection(), waitResponse()) + .WillOnce([]() -> boost::asio::awaitable { + throw exception::ConnectionClosed("Exception"); + }); + boost::json::object result; - EXPECT_THROW(result = runAwaitable(client.getCurrentUserData()), exception::ConnectionClosed); - EXPECT_TRUE(result.empty()); + EXPECT_THROW(result = runAwaitable(client->getCurrentUserData()), exception::ConnectionClosed); +} + +TEST_F(XStationClientTest, getIbsHistory_ok) +{ + const std::int64_t start = 1633046400000; + const std::int64_t end = 1633132800000; + + const boost::json::object expectedCommand = { + {"command", "getIbsHistory"}, + {"arguments", { + {"start", start}, + {"end", end} + }} + }; + + const boost::json::object serverResponse = { + {"status", "success"}, + {"historyData", {{"dataPoint1", 123}, {"dataPoint2", 456}}} + }; + + EXPECT_CALL(getMockedConnection(), makeRequest(testing::_)) + .WillOnce([&expectedCommand](const boost::json::object &command) -> boost::asio::awaitable { + EXPECT_EQ(command, expectedCommand); + co_return; + } + ); + + EXPECT_CALL(getMockedConnection(), waitResponse()) + .WillOnce([&serverResponse]() -> boost::asio::awaitable { + co_return serverResponse; + }); + + boost::json::object result; + EXPECT_NO_THROW(result = runAwaitable(client->getIbsHistory(start, end))); + EXPECT_FALSE(result.empty()); } TEST_F(XStationClientTest, getIbsHistory_exception) { - XStationClient client(getIoContext(), testAccountCredentials); + const std::int64_t start = 1633046400000; + const std::int64_t end = 1633132800000; + + EXPECT_CALL(getMockedConnection(), makeRequest(testing::_)) + .WillOnce([](const boost::json::object &command) -> boost::asio::awaitable { + co_return; + }); + + EXPECT_CALL(getMockedConnection(), waitResponse()) + .WillOnce([]() -> boost::asio::awaitable { + throw exception::ConnectionClosed("Exception"); + }); + boost::json::object result; - EXPECT_THROW(result = runAwaitable(client.getIbsHistory(1625097600, 1625184000)), exception::ConnectionClosed); - EXPECT_TRUE(result.empty()); + EXPECT_THROW(result = runAwaitable(client->getIbsHistory(start, end)), exception::ConnectionClosed); +} + +TEST_F(XStationClientTest, getMarginLevel_ok) +{ + const boost::json::object expectedCommand = { + {"command", "getMarginLevel"} + }; + + const boost::json::object serverResponse = { + {"status", "success"}, + {"marginLevel", 50.0} + }; + + EXPECT_CALL(getMockedConnection(), makeRequest(testing::_)) + .WillOnce([&expectedCommand](const boost::json::object &command) -> boost::asio::awaitable { + EXPECT_EQ(command, expectedCommand); + co_return; + } + ); + + EXPECT_CALL(getMockedConnection(), waitResponse()) + .WillOnce([&serverResponse]() -> boost::asio::awaitable { + co_return serverResponse; + }); + + boost::json::object result; + EXPECT_NO_THROW(result = runAwaitable(client->getMarginLevel())); + EXPECT_FALSE(result.empty()); } TEST_F(XStationClientTest, getMarginLevel_exception) { - XStationClient client(getIoContext(), testAccountCredentials); + EXPECT_CALL(getMockedConnection(), makeRequest(testing::_)) + .WillOnce([](const boost::json::object &command) -> boost::asio::awaitable { + co_return; + }); + + EXPECT_CALL(getMockedConnection(), waitResponse()) + .WillOnce([]() -> boost::asio::awaitable { + throw exception::ConnectionClosed("Exception"); + }); + boost::json::object result; - EXPECT_THROW(result = runAwaitable(client.getMarginLevel()), exception::ConnectionClosed); - EXPECT_TRUE(result.empty()); + EXPECT_THROW(result = runAwaitable(client->getMarginLevel()), exception::ConnectionClosed); +} + +TEST_F(XStationClientTest, getMarginTrade_ok) +{ + const std::string symbol = "GOOG"; + const float volume = 100.0f; + + const boost::json::object expectedCommand = { + {"command", "getMarginTrade"}, + {"arguments", { + {"symbol", symbol}, + {"volume", volume} + }} + }; + + const boost::json::object serverResponse = { + {"status", "success"}, + {"margin", 2000.0} + }; + + EXPECT_CALL(getMockedConnection(), makeRequest(testing::_)) + .WillOnce([&expectedCommand](const boost::json::object &command) -> boost::asio::awaitable { + EXPECT_EQ(command, expectedCommand); + co_return; + } + ); + + EXPECT_CALL(getMockedConnection(), waitResponse()) + .WillOnce([&serverResponse]() -> boost::asio::awaitable { + co_return serverResponse; + }); + + boost::json::object result; + EXPECT_NO_THROW(result = runAwaitable(client->getMarginTrade(symbol, volume))); + EXPECT_FALSE(result.empty()); } TEST_F(XStationClientTest, getMarginTrade_exception) { - XStationClient client(getIoContext(), testAccountCredentials); + const std::string symbol = "GOOG"; + const float volume = 100.0f; + + EXPECT_CALL(getMockedConnection(), makeRequest(testing::_)) + .WillOnce([](const boost::json::object &command) -> boost::asio::awaitable { + co_return; + }); + + EXPECT_CALL(getMockedConnection(), waitResponse()) + .WillOnce([]() -> boost::asio::awaitable { + throw exception::ConnectionClosed("Exception"); + }); + boost::json::object result; - EXPECT_THROW(result = runAwaitable(client.getMarginTrade("EURUSD", 1.0f)), exception::ConnectionClosed); - EXPECT_TRUE(result.empty()); + EXPECT_THROW(result = runAwaitable(client->getMarginTrade(symbol, volume)), exception::ConnectionClosed); +} + +TEST_F(XStationClientTest, getNews_ok) +{ + const std::int64_t start = 1633046400000; + const std::int64_t end = 1633132800000; + + const boost::json::object expectedCommand = { + {"command", "getNews"}, + {"arguments", { + {"start", start}, + {"end", end} + }} + }; + + const boost::json::object serverResponse = { + {"status", "success"}, + {"news", {{"headline", "Breaking news!"}}} + }; + + EXPECT_CALL(getMockedConnection(), makeRequest(testing::_)) + .WillOnce([&expectedCommand](const boost::json::object &command) -> boost::asio::awaitable { + EXPECT_EQ(command, expectedCommand); + co_return; + } + ); + + EXPECT_CALL(getMockedConnection(), waitResponse()) + .WillOnce([&serverResponse]() -> boost::asio::awaitable { + co_return serverResponse; + }); + + boost::json::object result; + EXPECT_NO_THROW(result = runAwaitable(client->getNews(start, end))); + EXPECT_FALSE(result.empty()); } TEST_F(XStationClientTest, getNews_exception) { - XStationClient client(getIoContext(), testAccountCredentials); + const std::int64_t start = 1633046400000; + const std::int64_t end = 1633132800000; + + EXPECT_CALL(getMockedConnection(), makeRequest(testing::_)) + .WillOnce([](const boost::json::object &command) -> boost::asio::awaitable { + co_return; + }); + + EXPECT_CALL(getMockedConnection(), waitResponse()) + .WillOnce([]() -> boost::asio::awaitable { + throw exception::ConnectionClosed("Exception"); + }); + boost::json::object result; - EXPECT_THROW(result = runAwaitable(client.getNews(1625097600, 1625184000)), exception::ConnectionClosed); - EXPECT_TRUE(result.empty()); + EXPECT_THROW(result = runAwaitable(client->getNews(start, end)), exception::ConnectionClosed); +} + +TEST_F(XStationClientTest, getProfitCalculation_ok) +{ + const std::string symbol = "GOOG"; + const int cmd = 1; + const float openPrice = 1500.0f; + const float closePrice = 1550.0f; + const float volume = 100.0f; + + const boost::json::object expectedCommand = { + {"command", "getProfitCalculation"}, + {"arguments", { + {"symbol", symbol}, + {"cmd", cmd}, + {"openPrice", openPrice}, + {"closePrice", closePrice}, + {"volume", volume} + }} + }; + + const boost::json::object serverResponse = { + {"status", "success"}, + {"profit", 5000.0} + }; + + EXPECT_CALL(getMockedConnection(), makeRequest(testing::_)) + .WillOnce([&expectedCommand](const boost::json::object &command) -> boost::asio::awaitable { + EXPECT_EQ(command, expectedCommand); + co_return; + } + ); + + EXPECT_CALL(getMockedConnection(), waitResponse()) + .WillOnce([&serverResponse]() -> boost::asio::awaitable { + co_return serverResponse; + }); + + boost::json::object result; + EXPECT_NO_THROW(result = runAwaitable(client->getProfitCalculation(symbol, cmd, openPrice, closePrice, volume))); + EXPECT_FALSE(result.empty()); } TEST_F(XStationClientTest, getProfitCalculation_exception) { - XStationClient client(getIoContext(), testAccountCredentials); + const std::string symbol = "GOOG"; + const int cmd = 1; + const float openPrice = 1500.0f; + const float closePrice = 1550.0f; + const float volume = 100.0f; + + EXPECT_CALL(getMockedConnection(), makeRequest(testing::_)) + .WillOnce([](const boost::json::object &command) -> boost::asio::awaitable { + co_return; + }); + + EXPECT_CALL(getMockedConnection(), waitResponse()) + .WillOnce([]() -> boost::asio::awaitable { + throw exception::ConnectionClosed("Exception"); + }); + boost::json::object result; - EXPECT_THROW(result = runAwaitable(client.getProfitCalculation("EURUSD", 0, 1.1000f, 1.1050f, 1.0f)), - exception::ConnectionClosed); - EXPECT_TRUE(result.empty()); + EXPECT_THROW(result = runAwaitable(client->getProfitCalculation(symbol, cmd, openPrice, closePrice, volume)), exception::ConnectionClosed); } -TEST_F(XStationClientTest, getServerTime_exception) +TEST_F(XStationClientTest, getStepRules_ok) { - XStationClient client(getIoContext(), testAccountCredentials); + const boost::json::object expectedCommand = { + {"command", "getStepRules"} + }; + + const boost::json::object serverResponse = { + {"status", "success"}, + {"data", {{"stepRules", {}}}} + }; + + EXPECT_CALL(getMockedConnection(), makeRequest(testing::_)) + .WillOnce([&expectedCommand](const boost::json::object &command) -> boost::asio::awaitable { + EXPECT_EQ(command, expectedCommand); + co_return; + } + ); + + EXPECT_CALL(getMockedConnection(), waitResponse()) + .WillOnce([&serverResponse]() -> boost::asio::awaitable { + co_return serverResponse; + }); + boost::json::object result; - EXPECT_THROW(result = runAwaitable(client.getServerTime()), exception::ConnectionClosed); - EXPECT_TRUE(result.empty()); + EXPECT_NO_THROW(result = runAwaitable(client->getStepRules())); + EXPECT_FALSE(result.empty()); } TEST_F(XStationClientTest, getStepRules_exception) { - XStationClient client(getIoContext(), testAccountCredentials); + EXPECT_CALL(getMockedConnection(), makeRequest(testing::_)) + .WillOnce([](const boost::json::object &command) -> boost::asio::awaitable { + co_return; + }); + + EXPECT_CALL(getMockedConnection(), waitResponse()) + .WillOnce([]() -> boost::asio::awaitable { + throw exception::ConnectionClosed("Exception"); + }); + boost::json::object result; - EXPECT_THROW(result = runAwaitable(client.getStepRules()), exception::ConnectionClosed); - EXPECT_TRUE(result.empty()); + EXPECT_THROW(result = runAwaitable(client->getStepRules()), exception::ConnectionClosed); +} + +TEST_F(XStationClientTest, getSymbol_ok) +{ + const std::string symbol = "AAPL"; + const boost::json::object expectedCommand = { + {"command", "getSymbol"}, + {"arguments", { + {"symbol", symbol} + }} + }; + + const boost::json::object serverResponse = { + {"status", "success"}, + {"symbol", symbol} + }; + + EXPECT_CALL(getMockedConnection(), makeRequest(testing::_)) + .WillOnce([&expectedCommand](const boost::json::object &command) -> boost::asio::awaitable { + EXPECT_EQ(command, expectedCommand); + co_return; + } + ); + + EXPECT_CALL(getMockedConnection(), waitResponse()) + .WillOnce([&serverResponse]() -> boost::asio::awaitable { + co_return serverResponse; + }); + + boost::json::object result; + EXPECT_NO_THROW(result = runAwaitable(client->getSymbol(symbol))); + EXPECT_FALSE(result.empty()); } TEST_F(XStationClientTest, getSymbol_exception) { - XStationClient client(getIoContext(), testAccountCredentials); + const std::string symbol = "AAPL"; + + EXPECT_CALL(getMockedConnection(), makeRequest(testing::_)) + .WillOnce([](const boost::json::object &command) -> boost::asio::awaitable { + co_return; + }); + + EXPECT_CALL(getMockedConnection(), waitResponse()) + .WillOnce([]() -> boost::asio::awaitable { + throw exception::ConnectionClosed("Exception"); + }); + boost::json::object result; - EXPECT_THROW(result = runAwaitable(client.getSymbol("EURUSD")), exception::ConnectionClosed); - EXPECT_TRUE(result.empty()); + EXPECT_THROW(result = runAwaitable(client->getSymbol(symbol)), exception::ConnectionClosed); } -TEST_F(XStationClientTest, getTickPrices_exception) +TEST_F(XStationClientTest, getTradeRecords_ok) { - XStationClient client(getIoContext(), testAccountCredentials); + const std::vector orders = {12345, 67890}; + const boost::json::object expectedCommand = { + {"command", "getTradeRecords"}, + {"arguments", { + {"orders", boost::json::array(orders.begin(), orders.end())} + }} + }; + + const boost::json::object serverResponse = { + {"status", "success"}, + {"data", {{"tradeRecords", {}}}} + }; + + EXPECT_CALL(getMockedConnection(), makeRequest(testing::_)) + .WillOnce([&expectedCommand](const boost::json::object &command) -> boost::asio::awaitable { + EXPECT_EQ(command, expectedCommand); + co_return; + } + ); + + EXPECT_CALL(getMockedConnection(), waitResponse()) + .WillOnce([&serverResponse]() -> boost::asio::awaitable { + co_return serverResponse; + }); + boost::json::object result; - EXPECT_THROW(result = runAwaitable(client.getTickPrices({"EURUSD", "GBPUSD"}, 1625097600, 1)), - exception::ConnectionClosed); - EXPECT_TRUE(result.empty()); + EXPECT_NO_THROW(result = runAwaitable(client->getTradeRecords(orders))); + EXPECT_FALSE(result.empty()); } TEST_F(XStationClientTest, getTradeRecords_exception) { - XStationClient client(getIoContext(), testAccountCredentials); + const std::vector orders = {12345, 67890}; + + EXPECT_CALL(getMockedConnection(), makeRequest(testing::_)) + .WillOnce([](const boost::json::object &command) -> boost::asio::awaitable { + co_return; + }); + + EXPECT_CALL(getMockedConnection(), waitResponse()) + .WillOnce([]() -> boost::asio::awaitable { + throw exception::ConnectionClosed("Exception"); + }); + boost::json::object result; - EXPECT_THROW(result = runAwaitable(client.getTradeRecords({123456, 789012})), exception::ConnectionClosed); - EXPECT_TRUE(result.empty()); + EXPECT_THROW(result = runAwaitable(client->getTradeRecords(orders)), exception::ConnectionClosed); +} + +TEST_F(XStationClientTest, getTrades_ok) +{ + const bool openedOnly = true; + const boost::json::object expectedCommand = { + {"command", "getTrades"}, + {"arguments", { + {"openedOnly", openedOnly} + }} + }; + + const boost::json::object serverResponse = { + {"status", "success"}, + {"data", {{"trades", {}}}} + }; + + EXPECT_CALL(getMockedConnection(), makeRequest(testing::_)) + .WillOnce([&expectedCommand](const boost::json::object &command) -> boost::asio::awaitable { + EXPECT_EQ(command, expectedCommand); + co_return; + } + ); + + EXPECT_CALL(getMockedConnection(), waitResponse()) + .WillOnce([&serverResponse]() -> boost::asio::awaitable { + co_return serverResponse; + }); + + boost::json::object result; + EXPECT_NO_THROW(result = runAwaitable(client->getTrades(openedOnly))); + EXPECT_FALSE(result.empty()); } TEST_F(XStationClientTest, getTrades_exception) { - XStationClient client(getIoContext(), testAccountCredentials); + const bool openedOnly = true; + + EXPECT_CALL(getMockedConnection(), makeRequest(testing::_)) + .WillOnce([](const boost::json::object &command) -> boost::asio::awaitable { + co_return; + }); + + EXPECT_CALL(getMockedConnection(), waitResponse()) + .WillOnce([]() -> boost::asio::awaitable { + throw exception::ConnectionClosed("Exception"); + }); + boost::json::object result; - EXPECT_THROW(result = runAwaitable(client.getTrades(true)), exception::ConnectionClosed); - EXPECT_TRUE(result.empty()); + EXPECT_THROW(result = runAwaitable(client->getTrades(openedOnly)), exception::ConnectionClosed); } -TEST_F(XStationClientTest, getTradesHistory_exception) +TEST_F(XStationClientTest, getTradesHistory_ok) { - XStationClient client(getIoContext(), testAccountCredentials); + const std::int64_t start = 1633046400000; + const std::int64_t end = 1633132800000; + const boost::json::object expectedCommand = { + {"command", "getTradesHistory"}, + {"arguments", { + {"start", start}, + {"end", end} + }} + }; + + const boost::json::object serverResponse = { + {"status", "success"}, + {"data", {{"tradeHistory", {}}}} + }; + + EXPECT_CALL(getMockedConnection(), makeRequest(testing::_)) + .WillOnce([&expectedCommand](const boost::json::object &command) -> boost::asio::awaitable { + EXPECT_EQ(command, expectedCommand); + co_return; + } + ); + + EXPECT_CALL(getMockedConnection(), waitResponse()) + .WillOnce([&serverResponse]() -> boost::asio::awaitable { + co_return serverResponse; + }); + boost::json::object result; - EXPECT_THROW(result = runAwaitable(client.getTradesHistory(1625097600, 1625184000)), exception::ConnectionClosed); - EXPECT_TRUE(result.empty()); + EXPECT_NO_THROW(result = runAwaitable(client->getTradesHistory(start, end))); + EXPECT_FALSE(result.empty()); } -TEST_F(XStationClientTest, getTradingHours_exception) +TEST_F(XStationClientTest, getTradesHistory_exception) { - XStationClient client(getIoContext(), testAccountCredentials); + const std::int64_t start = 1633046400000; + const std::int64_t end = 1633132800000; + + EXPECT_CALL(getMockedConnection(), makeRequest(testing::_)) + .WillOnce([](const boost::json::object &command) -> boost::asio::awaitable { + co_return; + }); + + EXPECT_CALL(getMockedConnection(), waitResponse()) + .WillOnce([]() -> boost::asio::awaitable { + throw exception::ConnectionClosed("Exception"); + }); + boost::json::object result; - EXPECT_THROW(result = runAwaitable(client.getTradingHours({"EURUSD", "GBPUSD"})), exception::ConnectionClosed); - EXPECT_TRUE(result.empty()); + EXPECT_THROW(result = runAwaitable(client->getTradesHistory(start, end)), exception::ConnectionClosed); } -TEST_F(XStationClientTest, getVersion_exception) +TEST_F(XStationClientTest, getTradingHours_ok) { - XStationClient client(getIoContext(), testAccountCredentials); + const std::vector symbols = {"AAPL", "GOOG"}; + const boost::json::object expectedCommand = { + {"command", "getTradingHours"}, + {"arguments", { + {"symbols", boost::json::array(symbols.begin(), symbols.end())} + }} + }; + + const boost::json::object serverResponse = { + {"status", "success"}, + {"data", {{"tradingHours", {}}}} + }; + + EXPECT_CALL(getMockedConnection(), makeRequest(testing::_)) + .WillOnce([&expectedCommand](const boost::json::object &command) -> boost::asio::awaitable { + EXPECT_EQ(command, expectedCommand); + co_return; + } + ); + + EXPECT_CALL(getMockedConnection(), waitResponse()) + .WillOnce([&serverResponse]() -> boost::asio::awaitable { + co_return serverResponse; + }); + boost::json::object result; - EXPECT_THROW(result = runAwaitable(client.getVersion()), exception::ConnectionClosed); - EXPECT_TRUE(result.empty()); + EXPECT_NO_THROW(result = runAwaitable(client->getTradingHours(symbols))); + EXPECT_FALSE(result.empty()); } -TEST_F(XStationClientTest, ping_exception) +TEST_F(XStationClientTest, getTradingHours_exception) { - XStationClient client(getIoContext(), testAccountCredentials); + const std::vector symbols = {"AAPL", "GOOG"}; + + EXPECT_CALL(getMockedConnection(), makeRequest(testing::_)) + .WillOnce([](const boost::json::object &command) -> boost::asio::awaitable { + co_return; + }); + + EXPECT_CALL(getMockedConnection(), waitResponse()) + .WillOnce([]() -> boost::asio::awaitable { + throw exception::ConnectionClosed("Exception"); + }); + boost::json::object result; - EXPECT_THROW(result = runAwaitable(client.ping()), exception::ConnectionClosed); - EXPECT_TRUE(result.empty()); + EXPECT_THROW(result = runAwaitable(client->getTradingHours(symbols)), exception::ConnectionClosed); } -TEST_F(XStationClientTest, tradeTransaction_safeMode_exception) +TEST_F(XStationClientTest, getVersion_ok) { - XStationClient client(getIoContext(), testAccountCredentials); + const boost::json::object expectedCommand = { + {"command", "getVersion"} + }; + + const boost::json::object serverResponse = { + {"status", "success"}, + {"data", {{"version", "1.0.0"}}} + }; + + EXPECT_CALL(getMockedConnection(), makeRequest(testing::_)) + .WillOnce([&expectedCommand](const boost::json::object &command) -> boost::asio::awaitable { + EXPECT_EQ(command, expectedCommand); + co_return; + } + ); + + EXPECT_CALL(getMockedConnection(), waitResponse()) + .WillOnce([&serverResponse]() -> boost::asio::awaitable { + co_return serverResponse; + }); + boost::json::object result; - EXPECT_NO_THROW(result = runAwaitable(client.tradeTransaction("EURUSD", TradeCmd::BUY, TradeType::OPEN, 1.1000f, - 1.0f, 0.0f, 0.0f, 123456, 0, 0, "Test comment"))); + EXPECT_NO_THROW(result = runAwaitable(client->getVersion())); + EXPECT_FALSE(result.empty()); +} - boost::json::object expectedResult; - expectedResult["status"] = false; - expectedResult["errorCode"] = "N/A"; - expectedResult["errorDescr"] = "Trading is disabled when safe=True"; +TEST_F(XStationClientTest, getVersion_exception) +{ + EXPECT_CALL(getMockedConnection(), makeRequest(testing::_)) + .WillOnce([](const boost::json::object &command) -> boost::asio::awaitable { + co_return; + }); - EXPECT_EQ(result, expectedResult); + EXPECT_CALL(getMockedConnection(), waitResponse()) + .WillOnce([]() -> boost::asio::awaitable { + throw exception::ConnectionClosed("Exception"); + }); + + boost::json::object result; + EXPECT_THROW(result = runAwaitable(client->getVersion()), exception::ConnectionClosed); } -TEST_F(XStationClientTest, tradeTransaction_exception) +TEST_F(XStationClientTest, tradeTransactionStatus_ok) { - XStationClient client(getIoContext(), testAccountCredentials); - client.setSafeMode(false); + const int order = 12345; + const boost::json::object expectedCommand = { + {"command", "tradeTransactionStatus"}, + {"arguments", { + {"order", order} + }} + }; + + const boost::json::object serverResponse = { + {"status", "success"}, + {"data", {{"status", "completed"}}} + }; + + EXPECT_CALL(getMockedConnection(), makeRequest(testing::_)) + .WillOnce([&expectedCommand](const boost::json::object &command) -> boost::asio::awaitable { + EXPECT_EQ(command, expectedCommand); + co_return; + } + ); + + EXPECT_CALL(getMockedConnection(), waitResponse()) + .WillOnce([&serverResponse]() -> boost::asio::awaitable { + co_return serverResponse; + }); boost::json::object result; - EXPECT_THROW(result = runAwaitable(client.tradeTransaction("EURUSD", TradeCmd::BUY, TradeType::OPEN, 1.1000f, 1.0f, - 0.0f, 0.0f, 123456, 0, 0, "Test comment")), - exception::ConnectionClosed); - EXPECT_TRUE(result.empty()); + EXPECT_NO_THROW(result = runAwaitable(client->tradeTransactionStatus(order))); + EXPECT_FALSE(result.empty()); } TEST_F(XStationClientTest, tradeTransactionStatus_exception) { - XStationClient client(getIoContext(), testAccountCredentials); + const int order = 12345; + + EXPECT_CALL(getMockedConnection(), makeRequest(testing::_)) + .WillOnce([](const boost::json::object &command) -> boost::asio::awaitable { + co_return; + }); + + EXPECT_CALL(getMockedConnection(), waitResponse()) + .WillOnce([]() -> boost::asio::awaitable { + throw exception::ConnectionClosed("Exception"); + }); + boost::json::object result; - EXPECT_THROW(result = runAwaitable(client.tradeTransactionStatus(123456)), exception::ConnectionClosed); - EXPECT_TRUE(result.empty()); + EXPECT_THROW(result = runAwaitable(client->tradeTransactionStatus(order)), exception::ConnectionClosed); } + +} // namespace xapi diff --git a/test/TestXStationClientStream.cpp b/test/TestXStationClientStream.cpp index 2f8b919..48070d4 100644 --- a/test/TestXStationClientStream.cpp +++ b/test/TestXStationClientStream.cpp @@ -1,22 +1,33 @@ +#include "MockConnection.hpp" #include "xapi/Exceptions.hpp" #include "xapi/XStationClientStream.hpp" #include #include -using namespace xapi; +namespace xapi +{ -class StreamTest : public testing::Test +class XStationClientStreamTest : public ::testing::Test { protected: + std::unique_ptr stream; void SetUp() override { + stream = std::make_unique(m_context, "demo", "testStreamSessionId"); + auto connection = std::make_unique(); + EXPECT_NO_THROW(stream->m_connection = std::move(connection)); } void TearDown() override { + stream.reset(); + } + + MockConnection &getMockedConnection() + { + return *dynamic_cast(stream->m_connection.get()); } - public: boost::asio::io_context &getIoContext() { return m_context; @@ -81,114 +92,537 @@ class StreamTest : public testing::Test boost::asio::io_context m_context; }; +TEST(XStationClientStreamConstructorTest, constructor) +{ + boost::asio::io_context ioContext; + EXPECT_NO_THROW(XStationClientStream stream(ioContext, "demo", "streamSessionId")); +} + +TEST_F(XStationClientStreamTest, open_ok) +{ + EXPECT_CALL(getMockedConnection(), connect(testing::_)) + .WillOnce([](const boost::url &url) -> boost::asio::awaitable { + EXPECT_EQ(url.buffer(), "wss://ws.xtb.com/demoStream"); + co_return; + }); + + EXPECT_NO_THROW(runAwaitableVoid(stream->open())); +} + +TEST_F(XStationClientStreamTest, open_exception) +{ + EXPECT_CALL(getMockedConnection(), connect(::testing::_)) + .WillOnce([](const boost::url &url) -> boost::asio::awaitable { + throw exception::ConnectionClosed("Exception"); + }); + + EXPECT_THROW(runAwaitableVoid(stream->open()), exception::ConnectionClosed); +} + +TEST_F(XStationClientStreamTest, close_ok) +{ + EXPECT_CALL(getMockedConnection(), disconnect()) + .WillOnce([]() -> boost::asio::awaitable { + co_return; + }); + + EXPECT_NO_THROW(runAwaitableVoid(stream->close())); +} + +TEST_F(XStationClientStreamTest, close_exception) +{ + EXPECT_CALL(getMockedConnection(), disconnect()) + .WillOnce([]() -> boost::asio::awaitable { + throw exception::ConnectionClosed("Exception"); + }); + + EXPECT_THROW(runAwaitableVoid(stream->close()), exception::ConnectionClosed); +} -TEST_F(StreamTest, listen_exception) +TEST_F(XStationClientStreamTest, listen_ok) { - XStationClientStream stream(getIoContext(), "demo", ""); - + const boost::json::object serverResponse = { + {"status", true} + }; + + EXPECT_CALL(getMockedConnection(), waitResponse()) + .WillOnce([&serverResponse]() -> boost::asio::awaitable { + co_return serverResponse; + }); + boost::json::object result; - EXPECT_THROW(result = runAwaitable(stream.listen()), exception::ConnectionClosed); + EXPECT_NO_THROW(result = runAwaitable(stream->listen())); + EXPECT_EQ(result, serverResponse); +} + +TEST_F(XStationClientStreamTest, listen_exception) +{ + EXPECT_CALL(getMockedConnection(), waitResponse()) + .WillOnce([]() -> boost::asio::awaitable { + throw exception::ConnectionClosed("Exception"); + }); + + boost::json::object result; + EXPECT_THROW(result = runAwaitable(stream->listen()), exception::ConnectionClosed); EXPECT_TRUE(result.empty()); } -TEST_F(StreamTest, getBalance_exception) +TEST_F(XStationClientStreamTest, getBalance_ok) { - XStationClientStream stream(getIoContext(), "demo", ""); - EXPECT_THROW(runAwaitableVoid(stream.getBalance()), exception::ConnectionClosed); + const boost::json::object expectedCommand = { + {"command", "getBalance"}, + {"streamSessionId", "testStreamSessionId"} + }; + + EXPECT_CALL(getMockedConnection(), makeRequest(testing::_)) + .WillOnce([&expectedCommand](const boost::json::object &command) -> boost::asio::awaitable { + EXPECT_EQ(command, expectedCommand); + co_return; + }); + + EXPECT_NO_THROW(runAwaitableVoid(stream->getBalance())); } -TEST_F(StreamTest, stopBalance_exception) +TEST_F(XStationClientStreamTest, getBalance_exception) { - XStationClientStream stream(getIoContext(), "demo", ""); - EXPECT_THROW(runAwaitableVoid(stream.stopBalance()), exception::ConnectionClosed); + EXPECT_CALL(getMockedConnection(), makeRequest(testing::_)) + .WillOnce([](const boost::json::object &) -> boost::asio::awaitable { + throw exception::ConnectionClosed("Exception"); + }); + + EXPECT_THROW(runAwaitableVoid(stream->getBalance()), exception::ConnectionClosed); } -TEST_F(StreamTest, getCandles_exception) +TEST_F(XStationClientStreamTest, stopBalance_ok) { - XStationClientStream stream(getIoContext(), "demo", ""); - EXPECT_THROW(runAwaitableVoid(stream.getCandles("EURUSD")), exception::ConnectionClosed); + const boost::json::object expectedCommand = { + {"command", "stopBalance"} + }; + + EXPECT_CALL(getMockedConnection(), makeRequest(testing::_)) + .WillOnce([&expectedCommand](const boost::json::object &command) -> boost::asio::awaitable { + EXPECT_EQ(command, expectedCommand); + co_return; + }); + + EXPECT_NO_THROW(runAwaitableVoid(stream->stopBalance())); } -TEST_F(StreamTest, stopCandles_exception) +TEST_F(XStationClientStreamTest, stopBalance_exception) { - XStationClientStream stream(getIoContext(), "demo", ""); - EXPECT_THROW(runAwaitableVoid(stream.stopCandles("EURUSD")), exception::ConnectionClosed); + EXPECT_CALL(getMockedConnection(), makeRequest(testing::_)) + .WillOnce([](const boost::json::object &) -> boost::asio::awaitable { + throw exception::ConnectionClosed("Exception"); + }); + + EXPECT_THROW(runAwaitableVoid(stream->stopBalance()), exception::ConnectionClosed); } -TEST_F(StreamTest, getKeepAlive_exception) +TEST_F(XStationClientStreamTest, getCandles_ok) { - XStationClientStream stream(getIoContext(), "demo", ""); - EXPECT_THROW(runAwaitableVoid(stream.getKeepAlive()), exception::ConnectionClosed); + const std::string symbol = "EURUSD"; + const boost::json::object expectedCommand = { + {"command", "getCandles"}, + {"streamSessionId", "testStreamSessionId"}, + {"symbol", symbol} + }; + + EXPECT_CALL(getMockedConnection(), makeRequest(testing::_)) + .WillOnce([&expectedCommand](const boost::json::object &command) -> boost::asio::awaitable { + EXPECT_EQ(command, expectedCommand); + co_return; + }); + + EXPECT_NO_THROW(runAwaitableVoid(stream->getCandles(symbol))); } -TEST_F(StreamTest, stopKeepAlive_exception) +TEST_F(XStationClientStreamTest, getCandles_exception) { - XStationClientStream stream(getIoContext(), "demo", ""); - EXPECT_THROW(runAwaitableVoid(stream.stopKeepAlive()), exception::ConnectionClosed); + const std::string symbol = "EURUSD"; + + EXPECT_CALL(getMockedConnection(), makeRequest(testing::_)) + .WillOnce([](const boost::json::object &) -> boost::asio::awaitable { + throw exception::ConnectionClosed("Exception"); + }); + + EXPECT_THROW(runAwaitableVoid(stream->getCandles(symbol)), exception::ConnectionClosed); } -TEST_F(StreamTest, getNews_exception) +TEST_F(XStationClientStreamTest, stopCandles_ok) { - XStationClientStream stream(getIoContext(), "demo", ""); - EXPECT_THROW(runAwaitableVoid(stream.getNews()), exception::ConnectionClosed); + const std::string symbol = "EURUSD"; + const boost::json::object expectedCommand = { + {"command", "stopCandles"}, + {"symbol", symbol} + }; + + EXPECT_CALL(getMockedConnection(), makeRequest(testing::_)) + .WillOnce([&expectedCommand](const boost::json::object &command) -> boost::asio::awaitable { + EXPECT_EQ(command, expectedCommand); + co_return; + }); + + EXPECT_NO_THROW(runAwaitableVoid(stream->stopCandles(symbol))); } -TEST_F(StreamTest, stopNews_exception) +TEST_F(XStationClientStreamTest, stopCandles_exception) { - XStationClientStream stream(getIoContext(), "demo", ""); - EXPECT_THROW(runAwaitableVoid(stream.stopNews()), exception::ConnectionClosed); + const std::string symbol = "EURUSD"; + + EXPECT_CALL(getMockedConnection(), makeRequest(testing::_)) + .WillOnce([](const boost::json::object &) -> boost::asio::awaitable { + throw exception::ConnectionClosed("Exception"); + }); + + EXPECT_THROW(runAwaitableVoid(stream->stopCandles(symbol)), exception::ConnectionClosed); } -TEST_F(StreamTest, getProfits_exception) +TEST_F(XStationClientStreamTest, getKeepAlive_ok) { - XStationClientStream stream(getIoContext(), "demo", ""); - EXPECT_THROW(runAwaitableVoid(stream.getProfits()), exception::ConnectionClosed); + const boost::json::object expectedCommand = { + {"command", "getKeepAlive"}, + {"streamSessionId", "testStreamSessionId"} + }; + + EXPECT_CALL(getMockedConnection(), makeRequest(testing::_)) + .WillOnce([&expectedCommand](const boost::json::object &command) -> boost::asio::awaitable { + EXPECT_EQ(command, expectedCommand); + co_return; + }); + + EXPECT_NO_THROW(runAwaitableVoid(stream->getKeepAlive())); } -TEST_F(StreamTest, stopProfits_exception) +TEST_F(XStationClientStreamTest, getKeepAlive_exception) { - XStationClientStream stream(getIoContext(), "demo", ""); - EXPECT_THROW(runAwaitableVoid(stream.stopProfits()), exception::ConnectionClosed); + EXPECT_CALL(getMockedConnection(), makeRequest(testing::_)) + .WillOnce([](const boost::json::object &) -> boost::asio::awaitable { + throw exception::ConnectionClosed("Exception"); + }); + + EXPECT_THROW(runAwaitableVoid(stream->getKeepAlive()), exception::ConnectionClosed); } -TEST_F(StreamTest, getTickPrices_exception) +TEST_F(XStationClientStreamTest, stopKeepAlive_ok) { - XStationClientStream stream(getIoContext(), "demo", ""); - EXPECT_THROW(runAwaitableVoid(stream.getTickPrices("EURUSD", 100, 10)), exception::ConnectionClosed); + const boost::json::object expectedCommand = { + {"command", "stopKeepAlive"} + }; + + EXPECT_CALL(getMockedConnection(), makeRequest(testing::_)) + .WillOnce([&expectedCommand](const boost::json::object &command) -> boost::asio::awaitable { + EXPECT_EQ(command, expectedCommand); + co_return; + }); + + EXPECT_NO_THROW(runAwaitableVoid(stream->stopKeepAlive())); } -TEST_F(StreamTest, stopTickPrices_exception) +TEST_F(XStationClientStreamTest, stopKeepAlive_exception) { - XStationClientStream stream(getIoContext(), "demo", ""); - EXPECT_THROW(runAwaitableVoid(stream.stopTickPrices("EURUSD")), exception::ConnectionClosed); + EXPECT_CALL(getMockedConnection(), makeRequest(testing::_)) + .WillOnce([](const boost::json::object &) -> boost::asio::awaitable { + throw exception::ConnectionClosed("Exception"); + }); + + EXPECT_THROW(runAwaitableVoid(stream->stopKeepAlive()), exception::ConnectionClosed); } -TEST_F(StreamTest, getTrades_exception) +TEST_F(XStationClientStreamTest, getNews_ok) { - XStationClientStream stream(getIoContext(), "demo", ""); - EXPECT_THROW(runAwaitableVoid(stream.getTrades()), exception::ConnectionClosed); + const boost::json::object expectedCommand = { + {"command", "getNews"}, + {"streamSessionId", "testStreamSessionId"} + }; + + EXPECT_CALL(getMockedConnection(), makeRequest(testing::_)) + .WillOnce([&expectedCommand](const boost::json::object &command) -> boost::asio::awaitable { + EXPECT_EQ(command, expectedCommand); + co_return; + }); + + EXPECT_NO_THROW(runAwaitableVoid(stream->getNews())); } -TEST_F(StreamTest, stopTrades_exception) +TEST_F(XStationClientStreamTest, getNews_exception) { - XStationClientStream stream(getIoContext(), "demo", ""); - EXPECT_THROW(runAwaitableVoid(stream.stopTrades()), exception::ConnectionClosed); + EXPECT_CALL(getMockedConnection(), makeRequest(testing::_)) + .WillOnce([](const boost::json::object &) -> boost::asio::awaitable { + throw exception::ConnectionClosed("Exception"); + }); + + EXPECT_THROW(runAwaitableVoid(stream->getNews()), exception::ConnectionClosed); } -TEST_F(StreamTest, getTradeStatus_exception) +TEST_F(XStationClientStreamTest, stopNews_ok) { - XStationClientStream stream(getIoContext(), "demo", ""); - EXPECT_THROW(runAwaitableVoid(stream.getTradeStatus()), exception::ConnectionClosed); + const boost::json::object expectedCommand = { + {"command", "stopNews"} + }; + + EXPECT_CALL(getMockedConnection(), makeRequest(testing::_)) + .WillOnce([&expectedCommand](const boost::json::object &command) -> boost::asio::awaitable { + EXPECT_EQ(command, expectedCommand); + co_return; + }); + + EXPECT_NO_THROW(runAwaitableVoid(stream->stopNews())); } -TEST_F(StreamTest, stopTradeStatus_exception) +TEST_F(XStationClientStreamTest, stopNews_exception) { - XStationClientStream stream(getIoContext(), "demo", ""); - EXPECT_THROW(runAwaitableVoid(stream.stopTradeStatus()), exception::ConnectionClosed); + EXPECT_CALL(getMockedConnection(), makeRequest(testing::_)) + .WillOnce([](const boost::json::object &) -> boost::asio::awaitable { + throw exception::ConnectionClosed("Exception"); + }); + + EXPECT_THROW(runAwaitableVoid(stream->stopNews()), exception::ConnectionClosed); } -TEST_F(StreamTest, ping_exception) +TEST_F(XStationClientStreamTest, getProfits_ok) { - XStationClientStream stream(getIoContext(), "demo", ""); - EXPECT_THROW(runAwaitableVoid(stream.ping()), exception::ConnectionClosed); + const boost::json::object expectedCommand = { + {"command", "getProfits"}, + {"streamSessionId", "testStreamSessionId"} + }; + + EXPECT_CALL(getMockedConnection(), makeRequest(testing::_)) + .WillOnce([&expectedCommand](const boost::json::object &command) -> boost::asio::awaitable { + EXPECT_EQ(command, expectedCommand); + co_return; + }); + + EXPECT_NO_THROW(runAwaitableVoid(stream->getProfits())); } + +TEST_F(XStationClientStreamTest, getProfits_exception) +{ + EXPECT_CALL(getMockedConnection(), makeRequest(testing::_)) + .WillOnce([](const boost::json::object &) -> boost::asio::awaitable { + throw exception::ConnectionClosed("Exception"); + }); + + EXPECT_THROW(runAwaitableVoid(stream->getProfits()), exception::ConnectionClosed); +} + +TEST_F(XStationClientStreamTest, stopProfits_ok) +{ + const boost::json::object expectedCommand = { + {"command", "stopProfits"} + }; + + EXPECT_CALL(getMockedConnection(), makeRequest(testing::_)) + .WillOnce([&expectedCommand](const boost::json::object &command) -> boost::asio::awaitable { + EXPECT_EQ(command, expectedCommand); + co_return; + }); + + EXPECT_NO_THROW(runAwaitableVoid(stream->stopProfits())); +} + +TEST_F(XStationClientStreamTest, stopProfits_exception) +{ + EXPECT_CALL(getMockedConnection(), makeRequest(testing::_)) + .WillOnce([](const boost::json::object &) -> boost::asio::awaitable { + throw exception::ConnectionClosed("Exception"); + }); + + EXPECT_THROW(runAwaitableVoid(stream->stopProfits()), exception::ConnectionClosed); +} + +TEST_F(XStationClientStreamTest, getTickPrices_ok) +{ + const std::string symbol = "EURUSD"; + const int minArrivalTime = 500; + const int maxLevel = 2; + + const boost::json::object expectedCommand = { + {"command", "getTickPrices"}, + {"streamSessionId", "testStreamSessionId"}, + {"symbol", symbol}, + {"minArrivalTime", minArrivalTime}, + {"maxLevel", maxLevel} + }; + + EXPECT_CALL(getMockedConnection(), makeRequest(testing::_)) + .WillOnce([&expectedCommand](const boost::json::object &command) -> boost::asio::awaitable { + EXPECT_EQ(command, expectedCommand); + co_return; + }); + + EXPECT_NO_THROW(runAwaitableVoid(stream->getTickPrices(symbol, minArrivalTime, maxLevel))); +} + +TEST_F(XStationClientStreamTest, getTickPrices_exception) +{ + const std::string symbol = "EURUSD"; + const int minArrivalTime = 500; + const int maxLevel = 2; + + EXPECT_CALL(getMockedConnection(), makeRequest(testing::_)) + .WillOnce([](const boost::json::object &) -> boost::asio::awaitable { + throw exception::ConnectionClosed("Exception"); + }); + + EXPECT_THROW(runAwaitableVoid(stream->getTickPrices(symbol, minArrivalTime, maxLevel)), exception::ConnectionClosed); +} + +TEST_F(XStationClientStreamTest, stopTickPrices_ok) +{ + const std::string symbol = "EURUSD"; + + const boost::json::object expectedCommand = { + {"command", "stopTickPrices"}, + {"symbol", symbol} + }; + + EXPECT_CALL(getMockedConnection(), makeRequest(testing::_)) + .WillOnce([&expectedCommand](const boost::json::object &command) -> boost::asio::awaitable { + EXPECT_EQ(command, expectedCommand); + co_return; + }); + + EXPECT_NO_THROW(runAwaitableVoid(stream->stopTickPrices(symbol))); +} + +TEST_F(XStationClientStreamTest, stopTickPrices_exception) +{ + const std::string symbol = "EURUSD"; + + EXPECT_CALL(getMockedConnection(), makeRequest(testing::_)) + .WillOnce([](const boost::json::object &) -> boost::asio::awaitable { + throw exception::ConnectionClosed("Exception"); + }); + + EXPECT_THROW(runAwaitableVoid(stream->stopTickPrices(symbol)), exception::ConnectionClosed); +} + +TEST_F(XStationClientStreamTest, getTrades_ok) +{ + const boost::json::object expectedCommand = { + {"command", "getTrades"}, + {"streamSessionId", "testStreamSessionId"} + }; + + EXPECT_CALL(getMockedConnection(), makeRequest(testing::_)) + .WillOnce([&expectedCommand](const boost::json::object &command) -> boost::asio::awaitable { + EXPECT_EQ(command, expectedCommand); + co_return; + }); + + EXPECT_NO_THROW(runAwaitableVoid(stream->getTrades())); +} + +TEST_F(XStationClientStreamTest, getTrades_exception) +{ + EXPECT_CALL(getMockedConnection(), makeRequest(testing::_)) + .WillOnce([](const boost::json::object &) -> boost::asio::awaitable { + throw exception::ConnectionClosed("Exception"); + }); + + EXPECT_THROW(runAwaitableVoid(stream->getTrades()), exception::ConnectionClosed); +} + +TEST_F(XStationClientStreamTest, stopTrades_ok) +{ + const boost::json::object expectedCommand = { + {"command", "stopTrades"} + }; + + EXPECT_CALL(getMockedConnection(), makeRequest(testing::_)) + .WillOnce([&expectedCommand](const boost::json::object &command) -> boost::asio::awaitable { + EXPECT_EQ(command, expectedCommand); + co_return; + }); + + EXPECT_NO_THROW(runAwaitableVoid(stream->stopTrades())); +} + +TEST_F(XStationClientStreamTest, stopTrades_exception) +{ + EXPECT_CALL(getMockedConnection(), makeRequest(testing::_)) + .WillOnce([](const boost::json::object &) -> boost::asio::awaitable { + throw exception::ConnectionClosed("Exception"); + }); + + EXPECT_THROW(runAwaitableVoid(stream->stopTrades()), exception::ConnectionClosed); +} + +TEST_F(XStationClientStreamTest, getTradeStatus_ok) +{ + const boost::json::object expectedCommand = { + {"command", "getTradeStatus"}, + {"streamSessionId", "testStreamSessionId"} + }; + + EXPECT_CALL(getMockedConnection(), makeRequest(testing::_)) + .WillOnce([&expectedCommand](const boost::json::object &command) -> boost::asio::awaitable { + EXPECT_EQ(command, expectedCommand); + co_return; + }); + + EXPECT_NO_THROW(runAwaitableVoid(stream->getTradeStatus())); +} + +TEST_F(XStationClientStreamTest, getTradeStatus_exception) +{ + EXPECT_CALL(getMockedConnection(), makeRequest(testing::_)) + .WillOnce([](const boost::json::object &) -> boost::asio::awaitable { + throw exception::ConnectionClosed("Exception"); + }); + + EXPECT_THROW(runAwaitableVoid(stream->getTradeStatus()), exception::ConnectionClosed); +} + +TEST_F(XStationClientStreamTest, stopTradeStatus_ok) +{ + const boost::json::object expectedCommand = { + {"command", "stopTradeStatus"} + }; + + EXPECT_CALL(getMockedConnection(), makeRequest(testing::_)) + .WillOnce([&expectedCommand](const boost::json::object &command) -> boost::asio::awaitable { + EXPECT_EQ(command, expectedCommand); + co_return; + }); + + EXPECT_NO_THROW(runAwaitableVoid(stream->stopTradeStatus())); +} + +TEST_F(XStationClientStreamTest, stopTradeStatus_exception) +{ + EXPECT_CALL(getMockedConnection(), makeRequest(testing::_)) + .WillOnce([](const boost::json::object &) -> boost::asio::awaitable { + throw exception::ConnectionClosed("Exception"); + }); + + EXPECT_THROW(runAwaitableVoid(stream->stopTradeStatus()), exception::ConnectionClosed); +} + +TEST_F(XStationClientStreamTest, ping_ok) +{ + const boost::json::object expectedCommand = { + {"command", "ping"}, + {"streamSessionId", "testStreamSessionId"} + }; + + EXPECT_CALL(getMockedConnection(), makeRequest(testing::_)) + .WillOnce([&expectedCommand](const boost::json::object &command) -> boost::asio::awaitable { + EXPECT_EQ(command, expectedCommand); + co_return; + }); + + EXPECT_NO_THROW(runAwaitableVoid(stream->ping())); +} + +TEST_F(XStationClientStreamTest, ping_exception) +{ + EXPECT_CALL(getMockedConnection(), makeRequest(testing::_)) + .WillOnce([](const boost::json::object &) -> boost::asio::awaitable { + throw exception::ConnectionClosed("Exception"); + }); + + EXPECT_THROW(runAwaitableVoid(stream->ping()), exception::ConnectionClosed); +} + +} // namespace xapi diff --git a/test/mocks/CMakeLists.txt b/test/mocks/CMakeLists.txt new file mode 100644 index 0000000..0365f75 --- /dev/null +++ b/test/mocks/CMakeLists.txt @@ -0,0 +1,8 @@ +set (TEST_MOCK_LIB test_mocks) + +set (TEST_MOCK_LIB_PUBLIC_H + MockConnection.hpp +) + +add_library(${TEST_MOCK_LIB} INTERFACE ${TEST_MOCK_LIB_PUBLIC_H}) +target_link_libraries(${TEST_MOCK_LIB} INTERFACE gmock gmock_main) diff --git a/test/mocks/MockConnection.hpp b/test/mocks/MockConnection.hpp new file mode 100644 index 0000000..fba5af7 --- /dev/null +++ b/test/mocks/MockConnection.hpp @@ -0,0 +1,20 @@ +#pragma once + +#include +#include "xapi/IConnection.hpp" + +class MockConnection : public xapi::internals::IConnection +{ +public: + // Mock the connect method + MOCK_METHOD((boost::asio::awaitable), connect, (const boost::url &url), (override)); + + // Mock the disconnect method + MOCK_METHOD((boost::asio::awaitable), disconnect, (), (override)); + + // Mock the makeRequest method + MOCK_METHOD((boost::asio::awaitable), makeRequest, (const boost::json::object &command), (override)); + + // Mock the waitResponse method + MOCK_METHOD((boost::asio::awaitable), waitResponse, (), (override)); +}; diff --git a/xapi/CMakeLists.txt b/xapi/CMakeLists.txt index c3034a6..7d56237 100644 --- a/xapi/CMakeLists.txt +++ b/xapi/CMakeLists.txt @@ -1,6 +1,7 @@ set(XAPI_PUBLIC_H Enums.hpp Exceptions.hpp + IConnection.hpp Connection.hpp XStationClient.hpp XStationClientStream.hpp @@ -16,14 +17,23 @@ set(XAPI_SOURCES add_library(Xapi SHARED ${XAPI_SOURCES}) -target_compile_options(Xapi PRIVATE +# TARGET SETUP OPTIONS ======================================== +set(COMMON_FLAGS -Wall -Werror -Wpedantic -Wextra - -O2 + -march=native ) +if(CMAKE_BUILD_TYPE STREQUAL "Release") + list(APPEND COMMON_FLAGS -O3) +else() + list(APPEND COMMON_FLAGS -O0 -g) +endif() +target_compile_options(Xapi PRIVATE ${COMMON_FLAGS}) + +# TARGET LINK OPTIONS ======================================== target_link_libraries(Xapi PRIVATE Boost::system Boost::url diff --git a/xapi/Connection.cpp b/xapi/Connection.cpp index 246ad61..9f04f7e 100644 --- a/xapi/Connection.cpp +++ b/xapi/Connection.cpp @@ -1,5 +1,6 @@ #include "Connection.hpp" #include "Exceptions.hpp" +#include namespace xapi { @@ -23,6 +24,23 @@ Connection::Connection(Connection &&other) noexcept { } +Connection::~Connection() +{ + m_cancellationSignal.emit(boost::asio::cancellation_type::all); + if (m_websocket.is_open()) + { + try + { + // Attempt a graceful WebSocket closure + m_websocket.close(boost::beast::websocket::close_code::normal); + } + catch (const boost::system::system_error &e) + { + std::cerr << "Fatal error: " << e.what() << std::endl; + } + } +} + boost::asio::awaitable Connection::connect(const boost::url &url) { const auto executor = co_await boost::asio::this_coro::executor; @@ -75,9 +93,6 @@ boost::asio::awaitable Connection::establishSSLConnection( boost::asio::awaitable Connection::disconnect() { m_cancellationSignal.emit(boost::asio::cancellation_type::all); - // Cancel all pending asynchronous operations - m_websocket.next_layer().next_layer().cancel(); - try { co_await m_websocket.async_close(boost::beast::websocket::close_code::normal, boost::asio::use_awaitable); @@ -98,7 +113,9 @@ boost::asio::awaitable Connection::makeRequest(const boost::json::object & const auto duration = currentTime - m_lastRequestTime; if (duration < m_requestTimeout) { - std::this_thread::sleep_for(m_requestTimeout - duration); + boost::asio::steady_timer timer(co_await boost::asio::this_coro::executor); + timer.expires_after(m_requestTimeout - duration); + co_await timer.async_wait(boost::asio::use_awaitable); } try @@ -144,15 +161,17 @@ boost::asio::awaitable Connection::startKeepAlive(boost::asio::cancellatio const auto executor = co_await boost::asio::this_coro::executor; boost::asio::steady_timer pingTimer(executor); const auto pingInterval = std::chrono::seconds(20); + bool canceled = true; - cancellationSlot.assign([&]([[maybe_unused]] boost::asio::cancellation_type type) { + cancellationSlot.assign([&](boost::asio::cancellation_type type) { if (type == boost::asio::cancellation_type::all) { + canceled = true; pingTimer.cancel(); } }); - while (true) + while (!canceled) { try { diff --git a/xapi/Connection.hpp b/xapi/Connection.hpp index df78202..aeb80ef 100644 --- a/xapi/Connection.hpp +++ b/xapi/Connection.hpp @@ -8,12 +8,9 @@ * establishing and managing connections, making requests, and handling responses. */ -#include -#include +#include "IConnection.hpp" #include #include -#include -#include #include #include @@ -29,13 +26,13 @@ namespace internals * The Connection class encapsulates the functionality for establishing an SSL connection, * sending requests, and receiving responses. */ -class Connection +class Connection : public IConnection { public: Connection() = delete; - Connection(const Connection &) = delete; - Connection &operator=(const Connection &) = delete; + Connection(const Connection &other) = delete; + Connection &operator=(const Connection &other) = delete; Connection(Connection &&other) noexcept; // Move assignment operator is not supported because of boost::beast::websocket::stream @@ -47,7 +44,7 @@ class Connection */ explicit Connection(boost::asio::io_context &ioContext); - virtual ~Connection() = default; + virtual ~Connection() override; /** * @brief Asynchronously establishes secure WebSocket connection to the server. @@ -55,13 +52,13 @@ class Connection * @return An awaitable void. * @throw xapi::exception::ConnectionClosed if the connection fails. */ - boost::asio::awaitable connect(const boost::url &url); + boost::asio::awaitable connect(const boost::url &url) override; /** * @brief Asynchronously disconnects from the server. * @return An awaitable void. */ - boost::asio::awaitable disconnect(); + boost::asio::awaitable disconnect() override; /** * @brief Makes an asynchronous request to the server. @@ -69,20 +66,19 @@ class Connection * @return An awaitable void. * @throw xapi::exception::ConnectionClosed if the request fails. */ - boost::asio::awaitable makeRequest(const boost::json::object &command); + boost::asio::awaitable makeRequest(const boost::json::object &command) override; /** * @brief Waits for a response from the server. * @return An awaitable boost::json::object with response from the server. * @throw xapi::exception::ConnectionClosed if the response fails. */ - boost::asio::awaitable waitResponse(); + boost::asio::awaitable waitResponse() override; - protected: + private: // The IO context for asynchronous operations. boost::asio::io_context &m_ioContext; - private: /** * @brief Establishes an SSL connection asynchronously. * @param results The resolved endpoints to attempt to connect to. diff --git a/xapi/IConnection.hpp b/xapi/IConnection.hpp new file mode 100644 index 0000000..ab5d4b3 --- /dev/null +++ b/xapi/IConnection.hpp @@ -0,0 +1,54 @@ +#pragma once + +/** + * @file IConnection.hpp + * @brief Declaration of the Connection interface. + */ + +#include +#include +#include +#include + +namespace xapi +{ +namespace internals +{ + +class IConnection +{ + public: + virtual ~IConnection() = default; + + /** + * @brief Asynchronously establishes secure WebSocket connection to the server. + * @param url The URL to connect to. + * @return An awaitable void. + * @throw xapi::exception::ConnectionClosed if the connection fails. + */ + virtual boost::asio::awaitable connect(const boost::url &url) = 0; + + /** + * @brief Asynchronously disconnects from the server. + * @return An awaitable void. + */ + virtual boost::asio::awaitable disconnect() = 0; + + /** + * @brief Makes an asynchronous request to the server. + * @param command The command to send as a JSON value. + * @return An awaitable void. + * @throw xapi::exception::ConnectionClosed if the request fails. + */ + virtual boost::asio::awaitable makeRequest(const boost::json::object &command) = 0; + + /** + * @brief Waits for a response from the server. + * @return An awaitable boost::json::object with response from the server. + * @throw xapi::exception::ConnectionClosed if the response fails. + */ + virtual boost::asio::awaitable waitResponse() = 0; +}; + +} // namespace internals +} // namespace xapi diff --git a/xapi/XStationClient.cpp b/xapi/XStationClient.cpp index 1cb0d85..f7da594 100644 --- a/xapi/XStationClient.cpp +++ b/xapi/XStationClient.cpp @@ -8,7 +8,7 @@ const std::unordered_set XStationClient::m_knownAccountTypes = {"de XStationClient::XStationClient(boost::asio::io_context &ioContext, const std::string &accountId, const std::string &password, const std::string &accountType) - : Connection(ioContext), m_accountId(accountId), m_password(password), + : m_ioContext(ioContext), m_connection(std::make_unique(ioContext)), m_accountId(accountId), m_password(password), m_accountType(accountType), m_safeMode(true), m_streamSessionId("") { } @@ -25,7 +25,7 @@ boost::asio::awaitable XStationClient::login() { validateAccountType(m_accountType); const boost::url socketUrl = boost::urls::format("wss://ws.xtb.com/{}", m_accountType); - co_await connect(socketUrl); + co_await m_connection->connect(socketUrl); boost::json::object command = { {"command", "login"}, @@ -35,6 +35,11 @@ boost::asio::awaitable XStationClient::login() { }} }; auto result = co_await request(command); + + if (!result.contains("status") && !result.contains("streamSessionId")) { + throw exception::LoginFailed("Invalid response from the server"); + } + if (result["status"].as_bool() != true) { throw exception::LoginFailed(boost::json::serialize(result)); @@ -48,7 +53,7 @@ boost::asio::awaitable XStationClient::logout() { {"command", "logout"} }; co_await request(command); - co_await disconnect(); + co_await m_connection->disconnect(); } @@ -366,8 +371,8 @@ boost::asio::awaitable XStationClient::tradeTransactionStat boost::asio::awaitable XStationClient::request(const boost::json::object &command) { - co_await makeRequest(command); - auto result = co_await waitResponse(); + co_await m_connection->makeRequest(command); + auto result = co_await m_connection->waitResponse(); co_return result; } diff --git a/xapi/XStationClient.hpp b/xapi/XStationClient.hpp index 0258a91..bf953f8 100644 --- a/xapi/XStationClient.hpp +++ b/xapi/XStationClient.hpp @@ -13,6 +13,20 @@ #include "Enums.hpp" #include +#undef TEST_FRIENDS +#ifdef ENABLE_TEST +#include "gtest/gtest_prod.h" +class XStationClientTest; +#define TEST_FRIENDS \ + friend class XStationClientTest; \ + FRIEND_TEST(XStationClientTest, login_ok); \ + FRIEND_TEST(XStationClientTest, login_invalid_account_type); \ + FRIEND_TEST(XStationClientTest, login_account_credentials_null); \ + FRIEND_TEST(XStationClientTest, login_invalid_return_from_the_server); +#else +#define TEST_FRIENDS +#endif + namespace xapi { @@ -23,7 +37,7 @@ namespace xapi * from xAPI. It is built on top of the Connection class, which handles the * low-level details of establishing and maintaining a connection. */ -class XStationClient : protected internals::Connection +class XStationClient final { public: XStationClient() = delete; @@ -67,7 +81,7 @@ class XStationClient : protected internals::Connection */ explicit XStationClient(boost::asio::io_context &ioContext, const boost::json::object &accountCredentials); - ~XStationClient() override = default; + ~XStationClient() = default; /** * @brief Opens connection to the server and logs in. @@ -151,7 +165,10 @@ class XStationClient : protected internals::Connection boost::asio::awaitable tradeTransactionStatus(int order); - protected: + private: + + boost::asio::io_context &m_ioContext; + std::unique_ptr m_connection; const std::string m_accountId; const std::string m_password; @@ -180,6 +197,8 @@ class XStationClient : protected internals::Connection * @throw xapi::exception::ConnectionClosed if the account type is not known. */ static void validateAccountType(const std::string &accountType); + + TEST_FRIENDS }; } // namespace xapi diff --git a/xapi/XStationClientStream.cpp b/xapi/XStationClientStream.cpp index eed7597..cc3c9c0 100644 --- a/xapi/XStationClientStream.cpp +++ b/xapi/XStationClientStream.cpp @@ -5,23 +5,23 @@ namespace xapi { XStationClientStream::XStationClientStream(boost::asio::io_context &ioContext, const std::string &accountType, const std::string& streamSessionId) -: Connection(ioContext), m_streamUrl(boost::urls::format("wss://ws.xtb.com/{}Stream", accountType)), m_streamSessionId(streamSessionId) +: m_connection(std::make_unique(ioContext)), m_streamUrl(boost::urls::format("wss://ws.xtb.com/{}Stream", accountType)), m_streamSessionId(streamSessionId) { } boost::asio::awaitable XStationClientStream::open() { - co_await connect(m_streamUrl); + co_await m_connection->connect(m_streamUrl); } boost::asio::awaitable XStationClientStream::close() { - co_await disconnect(); + co_await m_connection->disconnect(); } boost::asio::awaitable XStationClientStream::listen() { - auto result = co_await waitResponse(); + auto result = co_await m_connection->waitResponse(); co_return result; } @@ -31,7 +31,7 @@ boost::asio::awaitable XStationClientStream::getBalance() {"command", "getBalance"}, {"streamSessionId", m_streamSessionId} }; - co_await makeRequest(command); + co_await m_connection->makeRequest(command); } boost::asio::awaitable XStationClientStream::stopBalance() @@ -39,7 +39,7 @@ boost::asio::awaitable XStationClientStream::stopBalance() boost::json::object command = { {"command", "stopBalance"} }; - co_await makeRequest(command); + co_await m_connection->makeRequest(command); } boost::asio::awaitable XStationClientStream::getCandles(const std::string &symbol) @@ -49,7 +49,7 @@ boost::asio::awaitable XStationClientStream::getCandles(const std::string {"streamSessionId", m_streamSessionId}, {"symbol", symbol} }; - co_await makeRequest(command); + co_await m_connection->makeRequest(command); } boost::asio::awaitable XStationClientStream::stopCandles(const std::string &symbol) @@ -58,7 +58,7 @@ boost::asio::awaitable XStationClientStream::stopCandles(const std::string {"command", "stopCandles"}, {"symbol", symbol} }; - co_await makeRequest(command); + co_await m_connection->makeRequest(command); } boost::asio::awaitable XStationClientStream::getKeepAlive() @@ -67,7 +67,7 @@ boost::asio::awaitable XStationClientStream::getKeepAlive() {"command", "getKeepAlive"}, {"streamSessionId", m_streamSessionId} }; - co_await makeRequest(command); + co_await m_connection->makeRequest(command); } boost::asio::awaitable XStationClientStream::stopKeepAlive() @@ -75,7 +75,7 @@ boost::asio::awaitable XStationClientStream::stopKeepAlive() boost::json::object command = { {"command", "stopKeepAlive"} }; - co_await makeRequest(command); + co_await m_connection->makeRequest(command); } boost::asio::awaitable XStationClientStream::getNews() @@ -84,7 +84,7 @@ boost::asio::awaitable XStationClientStream::getNews() {"command", "getNews"}, {"streamSessionId", m_streamSessionId} }; - co_await makeRequest(command); + co_await m_connection->makeRequest(command); } boost::asio::awaitable XStationClientStream::stopNews() @@ -92,7 +92,7 @@ boost::asio::awaitable XStationClientStream::stopNews() boost::json::object command = { {"command", "stopNews"} }; - co_await makeRequest(command); + co_await m_connection->makeRequest(command); } boost::asio::awaitable XStationClientStream::getProfits() @@ -101,7 +101,7 @@ boost::asio::awaitable XStationClientStream::getProfits() {"command", "getProfits"}, {"streamSessionId", m_streamSessionId} }; - co_await makeRequest(command); + co_await m_connection->makeRequest(command); } boost::asio::awaitable XStationClientStream::stopProfits() @@ -109,7 +109,7 @@ boost::asio::awaitable XStationClientStream::stopProfits() boost::json::object command = { {"command", "stopProfits"} }; - co_await makeRequest(command); + co_await m_connection->makeRequest(command); } boost::asio::awaitable XStationClientStream::getTickPrices(const std::string &symbol, int minArrivalTime, int maxLevel) @@ -121,7 +121,7 @@ boost::asio::awaitable XStationClientStream::getTickPrices(const std::stri {"minArrivalTime", minArrivalTime}, {"maxLevel", maxLevel} }; - co_await makeRequest(command); + co_await m_connection->makeRequest(command); } boost::asio::awaitable XStationClientStream::stopTickPrices(const std::string &symbol) @@ -130,7 +130,7 @@ boost::asio::awaitable XStationClientStream::stopTickPrices(const std::str {"command", "stopTickPrices"}, {"symbol", symbol} }; - co_await makeRequest(command); + co_await m_connection->makeRequest(command); } boost::asio::awaitable XStationClientStream::getTrades() @@ -139,7 +139,7 @@ boost::asio::awaitable XStationClientStream::getTrades() {"command", "getTrades"}, {"streamSessionId", m_streamSessionId} }; - co_await makeRequest(command); + co_await m_connection->makeRequest(command); } boost::asio::awaitable XStationClientStream::stopTrades() @@ -147,7 +147,7 @@ boost::asio::awaitable XStationClientStream::stopTrades() boost::json::object command = { {"command", "stopTrades"} }; - co_await makeRequest(command); + co_await m_connection->makeRequest(command); } boost::asio::awaitable XStationClientStream::getTradeStatus() @@ -156,7 +156,7 @@ boost::asio::awaitable XStationClientStream::getTradeStatus() {"command", "getTradeStatus"}, {"streamSessionId", m_streamSessionId} }; - co_await makeRequest(command); + co_await m_connection->makeRequest(command); } boost::asio::awaitable XStationClientStream::stopTradeStatus() @@ -164,7 +164,7 @@ boost::asio::awaitable XStationClientStream::stopTradeStatus() boost::json::object command = { {"command", "stopTradeStatus"} }; - co_await makeRequest(command); + co_await m_connection->makeRequest(command); } boost::asio::awaitable XStationClientStream::ping() @@ -173,7 +173,7 @@ boost::asio::awaitable XStationClientStream::ping() {"command", "ping"}, {"streamSessionId", m_streamSessionId} }; - co_await makeRequest(command); + co_await m_connection->makeRequest(command); } } // namespace xapi diff --git a/xapi/XStationClientStream.hpp b/xapi/XStationClientStream.hpp index 56aed6d..9341062 100644 --- a/xapi/XStationClientStream.hpp +++ b/xapi/XStationClientStream.hpp @@ -10,6 +10,16 @@ #include "Connection.hpp" +#undef TEST_FRIENDS +#ifdef ENABLE_TEST +#include "gtest/gtest_prod.h" +class XStationClientStreamTest; +#define TEST_FRIENDS \ + friend class XStationClientStreamTest; +#else +#define TEST_FRIENDS +#endif + namespace xapi { @@ -19,7 +29,7 @@ namespace xapi * The XStationClientStream class provides a high-level interface for streaming real-time data * from xAPI. */ -class XStationClientStream : protected internals::Connection +class XStationClientStream { public: XStationClientStream() = delete; @@ -35,7 +45,7 @@ class XStationClientStream : protected internals::Connection * @param ioContext The IO context for asynchronous operations. */ explicit XStationClientStream(boost::asio::io_context &ioContext, const std::string &accountType, const std::string& streamSessionId); - ~XStationClientStream() override = default; + ~XStationClientStream() = default; /** * @brief Opens a connection to the streaming server. @@ -95,9 +105,13 @@ class XStationClientStream : protected internals::Connection boost::asio::awaitable ping(); private: + std::unique_ptr m_connection; + // The stream session ID. const boost::url m_streamUrl; const std::string m_streamSessionId; + + TEST_FRIENDS }; } // namespace xapi