From 1d88893715ac27606380363b0909efa729556e46 Mon Sep 17 00:00:00 2001 From: Antonio Andelic Date: Thu, 17 Feb 2022 10:36:10 +0100 Subject: [PATCH] Add Websocket e2e tests (#353) --- src/communication/websocket/auth.cpp | 3 +- src/communication/websocket/session.cpp | 14 +- src/query/interpreter.cpp | 2 +- tests/e2e/CMakeLists.txt | 1 + tests/e2e/memgraph.py | 44 +-- tests/e2e/runner.py | 58 ++-- tests/e2e/websocket/CMakeLists.txt | 10 + tests/e2e/websocket/common.hpp | 309 ++++++++++++++++++++ tests/e2e/websocket/memgraph-selfsigned.crt | 23 ++ tests/e2e/websocket/memgraph-selfsigned.key | 28 ++ tests/e2e/websocket/websocket.cpp | 68 +++++ tests/e2e/websocket/websocket_ssl.cpp | 76 +++++ tests/e2e/websocket/workloads.yaml | 34 +++ 13 files changed, 614 insertions(+), 56 deletions(-) create mode 100644 tests/e2e/websocket/CMakeLists.txt create mode 100644 tests/e2e/websocket/common.hpp create mode 100644 tests/e2e/websocket/memgraph-selfsigned.crt create mode 100644 tests/e2e/websocket/memgraph-selfsigned.key create mode 100644 tests/e2e/websocket/websocket.cpp create mode 100644 tests/e2e/websocket/websocket_ssl.cpp create mode 100644 tests/e2e/websocket/workloads.yaml diff --git a/src/communication/websocket/auth.cpp b/src/communication/websocket/auth.cpp index cee3cad2d..dbf30ff0b 100644 --- a/src/communication/websocket/auth.cpp +++ b/src/communication/websocket/auth.cpp @@ -16,11 +16,12 @@ namespace communication::websocket { bool SafeAuth::Authenticate(const std::string &username, const std::string &password) const { + // TODO: Make ReadLock after dealing with Authenticate return auth_->Lock()->Authenticate(username, password).has_value(); } bool SafeAuth::HasUserPermission(const std::string &username, const auth::Permission permission) const { - if (const auto user = auth_->Lock()->GetUser(username); user) { + if (const auto user = auth_->ReadLock()->GetUser(username); user) { return user->GetPermissions().Has(permission) == auth::PermissionLevel::GRANT; } return false; diff --git a/src/communication/websocket/session.cpp b/src/communication/websocket/session.cpp index d7513cfb8..8d2711d71 100644 --- a/src/communication/websocket/session.cpp +++ b/src/communication/websocket/session.cpp @@ -80,6 +80,7 @@ bool Session::Run() { return false; } + authenticated_ = !auth_.HasAnyUsers(); connected_.store(true, std::memory_order_relaxed); // run on the strand @@ -120,13 +121,14 @@ void Session::DoWrite() { void Session::OnWrite(boost::beast::error_code ec, size_t /*bytes_transferred*/) { messages_.pop_front(); - if (ec) { - return LogError(ec, "write"); - } if (close_) { DoShutdown(); return; } + if (ec) { + close_ = true; + return LogError(ec, "write"); + } if (!messages_.empty()) { DoWrite(); } @@ -149,10 +151,10 @@ void Session::DoClose() { } void Session::OnClose(boost::beast::error_code ec) { + connected_.store(false, std::memory_order_relaxed); if (ec) { return LogError(ec, "close"); } - connected_.store(false, std::memory_order_relaxed); } utils::BasicResult Session::Authorize(const nlohmann::json &creds) { @@ -194,9 +196,9 @@ void Session::OnRead(const boost::beast::error_code ec, const size_t /*bytes_tra response["success"] = true; response["message"] = "User has been successfully authenticated!"; MG_ASSERT(messages_.empty()); + authenticated_ = true; messages_.push_back(make_shared(response.dump())); DoWrite(); - authenticated_ = true; } catch (const nlohmann::json::out_of_range &out_of_range) { const auto err_msg = fmt::format("Invalid JSON for authentication received: {}!", out_of_range.what()); spdlog::error(err_msg); @@ -213,7 +215,7 @@ void Session::OnRead(const boost::beast::error_code ec, const size_t /*bytes_tra DoRead(); } -bool Session::IsAuthenticated() const { return authenticated_ || !auth_.HasAnyUsers(); } +bool Session::IsAuthenticated() const { return authenticated_; } void Session::DoShutdown() { std::visit(utils::Overloaded{[this](SSLWebSocket &ssl_ws) { diff --git a/src/query/interpreter.cpp b/src/query/interpreter.cpp index e119e352b..f538d2769 100644 --- a/src/query/interpreter.cpp +++ b/src/query/interpreter.cpp @@ -2314,7 +2314,7 @@ void Interpreter::Commit() { reset_necessary_members(); - SPDLOG_DEBUG("Finished comitting the transaction"); + SPDLOG_DEBUG("Finished committing the transaction"); } void Interpreter::AdvanceCommand() { diff --git a/tests/e2e/CMakeLists.txt b/tests/e2e/CMakeLists.txt index 7660e74e1..12d9969e1 100644 --- a/tests/e2e/CMakeLists.txt +++ b/tests/e2e/CMakeLists.txt @@ -14,5 +14,6 @@ add_subdirectory(streams) add_subdirectory(temporal_types) add_subdirectory(write_procedures) add_subdirectory(module_file_manager) +add_subdirectory(websocket) copy_e2e_python_files(pytest_runner pytest_runner.sh "") diff --git a/tests/e2e/memgraph.py b/tests/e2e/memgraph.py index 73597ff0f..dffb019a8 100755 --- a/tests/e2e/memgraph.py +++ b/tests/e2e/memgraph.py @@ -38,27 +38,32 @@ def wait_for_server(port, delay=0.01): def extract_bolt_port(args): for arg_index, arg in enumerate(args): - if arg.startswith('--bolt-port='): - maybe_port = arg.split('=')[1] + if arg.startswith("--bolt-port="): + maybe_port = arg.split("=")[1] if not maybe_port.isdigit(): - raise Exception('Unable to read Bolt port after --bolt-port=.') + raise Exception("Unable to read Bolt port after --bolt-port=.") return int(maybe_port) - elif arg == '--bolt-port': + elif arg == "--bolt-port": maybe_port = args[arg_index + 1] if not maybe_port.isdigit(): - raise Exception('Unable to read Bolt port after --bolt-port.') + raise Exception("Unable to read Bolt port after --bolt-port.") return int(maybe_port) return 7687 -class MemgraphInstanceRunner(): - def __init__(self, binary_path=MEMGRAPH_BINARY, args=[]): - self.host = '127.0.0.1' - self.bolt_port = extract_bolt_port(args) +def replace_paths(path): + return path.replace("$PROJECT_DIR", PROJECT_DIR).replace("$SCRIPT_DIR", SCRIPT_DIR).replace("$BUILD_DIR", BUILD_DIR) + + +class MemgraphInstanceRunner: + def __init__(self, binary_path=MEMGRAPH_BINARY, use_ssl=False): + self.host = "127.0.0.1" + self.bolt_port = None self.binary_path = binary_path - self.args = args + self.args = None self.proc_mg = None self.conn = None + self.ssl = use_ssl def query(self, query): cursor = self.conn.cursor() @@ -70,18 +75,21 @@ class MemgraphInstanceRunner(): return self.stop() self.args = copy.deepcopy(args) + self.args = [replace_paths(arg) for arg in self.args] self.data_directory = tempfile.TemporaryDirectory() - args_mg = [self.binary_path, - "--data-directory", self.data_directory.name, - "--storage-wal-enabled", - "--storage-snapshot-interval-sec", "300", - "--storage-properties-on-edges"] + self.args + args_mg = [ + self.binary_path, + "--data-directory", + self.data_directory.name, + "--storage-wal-enabled", + "--storage-snapshot-interval-sec", + "300", + "--storage-properties-on-edges", + ] + self.args self.bolt_port = extract_bolt_port(args_mg) self.proc_mg = subprocess.Popen(args_mg) wait_for_server(self.bolt_port) - self.conn = mgclient.connect( - host=self.host, - port=self.bolt_port) + self.conn = mgclient.connect(host=self.host, port=self.bolt_port, sslmode=self.ssl) self.conn.autocommit = True assert self.is_running(), "The Memgraph process died!" diff --git a/tests/e2e/runner.py b/tests/e2e/runner.py index 992141478..f283a578f 100755 --- a/tests/e2e/runner.py +++ b/tests/e2e/runner.py @@ -9,12 +9,13 @@ # by the Apache License, Version 2.0, included in the file # licenses/APL.txt. -from argparse import ArgumentParser import atexit import logging import os -from pathlib import Path import subprocess +from argparse import ArgumentParser +from pathlib import Path + import yaml from memgraph import MemgraphInstanceRunner @@ -36,18 +37,17 @@ def load_args(): def load_workloads(root_directory): workloads = [] - for file in Path(root_directory).rglob('*.yaml'): + for file in Path(root_directory).rglob("*.yaml"): with open(file, "r") as f: - workloads.extend(yaml.load(f, Loader=yaml.FullLoader)['workloads']) + workloads.extend(yaml.load(f, Loader=yaml.FullLoader)["workloads"]) return workloads def run(args): workloads = load_workloads(args.workloads_root_directory) for workload in workloads: - workload_name = workload['name'] - if args.workload_name is not None and \ - args.workload_name != workload_name: + workload_name = workload["name"] + if args.workload_name is not None and args.workload_name != workload_name: continue log.info("%s STARTED.", workload_name) # Setup. @@ -57,38 +57,36 @@ def run(args): def cleanup(): for mg_instance in mg_instances.values(): mg_instance.stop() - for name, config in workload['cluster'].items(): - mg_instance = MemgraphInstanceRunner(MEMGRAPH_BINARY) - mg_instances[name] = mg_instance - log_file_path = os.path.join(BUILD_DIR, 'logs', config['log_file']) - binary_args = config['args'] + ["--log-file", log_file_path] - if 'proc' in workload: - procdir = "--query-modules-directory=" + \ - os.path.join(BUILD_DIR, workload['proc']) - binary_args.append(procdir) + for name, config in workload["cluster"].items(): + use_ssl = False + if "ssl" in config: + use_ssl = bool(config["ssl"]) + config.pop("ssl") + mg_instance = MemgraphInstanceRunner(MEMGRAPH_BINARY, use_ssl) + mg_instances[name] = mg_instance + log_file_path = os.path.join(BUILD_DIR, "logs", config["log_file"]) + binary_args = config["args"] + ["--log-file", log_file_path] + if "proc" in workload: + procdir = "--query-modules-directory=" + os.path.join(BUILD_DIR, workload["proc"]) + binary_args.append(procdir) mg_instance.start(args=binary_args) - for query in config['setup_queries']: + for query in config.get("setup_queries", []): mg_instance.query(query) # Test. - mg_test_binary = os.path.join(BUILD_DIR, workload['binary']) - subprocess.run( - [mg_test_binary] + workload['args'], - check=True, - stderr=subprocess.STDOUT) + mg_test_binary = os.path.join(BUILD_DIR, workload["binary"]) + subprocess.run([mg_test_binary] + workload["args"], check=True, stderr=subprocess.STDOUT) # Validation. - for name, config in workload['cluster'].items(): - for validation in config['validation_queries']: + for name, config in workload["cluster"].items(): + for validation in config.get("validation_queries", []): mg_instance = mg_instances[name] - data = mg_instance.query(validation['query'])[0][0] - assert data == validation['expected'] + data = mg_instance.query(validation["query"])[0][0] + assert data == validation["expected"] cleanup() log.info("%s PASSED.", workload_name) -if __name__ == '__main__': +if __name__ == "__main__": args = load_args() - logging.basicConfig( - level=logging.INFO, - format='%(levelname)s %(asctime)s %(name)s] %(message)s') + logging.basicConfig(level=logging.INFO, format="%(levelname)s %(asctime)s %(name)s] %(message)s") run(args) diff --git a/tests/e2e/websocket/CMakeLists.txt b/tests/e2e/websocket/CMakeLists.txt new file mode 100644 index 000000000..15c3a0f3a --- /dev/null +++ b/tests/e2e/websocket/CMakeLists.txt @@ -0,0 +1,10 @@ +find_package(gflags REQUIRED) +find_package(Boost REQUIRED) + +add_executable(memgraph__e2e__websocket websocket.cpp) +target_link_libraries(memgraph__e2e__websocket mgclient mg-utils json gflags Boost::headers) + +add_executable(memgraph__e2e__websocket_ssl websocket_ssl.cpp) +target_link_libraries(memgraph__e2e__websocket_ssl mgclient mg-utils json gflags Boost::headers) +file(COPY ${CMAKE_CURRENT_SOURCE_DIR}/memgraph-selfsigned.crt DESTINATION ${CMAKE_CURRENT_BINARY_DIR}) +file(COPY ${CMAKE_CURRENT_SOURCE_DIR}/memgraph-selfsigned.key DESTINATION ${CMAKE_CURRENT_BINARY_DIR}) diff --git a/tests/e2e/websocket/common.hpp b/tests/e2e/websocket/common.hpp new file mode 100644 index 000000000..c6a448ade --- /dev/null +++ b/tests/e2e/websocket/common.hpp @@ -0,0 +1,309 @@ +// Copyright 2022 Memgraph Ltd. +// +// Use of this software is governed by the Business Source License +// included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source +// License, and you may not use this file except in compliance with the Business Source License. +// +// As of the Change Date specified in that file, in accordance with +// the Business Source License, use of this software will be governed +// by the Apache License, Version 2.0, included in the file +// licenses/APL.txt. + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "utils/logging.hpp" + +namespace beast = boost::beast; +namespace http = beast::http; +namespace websocket = beast::websocket; +namespace net = boost::asio; +using tcp = boost::asio::ip::tcp; +namespace ssl = boost::asio::ssl; + +constexpr std::array kSupportedLogLevels{"debug", "trace", "info", "warning", "error", "critical"}; + +struct Credentials { + std::string_view username; + std::string_view passsword; +}; + +inline void Fail(beast::error_code ec, char const *what) { std::cerr << what << ": " << ec.message() << "\n"; } + +inline std::string GetAuthenticationJSON(const Credentials &creds) { + nlohmann::json json_creds; + json_creds["username"] = creds.username; + json_creds["password"] = creds.passsword; + return json_creds.dump(); +} + +template +class Session : public std::enable_shared_from_this> { + using std::enable_shared_from_this>::shared_from_this; + + public: + explicit Session(net::io_context &ioc, ssl::context &ctx, std::vector &expected_messages) requires(ssl) + : resolver_(net::make_strand(ioc)), ws_(net::make_strand(ioc), ctx), received_messages_{expected_messages} {} + + explicit Session(net::io_context &ioc, std::vector &expected_messages) requires(!ssl) + : resolver_(net::make_strand(ioc)), ws_(net::make_strand(ioc)), received_messages_{expected_messages} {} + + template + explicit Session(Credentials creds, Args &&...args) : Session(std::forward(args)...) { + creds_.emplace(creds); + } + + void Run(std::string_view host, std::string_view port) { + host_ = host; + resolver_.async_resolve(host, port, beast::bind_front_handler(&Session::OnResolve, shared_from_this())); + } + + void OnResolve(beast::error_code ec, tcp::resolver::results_type results) { + if (ec) { + return Fail(ec, "resolve"); + } + + beast::get_lowest_layer(ws_).expires_after(std::chrono::seconds(30)); + beast::get_lowest_layer(ws_).async_connect(results, + beast::bind_front_handler(&Session::OnConnect, shared_from_this())); + } + + void OnConnect(beast::error_code ec, tcp::resolver::results_type::endpoint_type ep) { + if (ec) { + return Fail(ec, "connect"); + } + + host_ = fmt::format("{}:{}", host_, ep.port()); + + beast::get_lowest_layer(ws_).expires_after(std::chrono::seconds(30)); + + if constexpr (ssl) { + if (!SSL_set_tlsext_host_name(ws_.next_layer().native_handle(), host_.c_str())) { + ec = beast::error_code(static_cast(::ERR_get_error()), net::error::get_ssl_category()); + return Fail(ec, "connect"); + } + ws_.next_layer().async_handshake(ssl::stream_base::client, + beast::bind_front_handler(&Session::OnSSLHandshake, shared_from_this())); + } else { + ws_.set_option(websocket::stream_base::timeout::suggested(beast::role_type::client)); + + ws_.set_option(websocket::stream_base::decorator([](websocket::request_type &req) { + req.set(http::field::user_agent, std::string(BOOST_BEAST_VERSION_STRING) + " websocket-client-async"); + })); + + host_ = fmt::format("{}:{}", host_, ep.port()); + + ws_.async_handshake(host_, "/", beast::bind_front_handler(&Session::OnHandshake, shared_from_this())); + } + } + + void OnHandshake(beast::error_code ec) { + if (ec) { + return Fail(ec, "handshake"); + } + if (creds_) { + ws_.async_write(net::buffer(GetAuthenticationJSON(*creds_)), + beast::bind_front_handler(&Session::OnWrite, shared_from_this())); + } else { + ws_.async_read(buffer_, beast::bind_front_handler(&Session::OnRead, shared_from_this())); + } + } + + void OnSSLHandshake(beast::error_code ec) { + if (ec) { + return Fail(ec, "ssl_handshake"); + } + + beast::get_lowest_layer(ws_).expires_never(); + ws_.set_option(websocket::stream_base::timeout::suggested(beast::role_type::client)); + ws_.set_option(websocket::stream_base::decorator([](websocket::request_type &req) { + req.set(http::field::user_agent, std::string(BOOST_BEAST_VERSION_STRING) + " websocket-client-async-ssl"); + })); + + ws_.async_handshake(host_, "/", beast::bind_front_handler(&Session::OnHandshake, shared_from_this())); + } + + void OnWrite(beast::error_code ec, std::size_t bytes_transferred) { + boost::ignore_unused(bytes_transferred); + + if (ec) { + return Fail(ec, "write"); + } + + ws_.async_read(buffer_, beast::bind_front_handler(&Session::OnRead, shared_from_this())); + } + + void OnRead(beast::error_code ec, std::size_t bytes_transferred) { + boost::ignore_unused(bytes_transferred); + + if (ec) { + return Fail(ec, "read"); + } + + received_messages_.push_back(boost::beast::buffers_to_string(buffer_.data())); + buffer_.clear(); + + ws_.async_read(buffer_, beast::bind_front_handler(&Session::OnRead, shared_from_this())); + } + + void OnClose(beast::error_code ec) { + if (ec) { + return Fail(ec, "close"); + } + } + + private: + using InternalStream = std::conditional_t, beast::tcp_stream>; + tcp::resolver resolver_; + websocket::stream ws_; + beast::flat_buffer buffer_; + std::string host_; + std::vector &received_messages_; + std::optional creds_{std::nullopt}; +}; + +std::unique_ptr GetBoltClient(const uint16_t bolt_port, const bool use_ssl) { + auto client = mg::Client::Connect({.host = "127.0.0.1", .port = bolt_port, .use_ssl = use_ssl}); + MG_ASSERT(client, "Failed to connect!"); + + return client; +} + +inline void CleanDatabase(std::unique_ptr &client) { + MG_ASSERT(client->Execute("MATCH (n) DETACH DELETE n;")); + client->DiscardAll(); +} + +inline void AddUser(std::unique_ptr &client) { + MG_ASSERT(client->Execute("CREATE USER test IDENTIFIED BY 'testing';")); + client->DiscardAll(); +} + +inline void AddVertex(std::unique_ptr &client) { + MG_ASSERT(client->Execute("CREATE ();")); + client->DiscardAll(); +} +inline void AddConnectedVertices(std::unique_ptr &client) { + MG_ASSERT(client->Execute("CREATE ()-[:TO]->();")); + client->DiscardAll(); +} + +inline void RunQueries(std::unique_ptr &mg_client) { + CleanDatabase(mg_client); + AddVertex(mg_client); + AddVertex(mg_client); + AddVertex(mg_client); + AddConnectedVertices(mg_client); + CleanDatabase(mg_client); +} + +inline void AssertAuthMessage(auto &json_message, const bool success = true) { + MG_ASSERT(json_message.at("message").is_string(), "Event is not a string!"); + MG_ASSERT(json_message.at("success").is_boolean(), "Success is not a boolean!"); + MG_ASSERT(json_message.at("success").template get() == success, "Success does not match expected!"); +} + +inline void AssertLogMessage(const std::string &log_message) { + const auto json_message = nlohmann::json::parse(log_message); + if (json_message.contains("success")) { + spdlog::info("Received auth message: {}", json_message.dump()); + AssertAuthMessage(json_message); + return; + } + MG_ASSERT(json_message.at("event").is_string(), "Event is not a string!"); + MG_ASSERT(json_message.at("event").get() == "log", "Event is not equal to `log`!"); + MG_ASSERT(json_message.at("level").is_string(), "Level is not a string!"); + MG_ASSERT(std::ranges::count(kSupportedLogLevels, json_message.at("level")) == 1); + MG_ASSERT(json_message.at("message").is_string(), "Message is not a string!"); +} + +template +void TestWebsocketWithoutAnyUsers(std::unique_ptr &mg_client) { + spdlog::info("Starting websocket connection without any users."); + auto websocket_client = TWebsocketClient(); + websocket_client.Connect("127.0.0.1", "7444"); + + RunQueries(mg_client); + std::this_thread::sleep_for(std::chrono::seconds(1)); + + websocket_client.Close(); + websocket_client.AwaitClose(); + const auto received_messages = websocket_client.GetReceivedMessages(); + spdlog::info("Received {} messages.", received_messages.size()); + MG_ASSERT(!received_messages.empty(), "There are no received messages!"); + std::ranges::for_each(received_messages, AssertLogMessage); + + spdlog::info("Finishing websocket connection without any users."); +} + +template +void TestWebsocketWithAuthentication(std::unique_ptr &mg_client) { + spdlog::info("Starting websocket connection with users."); + AddUser(mg_client); + std::this_thread::sleep_for(std::chrono::seconds(1)); + auto websocket_client = TWebsocketClient({"test", "testing"}); + websocket_client.Connect("127.0.0.1", "7444"); + + RunQueries(mg_client); + std::this_thread::sleep_for(std::chrono::seconds(1)); + + websocket_client.Close(); + websocket_client.AwaitClose(); + const auto received_messages = websocket_client.GetReceivedMessages(); + spdlog::info("Received {} messages.", received_messages.size()); + + MG_ASSERT(!received_messages.empty(), "There are no received messages!"); + std::ranges::for_each(received_messages, AssertLogMessage); + + spdlog::info("Finishing websocket connection with users."); +} + +template +void TestWebsocketWithoutBeingAuthorized(std::unique_ptr &mg_client) { + spdlog::info("Starting websocket connection with users but without being authenticated."); + std::this_thread::sleep_for(std::chrono::seconds(1)); + auto websocket_client = TWebsocketClient({"wrong", "credentials"}); + websocket_client.Connect("127.0.0.1", "7444"); + + RunQueries(mg_client); + std::this_thread::sleep_for(std::chrono::seconds(1)); + + websocket_client.Close(); + websocket_client.AwaitClose(); + const auto received_messages = websocket_client.GetReceivedMessages(); + spdlog::info("Received {} messages.", received_messages.size()); + + MG_ASSERT(received_messages.size() == 1, "There must be only one message received!"); + if (!received_messages.empty()) { + auto json_message = nlohmann::json::parse(received_messages[0]); + AssertAuthMessage(json_message, false); + } + spdlog::info("Finishing websocket connection with users but without being authenticated."); +} + +template +void RunTestCases(std::unique_ptr &mg_client) { + TestWebsocketWithoutAnyUsers(mg_client); + TestWebsocketWithAuthentication(mg_client); + TestWebsocketWithoutBeingAuthorized(mg_client); +} diff --git a/tests/e2e/websocket/memgraph-selfsigned.crt b/tests/e2e/websocket/memgraph-selfsigned.crt new file mode 100644 index 000000000..14a3bac4c --- /dev/null +++ b/tests/e2e/websocket/memgraph-selfsigned.crt @@ -0,0 +1,23 @@ +-----BEGIN CERTIFICATE----- +MIIDwTCCAqmgAwIBAgIUYJ/LCgAVvJ3t/8PqdnhJFx/dD5UwDQYJKoZIhvcNAQEL +BQAwcDELMAkGA1UEBhMCR0IxDzANBgNVBAgMBkxvbmRvbjEPMA0GA1UEBwwGTG9u +ZG9uMRYwFAYDVQQKDA1NZW1ncmFwaCBMdGQuMRQwEgYDVQQLDAtNZW1ncmFwaCBE +QjERMA8GA1UEAwwITWVtZ3JhcGgwHhcNMjIwMjEwMTk1NjEzWhcNMzIwMjA4MTk1 +NjEzWjBwMQswCQYDVQQGEwJHQjEPMA0GA1UECAwGTG9uZG9uMQ8wDQYDVQQHDAZM +b25kb24xFjAUBgNVBAoMDU1lbWdyYXBoIEx0ZC4xFDASBgNVBAsMC01lbWdyYXBo +IERCMREwDwYDVQQDDAhNZW1ncmFwaDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC +AQoCggEBAL0f+jPohig22qiySSTYoTbaX8f2kPnf+/lW09nyrKZbb+NADRjcbzqa +kIOm1p5XdPNRCDRkTRZFscdibQFt4QjaD/81zSdCqZ32p02hz7NC6Kdh6dI35bVV +DavJ2F3yOfcCU38sr3lPP7BSh/frFRMWb87QjYHZrpm8d8lvW5V0Fyh8tk/YD5sI +lgrlzBKQJCbDv0IDgHkrvLRWtEQLDqWXyQVvTGoOifu+KqbwuFXAKYIMzAcMAmb2 +gQuc+nDMqk/kpAMh3xRt+swDEBntMSe0HTnbbbajWz3EMs4Wu6qp9x8lxi6O+q/K +5Mm9rbB8oSvdnPpOfDLnlt5uxb8aHtMCAwEAAaNTMFEwHQYDVR0OBBYEFE5nAFXR +1jQpal+s8tL5A6uCW8GeMB8GA1UdIwQYMBaAFE5nAFXR1jQpal+s8tL5A6uCW8Ge +MA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBALJFDLcKysFZt9Sr +SnsItVrl/xVqxZUDSqoQqIIyRxBjKjpTilUSBK/q/dfSM049GBK81mW7wrqtgv++ +yfiA6jKEaUa6HDN/5cOuSbjTUvdXIfHld3zL51UdGu9mpw1RIGQcjQcWS22Hbrus +GpxLuj7izP3h9JH9sTNC0XaO572T9cjEw7KMLw3vgQ5GSrevXjJx7+cxPoor8K8b +zfEu+Q38PsaFl7WYQChQI9hJr/Nzxnvppd59wEEB/NQiBxlJnM0FqDNErFrNTHfx +1w52tJQk5skKaa7kz70W9jm+VrKR5DeaHfk+aYWPRazDLzD4q7ggp8Z3CNFyCibC +vvT4VhA= +-----END CERTIFICATE----- diff --git a/tests/e2e/websocket/memgraph-selfsigned.key b/tests/e2e/websocket/memgraph-selfsigned.key new file mode 100644 index 000000000..06506fcdc --- /dev/null +++ b/tests/e2e/websocket/memgraph-selfsigned.key @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC9H/oz6IYoNtqo +skkk2KE22l/H9pD53/v5VtPZ8qymW2/jQA0Y3G86mpCDptaeV3TzUQg0ZE0WRbHH +Ym0BbeEI2g//Nc0nQqmd9qdNoc+zQuinYenSN+W1VQ2rydhd8jn3AlN/LK95Tz+w +Uof36xUTFm/O0I2B2a6ZvHfJb1uVdBcofLZP2A+bCJYK5cwSkCQmw79CA4B5K7y0 +VrRECw6ll8kFb0xqDon7viqm8LhVwCmCDMwHDAJm9oELnPpwzKpP5KQDId8UbfrM +AxAZ7TEntB052222o1s9xDLOFruqqfcfJcYujvqvyuTJva2wfKEr3Zz6Tnwy55be +bsW/Gh7TAgMBAAECggEAKrxEFGCxl/q1NDN9NbdBzpxP0maT4fLMBE6rdm9QthKp +WMeLbhe5hpuQlj8n9gW7JpQj73rOtH6xKmXCTfVfSVnFDZ1Bq2Jz437t1SwAO/id +i90jMd5xqKT8ftoQa0Il+PvslCsrx5Sxxo0PrmTPkU2bnDciErp2qsScqMbiAgYi +q+eUuPHqc1bFPQYdOufJFrDhKoj/wBdyAN7qLXk0CSk9uozKseaQkv6vCwn1k2W0 +cw9k9nwcebD1rZPpySLFXjDwLn/nprfxViRZWBYetOxENap6ohL6ADWXp004bOGp +zAWJ2zq3I97I7U/KMza2nvy2fK1F5CoTrTEqRbPykQKBgQDxTeF2mBY70aXRCG68 +2Jkofg6//yDmN+//HHbUJP4tmV0XrIdu0pyACBUmL1g5zfdwDWQJyDk4Qv5lT+Kt ++0sN43w8KkBWigJih6vQoUw/HRsx5WDVUjfYmCFODsmcrETnA2PIglJ6ssFtq0Rk ++k783kaVWdB/WpgLQB4yYs4fGwKBgQDIpJS0IpzuMkulrBgdtmCj1Xr3xNgoCho4 +ShXce/6pGfesfkm9OAApoPkbQYXHglEM+ON23B1UtvxawGtlP1ZQ8zRz0cHg7z5s +aK7dwj6+Kbs2xUw+hqeQNsQRurSEnvhegmsOym0sU9o9xvA5vcBbjFXQ2wvv2uMS +1Lx9LxIiqQKBgQDFmILB/HRoc5qW+5LCrZNtZxxo+GBWZQPwIbzkp45EhMuIcU1N +4MUqynOXGznhy9mNNknvALhqa+Gp0KrM0XR6YQgAtJCOzOB7EqINa6fmHs2AJFcR +GAyHHrxXESDjXOXnTg1NyfoEY5ClX5PYJGi+BL5D2pIzV1oeDFkt5V7odQKBgQCA +AI9lxFc9lxSvovTXr8xDeSQ0AX6tPJSxqIH94ZT7qLdbck30y08/P5TskIaaW0b9 +8aKb551GuF6SPwPE2f1lM2MZKI55w+edHcPBcfS5OMJZFNGpahpoZKf9b5FOsmRd +VvKZwwaXKv+mPc5v5+BaB1OQJM6evJP1JYcCjg8R0QKBgF/fepOeeNwv5nKD567G +3M5LEg7tNXfhz9bDODIXRoU75lP3le4PynhudL9SESDUZ/sVHTeNoddf5NEQVwjr +5TrPyjPua2S3o4WJ8n74MuQhwhp51m1sIMHzsEiVNWOu2DlK7sxxhZBkdfXwbGQg +VMdksPEMVdO2AFp0jrh7ax0r +-----END PRIVATE KEY----- diff --git a/tests/e2e/websocket/websocket.cpp b/tests/e2e/websocket/websocket.cpp new file mode 100644 index 000000000..1dfe21ad5 --- /dev/null +++ b/tests/e2e/websocket/websocket.cpp @@ -0,0 +1,68 @@ +// Copyright 2022 Memgraph Ltd. +// +// Use of this software is governed by the Business Source License +// included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source +// License, and you may not use this file except in compliance with the Business Source License. +// +// As of the Change Date specified in that file, in accordance with +// the Business Source License, use of this software will be governed +// by the Apache License, Version 2.0, included in the file +// licenses/APL.txt. +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include "common.hpp" +#include "utils/logging.hpp" + +DEFINE_uint64(bolt_port, 7687, "Bolt port"); + +class WebsocketClient { + public: + WebsocketClient() : session_{std::make_shared>(ioc_, received_messages_)} {} + + explicit WebsocketClient(Credentials creds) + : session_{std::make_shared>(creds, ioc_, received_messages_)} {} + + void Connect(const std::string_view host, const std::string_view port) { + session_->Run(host, port); + bg_thread_ = std::jthread([this]() { ioc_.run(); }); + } + + void Close() { ioc_.stop(); } + + void AwaitClose() { + MG_ASSERT(bg_thread_.joinable()); + bg_thread_.join(); + } + + std::vector GetReceivedMessages() { return received_messages_; } + + private: + std::vector received_messages_; + net::io_context ioc_; + std::jthread bg_thread_; + std::shared_ptr> session_; +}; + +int main(int argc, char **argv) { + google::SetUsageMessage("Memgraph E2E websocket!"); + gflags::ParseCommandLineFlags(&argc, &argv, true); + MG_ASSERT(FLAGS_bolt_port != 0); + logging::RedirectToStderr(); + + mg::Client::Init(); + auto mg_client = GetBoltClient(static_cast(FLAGS_bolt_port), false); + + RunTestCases(mg_client); + + return 0; +} diff --git a/tests/e2e/websocket/websocket_ssl.cpp b/tests/e2e/websocket/websocket_ssl.cpp new file mode 100644 index 000000000..28c4826a7 --- /dev/null +++ b/tests/e2e/websocket/websocket_ssl.cpp @@ -0,0 +1,76 @@ +// Copyright 2022 Memgraph Ltd. +// +// Use of this software is governed by the Business Source License +// included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source +// License, and you may not use this file except in compliance with the Business Source License. +// +// As of the Change Date specified in that file, in accordance with +// the Business Source License, use of this software will be governed +// by the Apache License, Version 2.0, included in the file +// licenses/APL.txt. + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include "common.hpp" +#include "utils/logging.hpp" + +DEFINE_uint64(bolt_port, 7687, "Bolt port"); + +class WebsocketSSLClient { + public: + WebsocketSSLClient() : session_{std::make_shared>(ioc_, ctx_, received_messages_)} {} + + explicit WebsocketSSLClient(Credentials creds) { + session_ = std::make_shared>(creds, ioc_, ctx_, received_messages_); + } + + void Connect(const std::string_view host, const std::string_view port) { + session_->Run(host, port); + bg_thread_ = std::jthread([this]() { ioc_.run(); }); + } + + void Close() { ioc_.stop(); } + + void AwaitClose() { + MG_ASSERT(bg_thread_.joinable()); + bg_thread_.join(); + } + + std::vector GetReceivedMessages() { return received_messages_; } + + private: + std::vector received_messages_; + ssl::context ctx_{ssl::context::tlsv12_client}; + net::io_context ioc_; + std::jthread bg_thread_; + std::shared_ptr> session_; +}; + +int main(int argc, char **argv) { + google::SetUsageMessage("Memgraph E2E websocket SSL!"); + gflags::ParseCommandLineFlags(&argc, &argv, true); + MG_ASSERT(FLAGS_bolt_port != 0); + logging::RedirectToStderr(); + + auto mg_client = GetBoltClient(static_cast(FLAGS_bolt_port), true); + mg::Client::Init(); + + RunTestCases(mg_client); + + return 0; +} diff --git a/tests/e2e/websocket/workloads.yaml b/tests/e2e/websocket/workloads.yaml new file mode 100644 index 000000000..883b366be --- /dev/null +++ b/tests/e2e/websocket/workloads.yaml @@ -0,0 +1,34 @@ +cert_file: &cert_file "$PROJECT_DIR/tests/e2e/websocket/memgraph-selfsigned.crt" +key_file: &key_file "$PROJECT_DIR/tests/e2e/websocket/memgraph-selfsigned.key" +bolt_port: &bolt_port "7687" +template_cluster: &template_cluster + cluster: + websocket: + args: ["--bolt-port=7687", "--log-level=TRACE", "--"] + log_file: "websocket-e2e.log" +template_cluster_ssl: &template_cluster_ssl + cluster: + websocket: + args: + [ + "--bolt-port", + *bolt_port, + "--log-level=TRACE", + "--bolt-cert-file", + *cert_file, + "--bolt-key-file", + *key_file, + "--", + ] + log_file: "websocket-ssl-e2e.log" + ssl: true + +workloads: + - name: "Websocket" + binary: "tests/e2e/websocket/memgraph__e2e__websocket" + args: ["--bolt-port", *bolt_port] + <<: *template_cluster + - name: "Websocket SSL" + binary: "tests/e2e/websocket/memgraph__e2e__websocket_ssl" + args: ["--bolt-port", *bolt_port] + <<: *template_cluster_ssl