diff --git a/CMakeLists.txt b/CMakeLists.txt index c89892f3..072597b0 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -170,6 +170,7 @@ if(BUILD_TESTS) unittests/rpc/handlers/NFTSellOffersTest.cpp unittests/rpc/handlers/NFTHistoryTest.cpp unittests/rpc/handlers/SubscribeTest.cpp + unittests/rpc/handlers/UnsubscribeTest.cpp # Backend unittests/backend/cassandra/BaseTests.cpp unittests/backend/cassandra/BackendTests.cpp diff --git a/src/rpc/common/Validators.cpp b/src/rpc/common/Validators.cpp index 97173f42..7289609a 100644 --- a/src/rpc/common/Validators.cpp +++ b/src/rpc/common/Validators.cpp @@ -234,36 +234,4 @@ CustomValidator SubscribeAccountsValidator = } return MaybeError{}; }}; - -CustomValidator SubscribeBooksValidator = - CustomValidator{[](boost::json::value const& value, std::string_view key) -> MaybeError { - if (!value.is_array()) - return Error{RPC::Status{RPC::RippledError::rpcINVALID_PARAMS, std::string(key) + "NotArray"}}; - for (auto const& book : value.as_array()) - { - if (!book.is_object()) - return Error{RPC::Status{RPC::RippledError::rpcINVALID_PARAMS, std::string(key) + "ItemNotObject"}}; - if (book.as_object().contains("both") && !book.as_object().at("both").is_bool()) - { - return Error{RPC::Status{RPC::RippledError::rpcINVALID_PARAMS, "bothNotBool"}}; - } - - if (book.as_object().contains("snapshot") && !book.as_object().at("snapshot").is_bool()) - { - return Error{RPC::Status{RPC::RippledError::rpcINVALID_PARAMS, "snapshotNotBool"}}; - } - - if (book.as_object().contains("taker")) - { - if (auto const err = AccountValidator.verify(book.as_object(), "taker"); !err) - return err; - } - - auto const parsedBook = RPC::parseBook(book.as_object()); - if (auto const status = std::get_if(&parsedBook)) - return Error(*status); - } - return MaybeError{}; - }}; - } // namespace RPCng::validation diff --git a/src/rpc/common/Validators.h b/src/rpc/common/Validators.h index 8e1b75af..2f0141eb 100644 --- a/src/rpc/common/Validators.h +++ b/src/rpc/common/Validators.h @@ -494,10 +494,4 @@ extern CustomValidator SubscribeStreamValidator; */ extern CustomValidator SubscribeAccountsValidator; -/** - * @brief Provide a validator for validating valid books used in - * subscribe/unsubscribe - */ -extern CustomValidator SubscribeBooksValidator; - } // namespace RPCng::validation diff --git a/src/rpc/ngHandlers/Subscribe.h b/src/rpc/ngHandlers/Subscribe.h index 73a99eca..0a578e1b 100644 --- a/src/rpc/ngHandlers/Subscribe.h +++ b/src/rpc/ngHandlers/Subscribe.h @@ -71,11 +71,38 @@ public: RpcSpecConstRef spec() const { + static auto const booksValidator = + validation::CustomValidator{[](boost::json::value const& value, std::string_view key) -> MaybeError { + if (!value.is_array()) + return Error{RPC::Status{RPC::RippledError::rpcINVALID_PARAMS, std::string(key) + "NotArray"}}; + for (auto const& book : value.as_array()) + { + if (!book.is_object()) + return Error{ + RPC::Status{RPC::RippledError::rpcINVALID_PARAMS, std::string(key) + "ItemNotObject"}}; + + if (book.as_object().contains("both") && !book.as_object().at("both").is_bool()) + return Error{RPC::Status{RPC::RippledError::rpcINVALID_PARAMS, "bothNotBool"}}; + + if (book.as_object().contains("snapshot") && !book.as_object().at("snapshot").is_bool()) + return Error{RPC::Status{RPC::RippledError::rpcINVALID_PARAMS, "snapshotNotBool"}}; + + if (book.as_object().contains("taker")) + if (auto const err = validation::AccountValidator.verify(book.as_object(), "taker"); !err) + return err; + + auto const parsedBook = RPC::parseBook(book.as_object()); + if (auto const status = std::get_if(&parsedBook)) + return Error(*status); + } + return MaybeError{}; + }}; + static auto const rpcSpec = RpcSpec{ {JS(streams), validation::SubscribeStreamValidator}, {JS(accounts), validation::SubscribeAccountsValidator}, {JS(accounts_proposed), validation::SubscribeAccountsValidator}, - {JS(books), validation::SubscribeBooksValidator}}; + {JS(books), booksValidator}}; return rpcSpec; } diff --git a/src/rpc/ngHandlers/Unsubscribe.h b/src/rpc/ngHandlers/Unsubscribe.h new file mode 100644 index 00000000..ec22dd9b --- /dev/null +++ b/src/rpc/ngHandlers/Unsubscribe.h @@ -0,0 +1,209 @@ +//------------------------------------------------------------------------------ +/* + This file is part of clio: https://github.com/XRPLF/clio + Copyright (c) 2023, the clio developers. + + Permission to use, copy, modify, and distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#pragma once + +#include +#include +#include +#include + +namespace RPCng { + +template +class BaseUnsubscribeHandler +{ + std::shared_ptr sharedPtrBackend_; + std::shared_ptr subscriptions_; + +public: + struct OrderBook + { + ripple::Book book; + bool both = false; + }; + + struct Input + { + std::optional> accounts; + std::optional> streams; + std::optional> accountsProposed; + std::optional> books; + }; + + using Output = VoidOutput; + using Result = RPCng::HandlerReturnType; + + BaseUnsubscribeHandler( + std::shared_ptr const& sharedPtrBackend, + std::shared_ptr const& subscriptions) + : sharedPtrBackend_(sharedPtrBackend), subscriptions_(subscriptions) + { + } + + RpcSpecConstRef + spec() const + { + static auto const booksValidator = + validation::CustomValidator{[](boost::json::value const& value, std::string_view key) -> MaybeError { + if (!value.is_array()) + return Error{RPC::Status{RPC::RippledError::rpcINVALID_PARAMS, std::string(key) + "NotArray"}}; + + for (auto const& book : value.as_array()) + { + if (!book.is_object()) + return Error{ + RPC::Status{RPC::RippledError::rpcINVALID_PARAMS, std::string(key) + "ItemNotObject"}}; + + if (book.as_object().contains("both") && !book.as_object().at("both").is_bool()) + return Error{RPC::Status{RPC::RippledError::rpcINVALID_PARAMS, "bothNotBool"}}; + + auto const parsedBook = RPC::parseBook(book.as_object()); + if (auto const status = std::get_if(&parsedBook)) + return Error(*status); + } + return MaybeError{}; + }}; + + static auto const rpcSpec = RpcSpec{ + {JS(streams), validation::SubscribeStreamValidator}, + {JS(accounts), validation::SubscribeAccountsValidator}, + {JS(accounts_proposed), validation::SubscribeAccountsValidator}, + {JS(books), booksValidator}}; + + return rpcSpec; + } + + Result + process(Input input, Context const& ctx) const + { + if (input.streams) + unsubscribeFromStreams(*(input.streams), ctx.session); + + if (input.accounts) + unsubscribeFromAccounts(*(input.accounts), ctx.session); + + if (input.accountsProposed) + unsubscribeFromProposedAccounts(*(input.accountsProposed), ctx.session); + + if (input.books) + unsubscribeFromBooks(*(input.books), ctx.session); + + return Output{}; + } + +private: + void + unsubscribeFromStreams(std::vector const& streams, std::shared_ptr const& session) const + { + for (auto const& stream : streams) + { + if (stream == "ledger") + subscriptions_->unsubLedger(session); + else if (stream == "transactions") + subscriptions_->unsubTransactions(session); + else if (stream == "transactions_proposed") + subscriptions_->unsubProposedTransactions(session); + else if (stream == "validations") + subscriptions_->unsubValidation(session); + else if (stream == "manifests") + subscriptions_->unsubManifest(session); + else if (stream == "book_changes") + subscriptions_->unsubBookChanges(session); + else + assert(false); + } + } + + void + unsubscribeFromAccounts(std::vector accounts, std::shared_ptr const& session) const + { + for (auto const& account : accounts) + { + auto accountID = RPC::accountFromStringStrict(account); + subscriptions_->unsubAccount(*accountID, session); + } + } + + void + unsubscribeFromProposedAccounts(std::vector accountsProposed, std::shared_ptr const& session) + const + { + for (auto const& account : accountsProposed) + { + auto accountID = RPC::accountFromStringStrict(account); + subscriptions_->unsubProposedAccount(*accountID, session); + } + } + + void + unsubscribeFromBooks(std::vector const& books, std::shared_ptr const& session) const + { + for (auto const& orderBook : books) + { + subscriptions_->unsubBook(orderBook.book, session); + if (orderBook.both) + subscriptions_->unsubBook(ripple::reversed(orderBook.book), session); + } + } + + friend Input + tag_invoke(boost::json::value_to_tag, boost::json::value const& jv) + { + auto const& jsonObject = jv.as_object(); + Input input; + if (auto const& streams = jsonObject.find(JS(streams)); streams != jsonObject.end()) + { + input.streams = std::vector(); + for (auto const& stream : streams->value().as_array()) + input.streams->push_back(stream.as_string().c_str()); + } + if (auto const& accounts = jsonObject.find(JS(accounts)); accounts != jsonObject.end()) + { + input.accounts = std::vector(); + for (auto const& account : accounts->value().as_array()) + input.accounts->push_back(account.as_string().c_str()); + } + if (auto const& accountsProposed = jsonObject.find(JS(accounts_proposed)); accountsProposed != jsonObject.end()) + { + input.accountsProposed = std::vector(); + for (auto const& account : accountsProposed->value().as_array()) + input.accountsProposed->push_back(account.as_string().c_str()); + } + if (auto const& books = jsonObject.find(JS(books)); books != jsonObject.end()) + { + input.books = std::vector(); + for (auto const& book : books->value().as_array()) + { + auto const& bookObject = book.as_object(); + OrderBook internalBook; + if (auto const& both = bookObject.find(JS(both)); both != bookObject.end()) + internalBook.both = both->value().as_bool(); + auto const parsedBookMaybe = RPC::parseBook(book.as_object()); + internalBook.book = std::get(parsedBookMaybe); + input.books->push_back(internalBook); + } + } + return input; + } +}; + +using UnsubscribeHandler = BaseUnsubscribeHandler; + +} // namespace RPCng diff --git a/unittests/rpc/BaseTests.cpp b/unittests/rpc/BaseTests.cpp index 3db3e203..28dba053 100644 --- a/unittests/rpc/BaseTests.cpp +++ b/unittests/rpc/BaseTests.cpp @@ -491,43 +491,3 @@ TEST_F(RPCBaseTest, SubscribeAccountsValidator) err = spec.validate(failingInput); ASSERT_FALSE(err); } - -TEST_F(RPCBaseTest, SubscribeBooksValidator) -{ - auto const spec = RpcSpec{{"books", SubscribeBooksValidator}}; - auto passingInput = json::parse( - R"({ - "books": [{ - "taker_pays": - { - "currency": "XRP" - }, - "taker_gets": - { - "currency": "USD", - "issuer": "rhub8VRN55s94qWKDv6jmDy1pUykJzF3wq" - } - }] - })"); - ASSERT_TRUE(spec.validate(passingInput)); - - auto failingInput = json::parse(R"({ "books": 256})"); - auto err = spec.validate(failingInput); - ASSERT_FALSE(err); - - failingInput = json::parse(R"({ "books": ["test"]})"); - err = spec.validate(failingInput); - ASSERT_FALSE(err); - - failingInput = json::parse( - R"({ - "books": [{ - "taker_pays": - { - "currency": "XRP" - } - }] - })"); - err = spec.validate(failingInput); - ASSERT_FALSE(err); -} diff --git a/unittests/rpc/handlers/UnsubscribeTest.cpp b/unittests/rpc/handlers/UnsubscribeTest.cpp new file mode 100644 index 00000000..68c3be53 --- /dev/null +++ b/unittests/rpc/handlers/UnsubscribeTest.cpp @@ -0,0 +1,636 @@ +//------------------------------------------------------------------------------ +/* + This file is part of clio: https://github.com/XRPLF/clio + Copyright (c) 2023, the clio developers. + + Permission to use, copy, modify, and distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#include +#include +#include +#include +#include +#include + +#include + +using namespace RPCng; +namespace json = boost::json; +using namespace testing; + +using TestUnsubscribeHandler = BaseUnsubscribeHandler; + +constexpr static auto ACCOUNT = "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn"; +constexpr static auto ACCOUNT2 = "rLEsXccBGNR3UPuPu2hUXPjziKC3qKSBun"; + +class RPCUnsubscribeTest : public HandlerBaseTest, public MockSubscriptionManagerTest +{ +protected: + void + SetUp() override + { + HandlerBaseTest::SetUp(); + MockSubscriptionManagerTest::SetUp(); + clio::Config cfg; + util::TagDecoratorFactory tagDecoratorFactory{cfg}; + session_ = std::make_shared(tagDecoratorFactory); + } + void + TearDown() override + { + MockSubscriptionManagerTest::TearDown(); + HandlerBaseTest::TearDown(); + } + + std::shared_ptr subManager_; + std::shared_ptr session_; +}; + +struct UnsubscribeParamTestCaseBundle +{ + std::string testName; + std::string testJson; + std::string expectedError; + std::string expectedErrorMessage; +}; + +// parameterized test cases for parameters check +struct UnsubscribeParameterTest : public RPCUnsubscribeTest, public WithParamInterface +{ + struct NameGenerator + { + template + std::string + operator()(const testing::TestParamInfo& info) const + { + auto bundle = static_cast(info.param); + return bundle.testName; + } + }; +}; + +static auto +generateTestValuesForParametersTest() +{ + return std::vector{ + UnsubscribeParamTestCaseBundle{ + "AccountsNotArray", + R"({"accounts": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn"})", + "invalidParams", + "accountsNotArray"}, + UnsubscribeParamTestCaseBundle{ + "AccountsItemNotString", R"({"accounts": [123]})", "invalidParams", "accounts'sItemNotString"}, + UnsubscribeParamTestCaseBundle{ + "AccountsItemInvalidString", R"({"accounts": ["123"]})", "actMalformed", "accounts'sItemMalformed"}, + UnsubscribeParamTestCaseBundle{ + "AccountsEmptyArray", R"({"accounts": []})", "actMalformed", "accounts malformed."}, + UnsubscribeParamTestCaseBundle{ + "AccountsProposedNotArray", + R"({"accounts_proposed": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn"})", + "invalidParams", + "accounts_proposedNotArray"}, + UnsubscribeParamTestCaseBundle{ + "AccountsProposedItemNotString", + R"({"accounts_proposed": [123]})", + "invalidParams", + "accounts_proposed'sItemNotString"}, + UnsubscribeParamTestCaseBundle{ + "AccountsProposedItemInvalidString", + R"({"accounts_proposed": ["123"]})", + "actMalformed", + "accounts_proposed'sItemMalformed"}, + UnsubscribeParamTestCaseBundle{ + "AccountsProposedEmptyArray", + R"({"accounts_proposed": []})", + "actMalformed", + "accounts_proposed malformed."}, + UnsubscribeParamTestCaseBundle{"StreamsNotArray", R"({"streams": 1})", "invalidParams", "streamsNotArray"}, + UnsubscribeParamTestCaseBundle{"StreamNotString", R"({"streams": [1]})", "invalidParams", "streamNotString"}, + UnsubscribeParamTestCaseBundle{ + "StreamNotValid", R"({"streams": ["1"]})", "malformedStream", "Stream malformed."}, + UnsubscribeParamTestCaseBundle{"BooksNotArray", R"({"books": "1"})", "invalidParams", "booksNotArray"}, + UnsubscribeParamTestCaseBundle{ + "BooksItemNotObject", R"({"books": ["1"]})", "invalidParams", "booksItemNotObject"}, + UnsubscribeParamTestCaseBundle{ + "BooksItemMissingTakerPays", + R"({"books": [{"taker_gets": {"currency": "XRP"}}]})", + "invalidParams", + "Missing field 'taker_pays'"}, + UnsubscribeParamTestCaseBundle{ + "BooksItemMissingTakerGets", + R"({"books": [{"taker_pays": {"currency": "XRP"}}]})", + "invalidParams", + "Missing field 'taker_gets'"}, + UnsubscribeParamTestCaseBundle{ + "BooksItemTakerGetsNotObject", + R"({ + "books": + [ + { + "taker_pays": + { + "currency": "XRP" + }, + "taker_gets": "USD" + } + ] + })", + "invalidParams", + "Field 'taker_gets' is not an object"}, + UnsubscribeParamTestCaseBundle{ + "BooksItemTakerPaysNotObject", + R"({ + "books": + [ + { + "taker_gets": + { + "currency": "XRP" + }, + "taker_pays": "USD" + } + ] + })", + "invalidParams", + "Field 'taker_pays' is not an object"}, + UnsubscribeParamTestCaseBundle{ + "BooksItemTakerPaysMissingCurrency", + R"({ + "books": + [ + { + "taker_gets": + { + "currency": "XRP" + }, + "taker_pays": {} + } + ] + })", + "srcCurMalformed", + "Source currency is malformed."}, + UnsubscribeParamTestCaseBundle{ + "BooksItemTakerGetsMissingCurrency", + R"({ + "books": + [ + { + "taker_pays": + { + "currency": "XRP" + }, + "taker_gets": {} + } + ] + })", + "dstAmtMalformed", + "Destination amount/currency/issuer is malformed."}, + UnsubscribeParamTestCaseBundle{ + "BooksItemTakerPaysCurrencyNotString", + R"({ + "books": + [ + { + "taker_gets": + { + "currency": "XRP" + }, + "taker_pays": { + "currency": 1, + "issuer": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn" + } + } + ] + })", + "srcCurMalformed", + "Source currency is malformed."}, + UnsubscribeParamTestCaseBundle{ + "BooksItemTakerGetsCurrencyNotString", + R"({ + "books": + [ + { + "taker_pays": + { + "currency": "XRP" + }, + "taker_gets": { + "currency": 1, + "issuer": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn" + } + } + ] + })", + "dstAmtMalformed", + "Destination amount/currency/issuer is malformed."}, + UnsubscribeParamTestCaseBundle{ + "BooksItemTakerPaysInvalidCurrency", + R"({ + "books": + [ + { + "taker_gets": + { + "currency": "XRP" + }, + "taker_pays": { + "currency": "XXXXXX", + "issuer": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn" + } + } + ] + })", + "srcCurMalformed", + "Source currency is malformed."}, + UnsubscribeParamTestCaseBundle{ + "BooksItemTakerGetsInvalidCurrency", + R"({ + "books": + [ + { + "taker_pays": + { + "currency": "XRP" + }, + "taker_gets": { + "currency": "xxxxxxx", + "issuer": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn" + } + } + ] + })", + "dstAmtMalformed", + "Destination amount/currency/issuer is malformed."}, + UnsubscribeParamTestCaseBundle{ + "BooksItemTakerPaysMissingIssuer", + R"({ + "books": + [ + { + "taker_gets": + { + "currency": "XRP" + }, + "taker_pays": { + "currency": "USD" + } + } + ] + })", + "srcIsrMalformed", + "Invalid field 'taker_pays.issuer', expected non-XRP issuer."}, + UnsubscribeParamTestCaseBundle{ + "BooksItemTakerGetsMissingIssuer", + R"({ + "books": + [ + { + "taker_pays": + { + "currency": "XRP" + }, + "taker_gets": { + "currency": "USD" + } + } + ] + })", + "dstIsrMalformed", + "Invalid field 'taker_gets.issuer', expected non-XRP issuer."}, + UnsubscribeParamTestCaseBundle{ + "BooksItemTakerPaysIssuerNotString", + R"({ + "books": + [ + { + "taker_gets": + { + "currency": "XRP" + }, + "taker_pays": { + "currency": "USD", + "issuer": 1 + } + } + ] + })", + "invalidParams", + "takerPaysIssuerNotString"}, + UnsubscribeParamTestCaseBundle{ + "BooksItemTakerGetsIssuerNotString", + R"({ + "books": + [ + { + "taker_pays": + { + "currency": "XRP" + }, + "taker_gets": { + "currency": "USD", + "issuer": 1 + } + } + ] + })", + "invalidParams", + "taker_gets.issuer should be string"}, + UnsubscribeParamTestCaseBundle{ + "BooksItemTakerPaysInvalidIssuer", + R"({ + "books": + [ + { + "taker_gets": + { + "currency": "XRP" + }, + "taker_pays": { + "currency": "USD", + "issuer": "123" + } + } + ] + })", + "srcIsrMalformed", + "Source issuer is malformed."}, + UnsubscribeParamTestCaseBundle{ + "BooksItemTakerGetsInvalidIssuer", + R"({ + "books": + [ + { + "taker_pays": + { + "currency": "XRP" + }, + "taker_gets": { + "currency": "USD", + "issuer": "123" + } + } + ] + })", + "dstIsrMalformed", + "Invalid field 'taker_gets.issuer', bad issuer."}, + UnsubscribeParamTestCaseBundle{ + "BooksItemTakerGetsXRPHasIssuer", + R"({ + "books": + [ + { + "taker_pays": + { + "currency": "USD", + "issuer": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn" + }, + "taker_gets": { + "currency": "XRP", + "issuer": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn" + } + } + ] + })", + "dstIsrMalformed", + "Unneeded field 'taker_gets.issuer' for XRP currency " + "specification."}, + UnsubscribeParamTestCaseBundle{ + "BooksItemTakerPaysXRPHasIssuer", + R"({ + "books": + [ + { + "taker_pays": + { + "currency": "XRP", + "issuer": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn" + }, + "taker_gets": { + "currency": "USD", + "issuer": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn" + } + } + ] + })", + "srcIsrMalformed", + "Unneeded field 'taker_pays.issuer' for XRP currency " + "specification."}, + UnsubscribeParamTestCaseBundle{ + "BooksItemBadMartket", + R"({ + "books": + [ + { + "taker_pays": + { + "currency": "XRP" + }, + "taker_gets": { + "currency": "XRP" + } + } + ] + })", + "badMarket", + "badMarket"}, + UnsubscribeParamTestCaseBundle{ + "BooksItemInvalidBoth", + R"({ + "books": + [ + { + "taker_pays": + { + "currency": "XRP" + }, + "taker_gets": { + "currency": "USD", + "issuer": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn" + }, + "both": 0 + } + ] + })", + "invalidParams", + "bothNotBool"}, + }; +} + +INSTANTIATE_TEST_CASE_P( + RPCUnsubscribe, + UnsubscribeParameterTest, + ValuesIn(generateTestValuesForParametersTest()), + UnsubscribeParameterTest::NameGenerator{}); + +TEST_P(UnsubscribeParameterTest, InvalidParams) +{ + auto const testBundle = GetParam(); + runSpawn([&, this](auto& yield) { + auto const handler = AnyHandler{TestUnsubscribeHandler{mockBackendPtr, mockSubscriptionManagerPtr}}; + auto const req = json::parse(testBundle.testJson); + auto const output = handler.process(req, Context{std::ref(yield)}); + ASSERT_FALSE(output); + auto const err = RPC::makeError(output.error()); + EXPECT_EQ(err.at("error").as_string(), testBundle.expectedError); + EXPECT_EQ(err.at("error_message").as_string(), testBundle.expectedErrorMessage); + }); +} + +TEST_F(RPCUnsubscribeTest, EmptyResponse) +{ + runSpawn([&, this](auto& yield) { + auto const handler = AnyHandler{TestUnsubscribeHandler{mockBackendPtr, mockSubscriptionManagerPtr}}; + auto const output = handler.process(json::parse(R"({})"), Context{std::ref(yield), session_}); + ASSERT_TRUE(output); + EXPECT_TRUE(output->as_object().empty()); + }); +} + +TEST_F(RPCUnsubscribeTest, Streams) +{ + auto const input = json::parse( + R"({ + "streams": ["transactions_proposed","transactions","validations","manifests","book_changes","ledger"] + })"); + + MockSubscriptionManager* rawSubscriptionManagerPtr = + static_cast(mockSubscriptionManagerPtr.get()); + EXPECT_CALL(*rawSubscriptionManagerPtr, unsubLedger).Times(1); + EXPECT_CALL(*rawSubscriptionManagerPtr, unsubTransactions).Times(1); + EXPECT_CALL(*rawSubscriptionManagerPtr, unsubValidation).Times(1); + EXPECT_CALL(*rawSubscriptionManagerPtr, unsubManifest).Times(1); + EXPECT_CALL(*rawSubscriptionManagerPtr, unsubBookChanges).Times(1); + + runSpawn([&, this](auto& yield) { + auto const handler = AnyHandler{TestUnsubscribeHandler{mockBackendPtr, mockSubscriptionManagerPtr}}; + auto const output = handler.process(input, Context{std::ref(yield), session_}); + ASSERT_TRUE(output); + EXPECT_TRUE(output->as_object().empty()); + }); +} + +TEST_F(RPCUnsubscribeTest, Accounts) +{ + auto const input = json::parse(fmt::format( + R"({{ + "accounts": ["{}","{}"] + }})", + ACCOUNT, + ACCOUNT2)); + + MockSubscriptionManager* rawSubscriptionManagerPtr = + static_cast(mockSubscriptionManagerPtr.get()); + EXPECT_CALL(*rawSubscriptionManagerPtr, unsubAccount(RPC::accountFromStringStrict(ACCOUNT).value(), _)).Times(1); + EXPECT_CALL(*rawSubscriptionManagerPtr, unsubAccount(RPC::accountFromStringStrict(ACCOUNT2).value(), _)).Times(1); + + runSpawn([&, this](auto& yield) { + auto const handler = AnyHandler{TestUnsubscribeHandler{mockBackendPtr, mockSubscriptionManagerPtr}}; + auto const output = handler.process(input, Context{std::ref(yield), session_}); + ASSERT_TRUE(output); + EXPECT_TRUE(output->as_object().empty()); + }); +} + +TEST_F(RPCUnsubscribeTest, AccountsProposed) +{ + auto const input = json::parse(fmt::format( + R"({{ + "accounts_proposed": ["{}","{}"] + }})", + ACCOUNT, + ACCOUNT2)); + + MockSubscriptionManager* rawSubscriptionManagerPtr = + static_cast(mockSubscriptionManagerPtr.get()); + EXPECT_CALL(*rawSubscriptionManagerPtr, unsubProposedAccount(RPC::accountFromStringStrict(ACCOUNT).value(), _)) + .Times(1); + EXPECT_CALL(*rawSubscriptionManagerPtr, unsubProposedAccount(RPC::accountFromStringStrict(ACCOUNT2).value(), _)) + .Times(1); + + runSpawn([&, this](auto& yield) { + auto const handler = AnyHandler{TestUnsubscribeHandler{mockBackendPtr, mockSubscriptionManagerPtr}}; + auto const output = handler.process(input, Context{std::ref(yield), session_}); + ASSERT_TRUE(output); + EXPECT_TRUE(output->as_object().empty()); + }); +} + +TEST_F(RPCUnsubscribeTest, Books) +{ + auto const input = json::parse(fmt::format( + R"({{ + "books": [ + {{ + "taker_pays": {{ + "currency": "XRP" + }}, + "taker_gets": {{ + "currency": "USD", + "issuer": "{}" + }}, + "both": true + }} + ] + }})", + ACCOUNT)); + + auto const parsedBookMaybe = RPC::parseBook(input.as_object().at("books").as_array()[0].as_object()); + auto const book = std::get(parsedBookMaybe); + + MockSubscriptionManager* rawSubscriptionManagerPtr = + static_cast(mockSubscriptionManagerPtr.get()); + EXPECT_CALL(*rawSubscriptionManagerPtr, unsubBook(book, _)).Times(1); + EXPECT_CALL(*rawSubscriptionManagerPtr, unsubBook(ripple::reversed(book), _)).Times(1); + + runSpawn([&, this](auto& yield) { + auto const handler = AnyHandler{TestUnsubscribeHandler{mockBackendPtr, mockSubscriptionManagerPtr}}; + auto const output = handler.process(input, Context{std::ref(yield), session_}); + ASSERT_TRUE(output); + EXPECT_TRUE(output->as_object().empty()); + }); +} + +TEST_F(RPCUnsubscribeTest, SingleBooks) +{ + auto const input = json::parse(fmt::format( + R"({{ + "books": [ + {{ + "taker_pays": {{ + "currency": "XRP" + }}, + "taker_gets": {{ + "currency": "USD", + "issuer": "{}" + }} + }} + ] + }})", + ACCOUNT)); + + auto const parsedBookMaybe = RPC::parseBook(input.as_object().at("books").as_array()[0].as_object()); + auto const book = std::get(parsedBookMaybe); + + MockSubscriptionManager* rawSubscriptionManagerPtr = + static_cast(mockSubscriptionManagerPtr.get()); + EXPECT_CALL(*rawSubscriptionManagerPtr, unsubBook(book, _)).Times(1); + + runSpawn([&, this](auto& yield) { + auto const handler = AnyHandler{TestUnsubscribeHandler{mockBackendPtr, mockSubscriptionManagerPtr}}; + auto const output = handler.process(input, Context{std::ref(yield), session_}); + ASSERT_TRUE(output); + EXPECT_TRUE(output->as_object().empty()); + }); +} diff --git a/unittests/util/MockSubscriptionManager.h b/unittests/util/MockSubscriptionManager.h index 8bcfea8a..972ad4e8 100644 --- a/unittests/util/MockSubscriptionManager.h +++ b/unittests/util/MockSubscriptionManager.h @@ -60,7 +60,7 @@ public: MOCK_METHOD(void, subAccount, (ripple::AccountID const&, session_ptr&), ()); - MOCK_METHOD(void, unsubAccount, (ripple::AccountID const&, session_ptr&), ()); + MOCK_METHOD(void, unsubAccount, (ripple::AccountID const&, session_ptr const&), ()); MOCK_METHOD(void, subBook, (ripple::Book const&, session_ptr), ());