| /** |
| @section license License |
| |
| Licensed to the Apache Software Foundation (ASF) under one |
| or more contributor license agreements. See the NOTICE file |
| distributed with this work for additional information |
| regarding copyright ownership. The ASF licenses this file |
| to you under the Apache License, Version 2.0 (the |
| "License"); you may not use this file except in compliance |
| with the License. You may obtain a copy of the License at |
| |
| http://www.apache.org/licenses/LICENSE-2.0 |
| |
| Unless required by applicable law or agreed to in writing, software |
| distributed under the License is distributed on an "AS IS" BASIS, |
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| See the License for the specific language governing permissions and |
| limitations under the License. |
| */ |
| #define CATCH_CONFIG_EXTERNAL_INTERFACES |
| |
| #include <catch2/catch_test_macros.hpp> /* catch unit-test framework */ |
| #include <catch2/reporters/catch_reporter_event_listener.hpp> |
| #include <catch2/reporters/catch_reporter_registrars.hpp> |
| #include <catch2/interfaces/catch_interfaces_config.hpp> |
| |
| #include <sys/types.h> |
| #include <sys/socket.h> |
| #include <sys/stat.h> |
| #include <sys/un.h> |
| #include <sys/wait.h> |
| #include <fcntl.h> |
| #include <signal.h> |
| #include <stdio.h> |
| #include <unistd.h> |
| |
| #include <thread> |
| #include <future> |
| #include <chrono> |
| #include <fstream> |
| #include <cerrno> |
| #include <cstring> |
| #include <cstdlib> |
| #include <vector> |
| #include <functional> |
| |
| #include "swoc/swoc_file.h" |
| |
| #include <swoc/BufferWriter.h> |
| #include "ts/ts.h" |
| |
| #include "mgmt/rpc/jsonrpc/JsonRPC.h" |
| #include "mgmt/rpc/server/RPCServer.h" |
| #include "mgmt/rpc/server/IPCSocketServer.h" |
| |
| #include "shared/rpc/IPCSocketClient.h" |
| #include "iocore/eventsystem/EventSystem.h" |
| #include "tscore/Layout.h" |
| #include "tscore/ink_sock.h" |
| #include "iocore/utils/diags.i" |
| |
| #define DEFINE_JSONRPC_PROTO_FUNCTION(fn) swoc::Rv<YAML::Node> fn(std::string_view const &, const YAML::Node ¶ms) |
| |
| namespace fs = swoc::file; |
| |
| namespace rpc |
| { |
| bool |
| test_remove_handler(std::string_view name) |
| { |
| return rpc::JsonRPCManager::instance().remove_handler(name); |
| } |
| |
| template <typename Func> |
| inline bool |
| add_method_handler(const std::string &name, Func &&call) |
| { |
| return rpc::JsonRPCManager::instance().add_method_handler(name, std::forward<Func>(call), nullptr, {}); |
| } |
| } // namespace rpc |
| |
| namespace |
| { |
| constexpr std::string_view rpc_test_dir_template{"ats_rpc_XXXXXX"}; |
| constexpr std::string_view rpc_test_socket_name{"s"}; |
| constexpr std::string_view rpc_test_lock_name{"l"}; |
| constexpr size_t max_rpc_socket_path_size{sizeof(sockaddr_un::sun_path) - 1}; |
| |
| fs::path rpcTestDir; |
| std::string sockPath; |
| std::string lockPath; |
| constexpr int default_backlog{5}; |
| constexpr int default_maxRetriesOnTransientErrors{64}; |
| constexpr size_t default_incoming_req_max_size{32000 * 3}; |
| DbgCtl dbg_ctl{"rpc.test.client"}; |
| |
| /** Prepare JSONRPC socket paths beneath @a base. |
| * |
| * This owns creation of the per-run test directory and publishes the socket |
| * and lock paths shared by the JSONRPC test server and clients. |
| * |
| * @param[in] base Candidate parent directory for the test directory. |
| * @param[out] error Reason setup failed, if a usable directory was not created. |
| * @return @c true if the socket and lock paths are ready for this test run. |
| */ |
| bool |
| try_setup_rpc_test_paths(fs::path const &base, std::string &error) |
| { |
| auto const dir_template = (base / rpc_test_dir_template).string(); |
| auto const socket_path = (fs::path{dir_template} / rpc_test_socket_name).string(); |
| |
| if (socket_path.size() > max_rpc_socket_path_size) { |
| error = "JSONRPC test socket path is too long under " + base.string() + ": " + socket_path; |
| return false; |
| } |
| |
| std::vector<char> mutable_template{dir_template.begin(), dir_template.end()}; |
| mutable_template.push_back('\0'); |
| |
| char *created_dir = mkdtemp(mutable_template.data()); |
| if (created_dir == nullptr) { |
| error = "Failed to create JSONRPC test directory under " + base.string() + ": " + std::strerror(errno); |
| return false; |
| } |
| |
| rpcTestDir = fs::path{created_dir}; |
| sockPath = (rpcTestDir / rpc_test_socket_name).string(); |
| lockPath = (rpcTestDir / rpc_test_lock_name).string(); |
| return true; |
| } |
| |
| /** Prepare JSONRPC socket paths for the test run. |
| * |
| * This prefers the environment temporary directory, then falls back to @c /tmp |
| * when the generated Unix-domain socket path would be too long or setup fails. |
| * |
| * @param[out] error Reason setup failed, if no candidate directory works. |
| * @return @c true if the socket and lock paths are ready for this test run. |
| */ |
| bool |
| setup_rpc_test_paths(std::string &error) |
| { |
| if (try_setup_rpc_test_paths(fs::temp_directory_path(), error)) { |
| error.clear(); |
| return true; |
| } |
| if (try_setup_rpc_test_paths(fs::path{"/tmp"}, error)) { |
| error.clear(); |
| return true; |
| } |
| return false; |
| } |
| |
| } // end anonymous namespace |
| |
| struct RPCServerTestListener : Catch::EventListenerBase { |
| using EventListenerBase::EventListenerBase; // inherit constructor |
| ~RPCServerTestListener(); |
| |
| // The whole test run starting |
| void |
| testRunStarting(Catch::TestRunInfo const & /* testRunInfo ATS_UNUSED */) override |
| { |
| std::string setup_error; |
| bool const setup_ok = setup_rpc_test_paths(setup_error); |
| INFO(setup_error); |
| REQUIRE(setup_ok); |
| |
| Layout::create(); |
| init_diags("rpc", nullptr); |
| RecProcessInit(); |
| |
| signal(SIGPIPE, SIG_IGN); |
| |
| ink_event_system_init(EVENT_SYSTEM_MODULE_PUBLIC_VERSION); |
| eventProcessor.start(2, 1048576); |
| |
| // EThread *main_thread = new EThread; |
| main_thread = std::make_unique<EThread>(); |
| main_thread->set_specific(); |
| |
| rpc::config::RPCConfig serverConfig; |
| |
| auto confStr{R"({"rpc": { "enabled": true, "unix": { "lock_path_name": ")" + lockPath + R"(", "sock_path_name": ")" + sockPath + |
| R"(", "backlog": 5,"max_retry_on_transient_errors": 64, "incoming_request_max_size": 32000 }}})"}; |
| YAML::Node configNode = YAML::Load(confStr); |
| serverConfig.load(configNode["rpc"]); |
| try { |
| jsonrpcServer = new rpc::RPCServer(serverConfig); |
| |
| jsonrpcServer->start_thread(); |
| } catch (std::exception const &ex) { |
| Dbg(dbg_ctl, "Oops: %s", ex.what()); |
| } |
| } |
| |
| // The whole test run ending |
| void |
| testRunEnded(Catch::TestRunStats const & /* testRunStats ATS_UNUSED */) override |
| { |
| if (jsonrpcServer) { |
| delete jsonrpcServer; // will stop the thread |
| } |
| |
| std::error_code ec; |
| if (!rpcTestDir.empty()) { |
| fs::remove_all(rpcTestDir, ec); |
| } |
| } |
| |
| private: |
| std::unique_ptr<EThread> main_thread; |
| }; |
| CATCH_REGISTER_LISTENER(RPCServerTestListener) |
| |
| RPCServerTestListener::~RPCServerTestListener() {} |
| |
| void |
| restart_json_rpc_server(YAML::Node n) |
| { |
| rpc::config::RPCConfig serverConfig; |
| serverConfig.load(n["rpc"]); |
| |
| if (jsonrpcServer) { |
| delete jsonrpcServer; |
| } |
| |
| try { |
| jsonrpcServer = new rpc::RPCServer(serverConfig); |
| jsonrpcServer->start_thread(); |
| } catch (std::exception const &ex) { |
| Dbg(dbg_ctl, "Oops: %s", ex.what()); |
| } |
| } |
| |
| DEFINE_JSONRPC_PROTO_FUNCTION(some_foo) // id, params |
| { |
| swoc::Rv<YAML::Node> resp; |
| int dur{1}; |
| try { |
| dur = params["duration"].as<int>(); |
| } catch (...) { |
| } |
| INFO("Sleeping for " << dur << "s"); |
| std::this_thread::sleep_for(std::chrono::seconds(dur)); |
| resp.result()["res"] = "ok"; |
| resp.result()["duration"] = dur; |
| |
| INFO("Done sleeping"); |
| return resp; |
| } |
| namespace |
| { |
| // Handy class to avoid manually disconnecting the socket. |
| // TODO: should it also connect? |
| struct ScopedLocalSocket : shared::rpc::IPCSocketClient { |
| using super = shared::rpc::IPCSocketClient; |
| // TODO, use another path. |
| ScopedLocalSocket() : IPCSocketClient(sockPath) {} |
| ~ScopedLocalSocket() { IPCSocketClient::disconnect(); } |
| |
| template <std::size_t N> |
| void |
| send_in_chunks(std::string_view data, int disconnect_after_chunk_n = -1) |
| { |
| int chunk_number{1}; |
| auto chunks = chunk<N>(data); |
| for (auto &&part : chunks) { |
| if (super::_safe_write(_sock, part.c_str(), part.size()) == -1) { |
| Dbg(dbg_ctl, "error sending message :%s", std ::strerror(errno)); |
| break; |
| } |
| |
| if (disconnect_after_chunk_n == chunk_number) { |
| Dbg(dbg_ctl, "Disconnecting it after chunk %d", chunk_number); |
| super::disconnect(); |
| return; |
| } |
| ++chunk_number; |
| } |
| } |
| |
| // basic read, if fail, why it fail is irrelevant in this test. |
| std::string |
| read() |
| { |
| std::string buf; |
| auto ret = super::read_all(buf); |
| if (ret == ReadStatus::NO_ERROR) { |
| return buf; |
| } |
| return {}; |
| } |
| // small wrapper function to deal with the flow. |
| std::string |
| query(std::string_view msg) |
| { |
| std::string buf; |
| auto ret = connect().send(msg).read_all(buf); |
| if (ret == ReadStatus::NO_ERROR) { |
| return buf; |
| } |
| |
| return {}; |
| } |
| |
| private: |
| template <typename Iter, std::size_t N> |
| std::array<std::string, N> |
| chunk_impl(Iter from, Iter to) |
| { |
| const std::size_t size = std::distance(from, to); |
| if (size <= N) { |
| return { |
| std::string{from, to} |
| }; |
| } |
| std::size_t index{0}; |
| std::array<std::string, N> ret; |
| const std::size_t each_part = size / N; |
| const std::size_t remainder = size % N; |
| |
| for (auto it = from; it != to;) { |
| if (std::size_t rem = std::distance(it, to); rem == (each_part + remainder)) { |
| ret[index++] = std::string{it, it + rem}; |
| break; |
| } |
| ret[index++] = std::string{it, it + each_part}; |
| std::advance(it, each_part); |
| } |
| |
| return ret; |
| } |
| |
| template <std::size_t N> |
| auto |
| chunk(std::string_view v) |
| { |
| return chunk_impl<std::string_view::const_iterator, N>(v.begin(), v.end()); |
| } |
| }; |
| |
| struct TestableIPCSocketClient : shared::rpc::IPCSocketClient { |
| using shared::rpc::IPCSocketClient::_safe_write; |
| }; |
| |
| // helper function to send a request and update the promise when the response is done. |
| // This is to be used in a multithread test. |
| void |
| send_request(std::string json, std::promise<std::string> p) |
| { |
| ScopedLocalSocket rpc_client; |
| auto resp = rpc_client.query(json); |
| p.set_value(resp); |
| } |
| |
| TEST_CASE("IPCSocketClient write returns when the peer stops reading", "[socket][client]") |
| { |
| int fds[2]; |
| REQUIRE(::socketpair(AF_UNIX, SOCK_STREAM, 0, fds) == 0); |
| |
| int const flags = ::fcntl(fds[0], F_GETFL, 0); |
| REQUIRE(flags >= 0); |
| REQUIRE(::fcntl(fds[0], F_SETFL, flags | O_NONBLOCK) == 0); |
| |
| std::vector<char> fill(4096, 'x'); |
| while (true) { |
| ssize_t const ret = ::write(fds[0], fill.data(), fill.size()); |
| if (ret < 0) { |
| REQUIRE((errno == EAGAIN || errno == EWOULDBLOCK)); |
| break; |
| } |
| REQUIRE(ret > 0); |
| } |
| |
| TestableIPCSocketClient rpc_client; |
| pid_t const pid = ::fork(); |
| REQUIRE(pid >= 0); |
| if (pid == 0) { |
| ::close(fds[1]); |
| char const byte = 'x'; |
| auto const ret = rpc_client._safe_write(fds[0], &byte, 1); |
| auto const err = errno; |
| ::close(fds[0]); |
| _exit(ret == -1 && err == ETIMEDOUT ? 0 : 1); |
| } |
| |
| int status = 0; |
| bool child_exited = false; |
| auto const child_deadline = std::chrono::steady_clock::now() + std::chrono::seconds(10); |
| while (std::chrono::steady_clock::now() < child_deadline) { |
| auto const wait_ret = ::waitpid(pid, &status, WNOHANG); |
| if (wait_ret == pid) { |
| child_exited = true; |
| break; |
| } |
| REQUIRE(wait_ret == 0); |
| std::this_thread::sleep_for(std::chrono::milliseconds(100)); |
| } |
| |
| if (!child_exited) { |
| auto const wait_ret = ::waitpid(pid, &status, WNOHANG); |
| if (wait_ret == pid) { |
| child_exited = true; |
| } else { |
| REQUIRE(wait_ret == 0); |
| } |
| } |
| |
| if (!child_exited) { |
| ::kill(pid, SIGKILL); |
| REQUIRE(::waitpid(pid, &status, 0) == pid); |
| FAIL("_safe_write did not return when the nonblocking socket stayed unwritable"); |
| } |
| |
| ::close(fds[0]); |
| ::close(fds[1]); |
| |
| REQUIRE(WIFEXITED(status)); |
| REQUIRE(WEXITSTATUS(status) == 0); |
| } |
| } // namespace |
| TEST_CASE("Sending 'concurrent' requests to the rpc server.", "[thread]") |
| { |
| SECTION("A registered handlers") |
| { |
| rpc::add_method_handler("some_foo", &some_foo); |
| rpc::add_method_handler("some_foo2", &some_foo); |
| |
| std::promise<std::string> p1; |
| std::promise<std::string> p2; |
| auto fut1 = p1.get_future(); |
| auto fut2 = p2.get_future(); |
| |
| REQUIRE_NOTHROW([&]() { |
| // Two different clients, on the same server, as the server is an Unix Domain Socket, it should handle all this |
| // properly, in any case we just run the basic smoke test for our server. |
| auto t1 = std::thread(&send_request, R"({"jsonrpc": "2.0", "method": "some_foo", "params": {"duration": 1}, "id": "aBcD"})", |
| std::move(p1)); |
| auto t2 = std::thread(&send_request, R"({"jsonrpc": "2.0", "method": "some_foo", "params": {"duration": 1}, "id": "eFgH"})", |
| std::move(p2)); |
| // wait to get the promise set. |
| fut1.wait(); |
| fut2.wait(); |
| |
| // the expected |
| std::string_view expected1{R"({"jsonrpc": "2.0", "result": {"res": "ok", "duration": "1"}, "id": "aBcD"})"}; |
| std::string_view expected2{R"({"jsonrpc": "2.0", "result": {"res": "ok", "duration": "1"}, "id": "eFgH"})"}; |
| |
| CHECK(fut1.get() == expected1); |
| CHECK(fut2.get() == expected2); |
| |
| t1.join(); |
| t2.join(); |
| }()); |
| } |
| } |
| |
| std::string |
| random_string(std::string::size_type length) |
| { |
| auto randchar = []() -> char { |
| const char charset[] = "0123456789" |
| "ABCDEFGHIJKLMNOPQRSTUVWXYZ" |
| "abcdefghijklmnopqrstuvwxyz"; |
| const size_t max_index = (sizeof(charset) - 1); |
| return charset[rand() % max_index]; |
| }; |
| std::string str(length, 0); |
| std::generate_n(str.begin(), length, randchar); |
| return str; |
| } |
| |
| DEFINE_JSONRPC_PROTO_FUNCTION(do_nothing) // id, params, resp |
| { |
| swoc::Rv<YAML::Node> resp; |
| resp.result()["size"] = params["msg"].as<std::string>().size(); |
| return resp; |
| } |
| |
| TEST_CASE("Basic message sending to a running server", "[socket]") |
| { |
| REQUIRE(rpc::add_method_handler("do_nothing", &do_nothing)); |
| SECTION("Basic single request to the rpc server") |
| { |
| const int S{500}; |
| auto json{R"({"jsonrpc": "2.0", "method": "do_nothing", "params": {"msg":")" + random_string(S) + R"("}, "id":"EfGh-1"})"}; |
| REQUIRE_NOTHROW([&]() { |
| ScopedLocalSocket rpc_client; |
| auto resp = rpc_client.query(json); |
| |
| REQUIRE(resp == R"({"jsonrpc": "2.0", "result": {"size": ")" + std::to_string(S) + R"("}, "id": "EfGh-1"})"); |
| }()); |
| } |
| REQUIRE(rpc::test_remove_handler("do_nothing")); |
| } |
| |
| TEST_CASE("JSONRPC socket inode permissions reflect restricted_api config", "[socket][permissions]") |
| { |
| SECTION("restricted_api=true yields mode 0700 on the socket inode") |
| { |
| auto confStr{ |
| R"({"rpc": { "enabled": true, "unix": { "lock_path_name": ")" + lockPath + R"(", "sock_path_name": ")" + sockPath + |
| R"(", "backlog": 5, "max_retry_on_transient_errors": 64, "incoming_request_max_size": 32000, "restricted_api": true }}})"}; |
| YAML::Node n = YAML::Load(confStr); |
| restart_json_rpc_server(n); |
| |
| // Restore the default test server configuration on scope exit, even if an |
| // assertion below fails (REQUIRE throws), so subsequent test cases are not |
| // left running against the restricted-api server. |
| struct ConfigRestorer { |
| std::function<void()> restore; |
| ~ConfigRestorer() |
| { |
| try { |
| restore(); |
| } catch (...) { |
| } |
| } |
| } config_restorer{[&]() { |
| auto restoreStr{R"({"rpc": { "enabled": true, "unix": { "lock_path_name": ")" + lockPath + R"(", "sock_path_name": ")" + |
| sockPath + |
| R"(", "backlog": 5, "max_retry_on_transient_errors": 64, "incoming_request_max_size": 32000 }}})"}; |
| YAML::Node restoreN = YAML::Load(restoreStr); |
| restart_json_rpc_server(restoreN); |
| }}; |
| |
| struct stat st { |
| }; |
| REQUIRE(::stat(sockPath.c_str(), &st) == 0); |
| CHECK(S_ISSOCK(st.st_mode)); |
| CHECK((st.st_mode & 0777) == 0700); |
| } |
| } |
| |
| TEST_CASE("Sending a message bigger than the internal server's buffer. 32000", "[buffer][error]") |
| { |
| REQUIRE(rpc::add_method_handler("do_nothing32000", &do_nothing)); |
| const int S{32000}; // + the rest of the json message. |
| auto json{R"({"jsonrpc": "2.0", "method": "do_nothing32000", "params": {"msg":")" + random_string(S) + R"("}, "id":"32k_1"})"}; |
| |
| SECTION("Message larger than the the accepted size.") |
| { |
| REQUIRE_NOTHROW([&]() { |
| ScopedLocalSocket rpc_client; |
| auto resp = rpc_client.query(json); |
| REQUIRE(resp.empty()); |
| }()); |
| } |
| |
| SECTION("Retry the big message after reconfigure(restart rpc server) the incoming request size limit.") |
| { |
| auto confStr{R"({"rpc": { "enabled": true, "unix": { "lock_path_name": ")" + lockPath + R"(", "sock_path_name": ")" + sockPath + |
| R"(", "backlog": 5,"max_retry_on_transient_errors": 64, "incoming_request_max_size": 62000 }}})"}; |
| YAML::Node n = YAML::Load(confStr); |
| restart_json_rpc_server(n); |
| REQUIRE_NOTHROW([&]() { |
| ScopedLocalSocket rpc_client; |
| auto resp = rpc_client.query(json); |
| REQUIRE(resp == R"({"jsonrpc": "2.0", "result": {"size": "32000"}, "id": "32k_1"})"); |
| }()); |
| |
| const int oversized_message_size{64000}; |
| auto oversized_json{R"({"jsonrpc": "2.0", "method": "do_nothing32000", "params": {"msg":")" + |
| random_string(oversized_message_size) + R"("}, "id":"over-limit"})"}; |
| REQUIRE_NOTHROW([&]() { |
| ScopedLocalSocket rpc_client; |
| auto resp = rpc_client.query(oversized_json); |
| REQUIRE(resp.empty()); |
| }()); |
| } |
| REQUIRE(rpc::test_remove_handler("do_nothing32000")); |
| } |
| |
| TEST_CASE("Test with invalid json message", "[socket]") |
| { |
| REQUIRE(rpc::add_method_handler("do_nothing", &do_nothing)); |
| |
| SECTION("A rpc server") |
| { |
| const int S{10}; |
| auto json{R"({"jsonrpc": "2.0", "method": "do_nothing", "params": { "msg": ")" + random_string(S) + R"("}, "id": "EfGh})"}; |
| REQUIRE_NOTHROW([&]() { |
| ScopedLocalSocket rpc_client; |
| auto resp = rpc_client.query(json); |
| |
| CHECK(resp == R"({"jsonrpc": "2.0", "error": {"code": -32700, "message": "Parse error"}})"); |
| }()); |
| } |
| REQUIRE(rpc::test_remove_handler("do_nothing")); |
| } |
| |
| TEST_CASE("Test with chunks", "[socket][chunks]") |
| { |
| REQUIRE(rpc::add_method_handler("do_nothing", &do_nothing)); |
| |
| SECTION("Sending request by chunks") |
| { |
| const int S{10}; |
| auto json{R"({"jsonrpc": "2.0", "method": "do_nothing", "params": { "msg": ")" + random_string(S) + |
| R"("}, "id": "chunk-parts-3"})"}; |
| |
| REQUIRE_NOTHROW([&]() { |
| ScopedLocalSocket rpc_client; |
| using namespace std::chrono_literals; |
| rpc_client.connect(); |
| rpc_client.send_in_chunks<3>(json); |
| auto resp = rpc_client.read(); |
| REQUIRE(resp == R"({"jsonrpc": "2.0", "result": {"size": ")" + std::to_string(S) + R"("}, "id": "chunk-parts-3"})"); |
| }()); |
| } |
| REQUIRE(rpc::test_remove_handler("do_nothing")); |
| } |
| |
| TEST_CASE("Test with chunks - disconnect after second part", "[socket][chunks]") |
| { |
| REQUIRE(rpc::add_method_handler("do_nothing", &do_nothing)); |
| |
| SECTION("Sending request by chunks") |
| { |
| const int S{4000}; |
| auto json{R"({"jsonrpc": "2.0", "method": "do_nothing", "params": { "msg": ")" + random_string(S) + |
| R"("}, "id": "chunk-parts-3-2"})"}; |
| |
| REQUIRE_NOTHROW([&]() { |
| ScopedLocalSocket rpc_client; |
| using namespace std::chrono_literals; |
| rpc_client.connect(); |
| rpc_client.send_in_chunks<3>(json, 2); |
| // read will fail. |
| auto resp = rpc_client.read(); |
| REQUIRE(resp == ""); |
| }()); |
| } |
| REQUIRE(rpc::test_remove_handler("do_nothing")); |
| } |
| |
| TEST_CASE("Test with chunks - incomplete message", "[socket][chunks]") |
| { |
| REQUIRE(rpc::add_method_handler("do_nothing", &do_nothing)); |
| |
| SECTION("Sending request by chunks, broken message") |
| { |
| const int S{50}; |
| auto json{R"({"jsonrpc": "2.0", "method": "do_nothing", "params": { "msg": ")" + random_string(S) + |
| R"("}, "id": "chunk-parts-3)"}; |
| // ^ missing-> "} |
| |
| REQUIRE_NOTHROW([&]() { |
| ScopedLocalSocket rpc_client; |
| using namespace std::chrono_literals; |
| rpc_client.connect(); |
| rpc_client.send_in_chunks<3>(json); |
| auto resp = rpc_client.read(); |
| REQUIRE(resp == R"({"jsonrpc": "2.0", "error": {"code": -32700, "message": "Parse error"}})"); |
| }()); |
| } |
| REQUIRE(rpc::test_remove_handler("do_nothing")); |
| } |
| |
| // Enable toggle |
| TEST_CASE("Test rpc enable toggle feature - default enabled.", "[default values]") |
| { |
| rpc::config::RPCConfig serverConfig; |
| REQUIRE(serverConfig.is_enabled() == true); |
| } |
| |
| TEST_CASE("Test rpc enable toggle feature. Enabled by configuration", "[rpc][enabled]") |
| { |
| rpc::config::RPCConfig serverConfig; |
| |
| auto confStr{R"({"rpc": {"enabled": true}})"}; |
| std::cout << "'" << confStr << "'" << std::endl; |
| YAML::Node configNode = YAML::Load(confStr); |
| serverConfig.load(configNode["rpc"]); |
| REQUIRE(serverConfig.is_enabled() == true); |
| } |
| |
| TEST_CASE("Test rpc enable toggle feature. Disabled by configuration", "[rpc][disabled]") |
| { |
| rpc::config::RPCConfig serverConfig; |
| |
| auto confStr{R"({"rpc": {"enabled":false}})"}; |
| |
| REQUIRE_NOTHROW([&]() { |
| YAML::Node configNode = YAML::Load(confStr); |
| serverConfig.load(configNode["rpc"]); |
| }()); |
| REQUIRE(serverConfig.is_enabled() == false); |
| } |
| |
| // TEST UDS Server configuration |
| namespace |
| { |
| namespace trp = rpc::comm; |
| // This class is defined to get access to the protected config object inside the IPCSocketServer class. |
| struct LocalSocketTest : public trp::IPCSocketServer { |
| inline static const std::string _name = "LocalSocketTest"; |
| bool |
| configure(YAML::Node const ¶ms) override |
| { |
| return trp::IPCSocketServer::configure(params); |
| } |
| void |
| run() override |
| { |
| } |
| std::error_code |
| init() override |
| { |
| return trp::IPCSocketServer::init(); |
| } |
| bool |
| stop() override |
| { |
| return true; |
| } |
| std::string const & |
| name() const override |
| { |
| return _name; |
| } |
| trp::IPCSocketServer::Config const & |
| get_conf() const |
| { |
| return _conf; |
| } |
| }; |
| } // namespace |
| |
| TEST_CASE("Test configuration parsing from a YAML node. UDS values", "[string]") |
| { |
| rpc::config::RPCConfig serverConfig; |
| |
| auto confStr{R"({"rpc": { "enabled": true, "unix": { "lock_path_name": ")" + lockPath + R"(", "sock_path_name": ")" + sockPath + |
| R"(", "backlog": 5,"max_retry_on_transient_errors": 64 }}})"}; |
| YAML::Node configNode = YAML::Load(confStr); |
| serverConfig.load(configNode["rpc"]); |
| |
| REQUIRE(serverConfig.get_comm_type() == rpc::config::RPCConfig::CommType::UNIX); |
| |
| auto socket = std::make_unique<LocalSocketTest>(); |
| auto const ret = socket->configure(serverConfig.get_comm_config_params()); |
| REQUIRE(ret); |
| REQUIRE(socket->get_conf().backlog == default_backlog); |
| REQUIRE(socket->get_conf().maxRetriesOnTransientErrors == default_maxRetriesOnTransientErrors); |
| REQUIRE(socket->get_conf().sockPathName == sockPath); |
| REQUIRE(socket->get_conf().lockPathName == lockPath); |
| REQUIRE(socket->get_conf().incomingRequestMaxBufferSize == default_incoming_req_max_size); |
| } |
| |
| TEST_CASE("Test configuration parsing from a file. UDS Server", "[file]") |
| { |
| fs::path configPath = fs::path("tests/config") / "jsonrpc.yaml"; |
| |
| // define here to later compare. |
| std::string sockPathName{configPath.string() + "jsonrpc20_test2.sock"}; |
| std::string lockPathName{configPath.string() + "jsonrpc20_test2.lock"}; |
| |
| auto confStr{R"({"rpc": { "enabled": true, "unix": { "lock_path_name": ")" + lockPathName + R"(", "sock_path_name": ")" + |
| sockPathName + R"(", "backlog": 5,"max_retry_on_transient_errors": 64 }}})"}; |
| // write the config. |
| std::ofstream ofs(configPath.string(), std::ofstream::out); |
| // Yes, we write json into the yaml, remember, YAML is a superset of JSON, yaml parser can handle this. |
| ofs << confStr; |
| ofs.close(); |
| |
| rpc::config::RPCConfig serverConfig; |
| // on any error reading the file, default values will be used. |
| serverConfig.load_from_file(configPath.string()); |
| |
| REQUIRE(serverConfig.get_comm_type() == rpc::config::RPCConfig::CommType::UNIX); |
| |
| auto socket = std::make_unique<LocalSocketTest>(); |
| auto const &ret = socket->configure(serverConfig.get_comm_config_params()); |
| REQUIRE(ret); |
| REQUIRE(socket->get_conf().backlog == 5); |
| REQUIRE(socket->get_conf().maxRetriesOnTransientErrors == 64); |
| REQUIRE(socket->get_conf().sockPathName == sockPathName); |
| REQUIRE(socket->get_conf().lockPathName == lockPathName); |
| } |