diff --git a/include/CMakeLists.txt b/include/CMakeLists.txt index 99f8ae9..7cc1ee6 100644 --- a/include/CMakeLists.txt +++ b/include/CMakeLists.txt @@ -45,6 +45,8 @@ else() opengemini/impl/ErrorCode.cpp opengemini/impl/comm/Context.cpp opengemini/impl/http/IHttpClient.cpp + opengemini/impl/http/HttpClient.cpp + opengemini/impl/http/HttpsClient.cpp ) opengemini_target_setting(Client PUBLIC) endif() diff --git a/include/opengemini/impl/http/ConnectionPool.hpp b/include/opengemini/impl/http/ConnectionPool.hpp new file mode 100644 index 0000000..6383a26 --- /dev/null +++ b/include/opengemini/impl/http/ConnectionPool.hpp @@ -0,0 +1,79 @@ +// +// Copyright 2024 Huawei Cloud Computing Technologies Co., Ltd. +// +// Licensed 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. +// + +#ifndef OPENGEMINI_IMPL_HTTP_CONNECTIONPOOL_HPP +#define OPENGEMINI_IMPL_HTTP_CONNECTIONPOOL_HPP + +#include +#include +#include +#include + +#include +#include + +#include "opengemini/Endpoint.hpp" +#include "opengemini/impl/comm/TaskSlot.hpp" + +namespace opengemini::impl::http { + +template +struct Connection { + using Stream = STREAM; + + Stream stream; + bool used; + + bool ShouldRetry(boost::beast::error_code& error, + std::string_view what) const; + +#if __cplusplus < 202002L + Connection(Stream _stream, bool _used); +#endif // (__cplusplus < 202002L) +}; + +template +class ConnectionPool : public TaskSlot { +public: + using ConnectionPtr = std::unique_ptr>; + using Stream = STREAM; + +public: + ConnectionPool(boost::asio::io_context& ctx, + std::chrono::milliseconds connectTimeout, + std::size_t maxKeepalive = 3); + + ConnectionPtr Retrieve(const Endpoint& endpoint, + boost::asio::yield_context yield); + + void Push(const Endpoint& endpoint, ConnectionPtr connection); + +protected: + const std::chrono::milliseconds connectTimeout_; + +private: + std::unordered_map, Endpoint::Hasher> + pool_; + std::mutex mutex_; + + const std::size_t maxKeepalive_; +}; + +} // namespace opengemini::impl::http + +#include "opengemini/impl/http/ConnectionPool.tpp" + +#endif // !OPENGEMINI_IMPL_HTTP_CONNECTIONPOOL_HPP diff --git a/include/opengemini/impl/http/ConnectionPool.tpp b/include/opengemini/impl/http/ConnectionPool.tpp new file mode 100644 index 0000000..ebd51fc --- /dev/null +++ b/include/opengemini/impl/http/ConnectionPool.tpp @@ -0,0 +1,79 @@ +// +// Copyright 2024 Huawei Cloud Computing Technologies Co., Ltd. +// +// Licensed 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. +// + +#include "opengemini/impl/http/ConnectionPool.hpp" + +#include "opengemini/Exception.hpp" + +namespace opengemini::impl::http { + +#if __cplusplus < 202002L +template +Connection::Connection(Stream _stream, bool _used) : + stream(std::move(_stream)), + used(_used) +{ } +#endif // (__cplusplus < 202002L) + +template +bool Connection::ShouldRetry(boost::beast::error_code& error, + std::string_view what) const +{ + if (!error) { return false; } + if (used) { return true; } + throw Exception(std::move(error), std::string(what)); +} + +template +ConnectionPool::ConnectionPool( + boost::asio::io_context& ctx, + std::chrono::milliseconds connectTimeout, + std::size_t maxKeepalive) : + TaskSlot(ctx), + connectTimeout_(connectTimeout), + maxKeepalive_(maxKeepalive) +{ } + +template +typename ConnectionPool::ConnectionPtr +ConnectionPool::Retrieve(const Endpoint& endpoint, + boost::asio::yield_context yield) +{ + { + std::lock_guard lock(mutex_); + if (auto& connections = pool_[endpoint]; !connections.empty()) { + auto connection = std::move(connections.front()); + connections.pop(); + return connection; + } + } + + return static_cast(this)->CreateConnection(endpoint, yield); +} + +template +void ConnectionPool::Push(const Endpoint& endpoint, + ConnectionPtr connection) +{ + if (!connection->used) { connection->used = true; } + + std::lock_guard lock(mutex_); + auto& connections = pool_[endpoint]; + if (connections.size() >= maxKeepalive_) { return; } + pool_[endpoint].push(std::move(connection)); +} + +} // namespace opengemini::impl::http diff --git a/include/opengemini/impl/http/HttpClient.cpp b/include/opengemini/impl/http/HttpClient.cpp new file mode 100644 index 0000000..77c27a0 --- /dev/null +++ b/include/opengemini/impl/http/HttpClient.cpp @@ -0,0 +1,106 @@ +// +// Copyright 2024 Huawei Cloud Computing Technologies Co., Ltd. +// +// Licensed 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. +// + +#include "opengemini/impl/http/HttpClient.hpp" + +#include "opengemini/Exception.hpp" +#include "opengemini/impl/util/Preprocessor.hpp" + +namespace opengemini::impl::http { + +OPENGEMINI_INLINE_SPECIFIER +HttpClient::HttpClient(boost::asio::io_context& ctx, + std::chrono::milliseconds connectTimeout, + std::chrono::milliseconds readWriteTimeout) : + + IHttpClient(ctx, connectTimeout, readWriteTimeout), + pool_(ctx, connectTimeout) +{ } + +OPENGEMINI_INLINE_SPECIFIER +Response HttpClient::SendRequest(const Endpoint& endpoint, + Request request, + boost::asio::yield_context yield) +{ + namespace beast = boost::beast; + namespace http = boost::beast::http; + + Response response; + beast::flat_buffer buffer; + beast::error_code error; + + for (;; error.clear()) { + auto connection = pool_.Retrieve(endpoint, yield); + auto& stream = connection->stream; + + stream.expires_after(readWriteTimeout_); + http::async_write(stream, request, yield[error]); + if (connection->ShouldRetry(error, "Write to stream failed.")) { + continue; + } + + buffer.clear(); + http::async_read(stream, buffer, response, yield[error]); + if (connection->ShouldRetry(error, "Read from stream failed.")) { + continue; + } + + if (response.keep_alive()) { + pool_.Push(endpoint, std::move(connection)); + break; + } + + std::ignore = stream.socket().shutdown( + boost::asio::ip::tcp::socket::shutdown_both, + error); + if (error && error != beast::errc::not_connected) { + throw Exception(error, "Shutdown stream failed."); + } + break; + } + + return response; +} + +OPENGEMINI_INLINE_SPECIFIER +HttpClient::Pool::Pool(boost::asio::io_context& ctx, + std::chrono::milliseconds connectTimeout) : + ConnectionPool(ctx, connectTimeout) +{ } + +OPENGEMINI_INLINE_SPECIFIER +HttpClient::Pool::ConnectionPtr +HttpClient::Pool::CreateConnection(const Endpoint& endpoint, + boost::asio::yield_context yield) +{ + boost::asio::ip::tcp::resolver resolver(ctx_); + boost::beast::error_code error; + auto results = resolver.async_resolve(endpoint.host, + std::to_string(endpoint.port), + yield[error]); + if (error) { throw Exception(error, "Resolve hostname failed."); } + + auto connection = + std::make_unique(Stream{ ctx_ }, false); + auto& stream = connection->stream; + stream.expires_after(connectTimeout_); + stream.async_connect(results, yield[error]); + if (error) { throw Exception(error, "Connect to server failed."); } + + return connection; +} + +} // namespace opengemini::impl::http diff --git a/include/opengemini/impl/http/HttpClient.hpp b/include/opengemini/impl/http/HttpClient.hpp new file mode 100644 index 0000000..bb22c36 --- /dev/null +++ b/include/opengemini/impl/http/HttpClient.hpp @@ -0,0 +1,60 @@ +// +// Copyright 2024 Huawei Cloud Computing Technologies Co., Ltd. +// +// Licensed 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. +// + +#ifndef OPENGEMINI_IMPL_HTTP_HTTPCLIENT_HPP +#define OPENGEMINI_IMPL_HTTP_HTTPCLIENT_HPP + +#include "opengemini/impl/http/ConnectionPool.hpp" +#include "opengemini/impl/http/IHttpClient.hpp" + +namespace opengemini::impl::http { + +class HttpClient : public IHttpClient { +public: + explicit HttpClient(boost::asio::io_context& ctx, + std::chrono::milliseconds connectTimeout, + std::chrono::milliseconds readWriteTimeout); + ~HttpClient() = default; + +private: + class Pool : public ConnectionPool { + friend class ConnectionPool; + + public: + Pool(boost::asio::io_context& ctx, + std::chrono::milliseconds connectTimeout); + + private: + ConnectionPtr CreateConnection(const Endpoint& endpoint, + boost::asio::yield_context yield); + }; + +private: + Response SendRequest(const Endpoint& endpoint, + Request request, + boost::asio::yield_context yield) override; + +private: + Pool pool_; +}; + +} // namespace opengemini::impl::http + +#ifndef OPENGEMINI_SEPARATE_COMPILATION +# include "opengemini/impl/http/HttpClient.cpp" +#endif // !OPENGEMINI_SEPARATE_COMPILATION + +#endif // !OPENGEMINI_IMPL_HTTP_HTTPCLIENT_HPP diff --git a/include/opengemini/impl/http/HttpsClient.cpp b/include/opengemini/impl/http/HttpsClient.cpp new file mode 100644 index 0000000..044c87a --- /dev/null +++ b/include/opengemini/impl/http/HttpsClient.cpp @@ -0,0 +1,154 @@ +// +// Copyright 2024 Huawei Cloud Computing Technologies Co., Ltd. +// +// Licensed 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. +// + +#ifdef OPENGEMINI_ENABLE_SSL_SUPPORT + +// clang-format off +#include "opengemini/impl/http/HttpsClient.hpp" + +#include "opengemini/Exception.hpp" +#include "opengemini/impl/util/Preprocessor.hpp" +// clang-format on + +namespace opengemini::impl::http { + +OPENGEMINI_INLINE_SPECIFIER +HttpsClient::HttpsClient(boost::asio::io_context& ctx, + std::chrono::milliseconds connectTimeout, + std::chrono::milliseconds readWriteTimeout, + const TLSConfig& tlsConfig) : + IHttpClient(ctx, connectTimeout, readWriteTimeout), + sslCtx_(static_cast(tlsConfig.version)), + pool_(ctx, connectTimeout_, sslCtx_) +{ + try { + if (tlsConfig.rootCAs.empty()) { sslCtx_.set_default_verify_paths(); } + else { + sslCtx_.add_certificate_authority( + boost::asio::buffer(tlsConfig.rootCAs.data(), + tlsConfig.rootCAs.size())); + } + + if (!tlsConfig.certificates.empty()) { + sslCtx_.use_certificate_chain( + boost::asio::buffer(tlsConfig.certificates.data(), + tlsConfig.certificates.size())); + } + + sslCtx_.set_verify_mode(tlsConfig.skipVerifyPeer + ? boost::asio::ssl::verify_none + : boost::asio::ssl::verify_peer); + } + catch (const boost::system::system_error& e) { + throw Exception(e.code(), "Initialize TLS configuration failed."); + } +} + +OPENGEMINI_INLINE_SPECIFIER +Response HttpsClient::SendRequest(const Endpoint& endpoint, + Request request, + boost::asio::yield_context yield) +{ + namespace asio = boost::asio; + namespace beast = boost::beast; + namespace http = boost::beast::http; + + Response response; + beast::flat_buffer buffer; + beast::error_code error; + + for (;; error.clear()) { + auto connection = pool_.Retrieve(endpoint, yield); + auto& tlsStream = connection->stream; + auto& tcpStream = beast::get_lowest_layer(tlsStream); + + tcpStream.expires_after(readWriteTimeout_); + http::async_write(tlsStream, request, yield[error]); + if (connection->ShouldRetry(error, "Write to stream failed.")) { + continue; + } + + buffer.clear(); + http::async_read(tlsStream, buffer, response, yield[error]); + if (connection->ShouldRetry(error, "Read from stream failed.")) { + continue; + } + + if (response.keep_alive()) { + pool_.Push(endpoint, std::move(connection)); + break; + } + + tlsStream.async_shutdown(yield[error]); + if (error && error != asio::error::eof && + error != asio::ssl::error::stream_truncated) { + throw Exception(error, "Shutdown stream failed."); + } + break; + } + + return response; +} + +OPENGEMINI_INLINE_SPECIFIER +HttpsClient::Pool::Pool(boost::asio::io_context& ctx, + std::chrono::milliseconds connectTimeout, + boost::asio::ssl::context& sslCtx) : + ConnectionPool(ctx, connectTimeout), + sslCtx_(sslCtx) +{ } + +OPENGEMINI_INLINE_SPECIFIER +HttpsClient::Pool::ConnectionPtr +HttpsClient::Pool::CreateConnection(const Endpoint& endpoint, + boost::asio::yield_context yield) +{ + namespace asio = boost::asio; + namespace beast = boost::beast; + + auto& [host, port] = endpoint; + boost::beast::error_code error; + asio::ip::tcp::resolver resolver(ctx_); + + auto results = + resolver.async_resolve(host, std::to_string(port), yield[error]); + if (error) { throw Exception(error, "Resolve hostname failed."); } + + auto connection = + std::make_unique(Stream{ ctx_, sslCtx_ }, + false); + + auto& sslStream = connection->stream; + if (!SSL_set_tlsext_host_name(sslStream.native_handle(), host.c_str())) { + error.assign(static_cast(::ERR_get_error()), + asio::error::get_ssl_category()); + throw Exception(error, "Set TLS server name failed."); + } + + auto& tcpStream = beast::get_lowest_layer(sslStream); + tcpStream.expires_after(connectTimeout_); + tcpStream.async_connect(results, yield[error]); + if (error) { throw Exception(error, "Connect to server failed."); } + + sslStream.async_handshake(asio::ssl::stream_base::client, yield[error]); + if (error) { throw Exception(error, "Perform TLS handshake failed."); } + + return connection; +} + +} // namespace opengemini::impl::http + +#endif // OPENGEMINI_ENABLE_SSL_SUPPORT diff --git a/include/opengemini/impl/http/HttpsClient.hpp b/include/opengemini/impl/http/HttpsClient.hpp new file mode 100644 index 0000000..10b49ad --- /dev/null +++ b/include/opengemini/impl/http/HttpsClient.hpp @@ -0,0 +1,72 @@ +// +// Copyright 2024 Huawei Cloud Computing Technologies Co., Ltd. +// +// Licensed 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. +// + +#ifndef OPENGEMINI_IMPL_HTTP_HTTPSCLIENT_HPP +#define OPENGEMINI_IMPL_HTTP_HTTPSCLIENT_HPP + +#include + +#include "opengemini/ClientConfig.hpp" +#include "opengemini/impl/http/ConnectionPool.hpp" +#include "opengemini/impl/http/IHttpClient.hpp" + +namespace opengemini::impl::http { + +class HttpsClient : public IHttpClient { +public: + explicit HttpsClient(boost::asio::io_context& ctx, + std::chrono::milliseconds connectTimeout, + std::chrono::milliseconds readWriteTimeout, + const TLSConfig& tlsConfig); + ~HttpsClient() = default; + +private: + class Pool : + public ConnectionPool< + Pool, + boost::beast::ssl_stream> { + friend class ConnectionPool; + + public: + Pool(boost::asio::io_context& ctx, + std::chrono::milliseconds connectTimeout, + boost::asio::ssl::context& sslCtx); + + private: + ConnectionPtr CreateConnection(const Endpoint& endpoint, + boost::asio::yield_context yield); + + private: + boost::asio::ssl::context& sslCtx_; + }; + +private: + Response SendRequest(const Endpoint& endpoint, + Request request, + boost::asio::yield_context yield) override; + +private: + boost::asio::ssl::context sslCtx_; + Pool pool_; +}; + +} // namespace opengemini::impl::http + +#ifndef OPENGEMINI_SEPARATE_COMPILATION +# include "opengemini/impl/http/HttpsClient.cpp" +#endif // !OPENGEMINI_SEPARATE_COMPILATION + +#endif // !OPENGEMINI_IMPL_HTTP_HTTPSCLIENT_HPP diff --git a/include/opengemini/impl/http/IHttpClient.cpp b/include/opengemini/impl/http/IHttpClient.cpp index 8fc4b35..5848297 100644 --- a/include/opengemini/impl/http/IHttpClient.cpp +++ b/include/opengemini/impl/http/IHttpClient.cpp @@ -39,30 +39,28 @@ IHttpClient::IHttpClient(boost::asio::io_context& ctx, { } OPENGEMINI_INLINE_SPECIFIER -Response IHttpClient::Get(const Endpoint& endpoint, +Response IHttpClient::Get(Endpoint endpoint, std::string target, boost::asio::yield_context yield) { - return SendRequest(endpoint, - BuildRequest(endpoint.host, - std::move(target), - {}, - boost::beast::http::verb::get), - yield); + auto request = BuildRequest(endpoint.host, + std::move(target), + {}, + boost::beast::http::verb::get); + return SendRequest(std::move(endpoint), std::move(request), yield); } OPENGEMINI_INLINE_SPECIFIER -Response IHttpClient::Post(const Endpoint& endpoint, +Response IHttpClient::Post(Endpoint endpoint, std::string target, std::string body, boost::asio::yield_context yield) { - return SendRequest(endpoint, - BuildRequest(endpoint.host, - std::move(target), - std::move(body), - boost::beast::http::verb::post), - yield); + auto request = BuildRequest(endpoint.host, + std::move(target), + std::move(body), + boost::beast::http::verb::post); + return SendRequest(std::move(endpoint), std::move(request), yield); } OPENGEMINI_INLINE_SPECIFIER diff --git a/include/opengemini/impl/http/IHttpClient.hpp b/include/opengemini/impl/http/IHttpClient.hpp index c293536..b59e864 100644 --- a/include/opengemini/impl/http/IHttpClient.hpp +++ b/include/opengemini/impl/http/IHttpClient.hpp @@ -40,11 +40,11 @@ class IHttpClient : public TaskSlot { virtual ~IHttpClient() = default; - Response Get(const Endpoint& endpoint, + Response Get(Endpoint endpoint, std::string target, boost::asio::yield_context yield); - Response Post(const Endpoint& endpoint, + Response Post(Endpoint endpoint, std::string target, std::string body, boost::asio::yield_context yield); diff --git a/test/unit/CMakeLists.txt b/test/unit/CMakeLists.txt index 02524d7..8ed49ed 100644 --- a/test/unit/CMakeLists.txt +++ b/test/unit/CMakeLists.txt @@ -19,6 +19,7 @@ include(${PROJECT_SOURCE_DIR}/cmake/deps/googletest.cmake) add_executable(UnitTest Client_Test.cpp ClientConfigBuilder_Test.cpp + impl/http/IHttpClient_Test.cpp ) add_executable(${PROJECT_NAME}::UnitTest ALIAS UnitTest) diff --git a/test/unit/impl/http/IHttpClient_Test.cpp b/test/unit/impl/http/IHttpClient_Test.cpp new file mode 100644 index 0000000..54de913 --- /dev/null +++ b/test/unit/impl/http/IHttpClient_Test.cpp @@ -0,0 +1,233 @@ +// +// Copyright 2024 Huawei Cloud Computing Technologies Co., Ltd. +// +// Licensed 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. +// + +#include +#include + +#include +#include + +#include "opengemini/impl/comm/Context.hpp" +#include "opengemini/impl/http/HttpClient.hpp" +#ifdef OPENGEMINI_ENABLE_SSL_SUPPORT +# include "opengemini/impl/http/HttpsClient.hpp" +# include "test/SelfRootCA.hpp" +#endif // OPENGEMINI_ENABLE_SSL_SUPPORT +#include "opengemini/Exception.hpp" +#include "test/Random.hpp" +#include "test/TestFixtureWithContext.hpp" + +namespace opengemini::test { + +using namespace std::chrono_literals; +using namespace impl::http; + +class IHttpClientTestFixture : public TestFixtureWithContext { +protected: + IHttpClientTestFixture() + { + clients_.emplace_back(std::make_unique(ctx_(), 5s, 5s), + Endpoint{ "httpbin.org", 80 }); +#ifdef OPENGEMINI_ENABLE_SSL_SUPPORT + clients_.emplace_back( + std::make_unique(ctx_(), 5s, 5s, TLSConfig{}), + Endpoint{ "httpbin.org", 443 }); +#endif // OPENGEMINI_ENABLE_SSL_SUPPORT + } + + template + Response DoGet(const std::unique_ptr& client, Args&&... args) + { + return boost::asio::spawn( + ctx_(), + [&](auto yield) { + return client->Get(std::forward(args)..., yield); + }, + boost::asio::use_future) + .get(); + } + + template + Response DoPost(const std::unique_ptr& client, Args&&... args) + { + return boost::asio::spawn( + ctx_(), + [&](auto yield) { + return client->Post(std::forward(args)..., yield); + }, + boost::asio::use_future) + .get(); + } + +protected: + std::vector, Endpoint>> clients_; +}; + +TEST_F(IHttpClientTestFixture, GetRequest) +{ + for (auto& [client, endpoint] : clients_) { + auto rsp = DoGet(client, endpoint, "/get"); + EXPECT_EQ(rsp.result_int(), 200); + EXPECT_TRUE(!rsp.body().empty()); + } +} + +TEST_F(IHttpClientTestFixture, PostRequest) +{ + const auto body = test::GenerateRandomString(GenerateRandomNumber(1, 64)); + for (auto& [client, endpoint] : clients_) { + auto rsp = DoPost(client, endpoint, "/anything", body); + EXPECT_EQ(rsp.result_int(), 200); + EXPECT_THAT(rsp.body(), testing::HasSubstr(body)); + } +} + +TEST_F(IHttpClientTestFixture, CallFromMultiThreads) +{ + std::vector> futures; + for (auto idx = 0; idx < clients_.size(); ++idx) { + for (auto cnt = 0; cnt < 10; ++cnt) { + futures.push_back(std::async(std::launch::async, [this, idx] { + auto rsp = DoGet(clients_[idx].first, + clients_[idx].second, + "/range/32"); + EXPECT_EQ(rsp.result_int(), 200); + EXPECT_EQ(rsp.body(), "abcdefghijklmnopqrstuvwxyzabcdef"); + })); + } + } + + for (auto& future : futures) { future.get(); } +} + +TEST_F(IHttpClientTestFixture, InvalidHost) +{ + for (auto& [client, _] : clients_) { + EXPECT_THROW(DoGet(client, Endpoint{ "123.456.789.10", 123 }, "/get"), + Exception); + } +} + +TEST_F(IHttpClientTestFixture, InvalidPort) +{ + for (auto& [client, _] : clients_) { + EXPECT_THROW(std::ignore = + DoGet(client, Endpoint{ "httpbin.org", 12345 }, "/"), + Exception); + } +} + +TEST_F(IHttpClientTestFixture, WithoutSetDefaultHeaders) +{ + using boost::beast::http::field; + + for (auto& [client, endpoint] : clients_) { + auto rsp = DoGet(client, endpoint, "/headers"); + EXPECT_THAT(rsp.body(), testing::HasSubstr(R"("Host": "httpbin.org")")); + EXPECT_THAT(rsp.body(), + testing::ContainsRegex( + R"(opengemini-client-cxx/[0-9]+\.[0-9]+\.[0-9]+)")); + } +} + +TEST_F(IHttpClientTestFixture, SetDefaultHeaders) +{ + using boost::beast::http::field; + + for (auto& [client, endpoint] : clients_) { + client->DefaultHeaders() = { { "Content-Type", "text/plain" }, + { "Authorization", "dummy" } }; + + auto rsp = DoGet(client, endpoint, "/headers"); + EXPECT_THAT(rsp.body(), testing::HasSubstr(R"("Host": "httpbin.org")")); + EXPECT_THAT( + rsp.body(), + testing::ContainsRegex( + R"("User-Agent": "opengemini-client-cxx/[0-9]+\.[0-9]+\.[0-9]+")")); + EXPECT_THAT(rsp.body(), + testing::HasSubstr(R"("Content-Type": "text/plain")")); + EXPECT_THAT(rsp.body(), + testing::HasSubstr(R"("Authorization": "dummy")")); + } +} + +TEST_F(IHttpClientTestFixture, SetDefaultHeadersWhichWillBeCovered) +{ + using boost::beast::http::field; + + for (auto& [client, endpoint] : clients_) { + client->DefaultHeaders() = { { "User-Agent", "dummy" }, + { "Host", "dummy" } }; + + auto rsp = DoGet(client, endpoint, "/headers"); + EXPECT_THAT(rsp.body(), testing::HasSubstr(R"("Host": "httpbin.org")")); + EXPECT_THAT( + rsp.body(), + testing::ContainsRegex( + R"("User-Agent": "opengemini-client-cxx/[0-9]+\.[0-9]+\.[0-9]+")")); + } +} + +TEST_F(IHttpClientTestFixture, ReadWriteTimeout) +{ + for (auto& [client, endpoint] : clients_) { + EXPECT_THROW(std::ignore = DoGet(client, endpoint, "/delay/7"), + Exception); + } +} + +#ifdef OPENGEMINI_ENABLE_SSL_SUPPORT + +TEST_F(IHttpClientTestFixture, WithInvalidRootCA) +{ + EXPECT_THROW(HttpsClient cli(ctx_(), 5s, 5s, { false, {}, "dummy ca" }), + Exception); +} + +TEST_F(IHttpClientTestFixture, WithInvalidCertificate) +{ + EXPECT_THROW( + HttpsClient cli(ctx_(), 5s, 5s, { false, "dummy", selfRootCA }), + Exception); +} + +TEST_F(IHttpClientTestFixture, WithSelfSignedRootCA) +{ + std::unique_ptr client = + std::make_unique(ctx_(), + 5s, + 5s, + TLSConfig{ false, {}, selfRootCA }); + + EXPECT_THROW(std::ignore = DoGet(client, clients_[1].second, "/range/26"), + Exception); +} + +TEST_F(IHttpClientTestFixture, SkipVerifyPeer) +{ + std::unique_ptr client = + std::make_unique(ctx_(), + 5s, + 5s, + TLSConfig{ true, {}, selfRootCA }); + + auto rsp = DoGet(client, clients_[1].second, "/range/26"); + EXPECT_EQ(rsp.body(), "abcdefghijklmnopqrstuvwxyz"); +} + +#endif // OPENGEMINI_ENABLE_SSL_SUPPORT + +} // namespace opengemini::test diff --git a/test/util/test/Random.hpp b/test/util/test/Random.hpp new file mode 100644 index 0000000..b445a2b --- /dev/null +++ b/test/util/test/Random.hpp @@ -0,0 +1,51 @@ +// +// Copyright 2024 Huawei Cloud Computing Technologies Co., Ltd. +// +// Licensed 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. +// + +#ifndef TEST_UTIL_TEST_RANDOM_HPP +#define TEST_UTIL_TEST_RANDOM_HPP + +#include +#include + +namespace opengemini::test { + +inline int GenerateRandomNumber(int min, int max) +{ + std::random_device seed; + std::mt19937 engine(seed()); + std::uniform_int_distribution generator(min, max); + return generator(engine); +} + +inline std::string GenerateRandomString(std::size_t length) +{ + std::string dict{ + "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" + }; + std::random_device seed; + std::mt19937 engine(seed()); + std::uniform_int_distribution<> generator(0, dict.size() - 1); + + std::string result; + for (int cnt = 0; cnt < length; ++cnt) { + result.push_back(dict[generator(engine)]); + } + return result; +} + +} // namespace opengemini::test + +#endif // !TEST_UTIL_TEST_RANDOM_HPP diff --git a/test/util/test/SelfRootCA.hpp b/test/util/test/SelfRootCA.hpp new file mode 100644 index 0000000..71414e6 --- /dev/null +++ b/test/util/test/SelfRootCA.hpp @@ -0,0 +1,40 @@ +// +// Copyright 2024 Huawei Cloud Computing Technologies Co., Ltd. +// +// Licensed 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. +// + +#ifndef TEST_UTIL_TEST_SELFROOTCA_HPP +#define TEST_UTIL_TEST_SELFROOTCA_HPP + +namespace opengemini::test { + +constexpr auto selfRootCA = R"(-----BEGIN CERTIFICATE----- +MIICajCCAhGgAwIBAgIUE0IXwsCWFMxSvWYeOKFPyS0QX8MwCgYIKoZIzj0EAwIw +gaIxCzAJBgNVBAYTAkNOMRYwFAYDVQQIDA1UZXN0IFByb3ZpbmNlMRYwFAYDVQQH +DA1UZXN0IExvY2FsaXR5MRowGAYDVQQKDBFUZXN0IE9yZ2FuaXphdGlvbjESMBAG +A1UECwwJVGVzdCBVbml0MRQwEgYDVQQDDAtUZXN0IENvbW1vbjEdMBsGCSqGSIb3 +DQEJARYOdGVzdEB0ZXN0LnRlc3QwIBcNMjQwMzIwMTQyMTU1WhgPMjEyNDAyMjUx +NDIxNTVaMIGiMQswCQYDVQQGEwJDTjEWMBQGA1UECAwNVGVzdCBQcm92aW5jZTEW +MBQGA1UEBwwNVGVzdCBMb2NhbGl0eTEaMBgGA1UECgwRVGVzdCBPcmdhbml6YXRp +b24xEjAQBgNVBAsMCVRlc3QgVW5pdDEUMBIGA1UEAwwLVGVzdCBDb21tb24xHTAb +BgkqhkiG9w0BCQEWDnRlc3RAdGVzdC50ZXN0MFkwEwYHKoZIzj0CAQYIKoZIzj0D +AQcDQgAEa68DUSX3y451Xax+koPPPJzNCuNLsSr8OLGbGfXZfw4NsB679bfDDXU5 +MDN+NYGbb4NKWH2CNDPAz9kKu/qwcKMhMB8wHQYDVR0OBBYEFCemJ94A8tdF8S2m +oc0vHrg8jO47MAoGCCqGSM49BAMCA0cAMEQCIA55qesHsDB/MmVVQoxzz2/+3O4E +G2J4Xrpbjca5aWIbAiBB4QEL8MF8FdjMUSL5iMt3TfC9kMbOhjqk1CmpOkitPA== +-----END CERTIFICATE-----)"; + +} // namespace opengemini::test + +#endif // !TEST_UTIL_TEST_SELFROOTCA_HPP diff --git a/test/util/test/TestFixtureWithContext.hpp b/test/util/test/TestFixtureWithContext.hpp new file mode 100644 index 0000000..b408fc0 --- /dev/null +++ b/test/util/test/TestFixtureWithContext.hpp @@ -0,0 +1,38 @@ +// +// Copyright 2024 Huawei Cloud Computing Technologies Co., Ltd. +// +// Licensed 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. +// + +#ifndef TEST_UTIL_TEST_TESTFIXTUREWITHCONTEXT_HPP +#define TEST_UTIL_TEST_TESTFIXTUREWITHCONTEXT_HPP + +#include + +#include "opengemini/impl/comm/Context.hpp" + +namespace opengemini::test { + +class TestFixtureWithContext : public testing::Test { +protected: + TestFixtureWithContext(std::size_t concurrencyHint = 0) : + ctx_(concurrencyHint) + { } + +protected: + impl::Context ctx_; +}; + +} // namespace opengemini::test + +#endif // !TEST_UTIL_TEST_TESTFIXTUREWITHCONTEXT_HPP