diff options
Diffstat (limited to 'src/zenhttp/httpclient_test.cpp')
| -rw-r--r-- | src/zenhttp/httpclient_test.cpp | 81 |
1 files changed, 81 insertions, 0 deletions
diff --git a/src/zenhttp/httpclient_test.cpp b/src/zenhttp/httpclient_test.cpp index deaeca2a8..b0e097a54 100644 --- a/src/zenhttp/httpclient_test.cpp +++ b/src/zenhttp/httpclient_test.cpp @@ -1,6 +1,7 @@ // Copyright Epic Games, Inc. All Rights Reserved. #include <zenhttp/httpclient.h> +#include <zenhttp/httpclientshare.h> #include <zenhttp/httpserver.h> #if ZEN_WITH_TESTS @@ -19,7 +20,10 @@ # include "servers/httpasio.h" # include <atomic> +# include <chrono> +# include <memory> # include <thread> +# include <vector> ZEN_THIRD_PARTY_INCLUDES_START # include <asio.hpp> @@ -1816,6 +1820,83 @@ TEST_CASE("httpclient.uri_decoding") } } +// HttpClientShare contention: drive one share from multiple threads, each +// with its own HttpClient. Catches lock-callback races, missed unlocks +// (deadlock surfaces as test timeout), and out-of-bounds Data indexing under +// ASAN/TSAN. +TEST_CASE("httpclient.share_concurrent_lock_callbacks") +{ + TestServerFixture Fixture; + HttpClientShare Share; + + constexpr int NumThreads = 4; + constexpr int RequestsPerThread = 25; + std::atomic<int> SuccessCount{0}; + std::vector<std::thread> Workers; + Workers.reserve(NumThreads); + + for (int T = 0; T < NumThreads; ++T) + { + Workers.emplace_back([&]() { + HttpClient Client = + Fixture.MakeClient(HttpClientSettings{.ConnectTimeout = std::chrono::milliseconds(500), .OptionalShare = &Share}); + for (int I = 0; I < RequestsPerThread; ++I) + { + HttpClient::Response Resp = Client.Get("/api/test/hello"); + if (Resp.IsSuccess()) + { + SuccessCount.fetch_add(1, std::memory_order_relaxed); + } + } + }); + } + + for (auto& W : Workers) + { + W.join(); + } + + CHECK_EQ(SuccessCount.load(), NumThreads * RequestsPerThread); +} + +// HttpClientShare reuse smoke: many short-lived HttpClient instances bound to +// one share all reach the server. Hub workload pattern. The stronger +// "connection-actually-reused" assertion would require a server-side accept +// counter or extending HttpClient::Response with CURLINFO_NUM_CONNECTS; +// neither are wanted in the public API surface. +TEST_CASE("httpclient.share_connection_reuse_observed") +{ + TestServerFixture Fixture; + HttpClientShare Share; + + constexpr int NumIters = 50; + for (int I = 0; I < NumIters; ++I) + { + HttpClient Client = + Fixture.MakeClient(HttpClientSettings{.ConnectTimeout = std::chrono::milliseconds(500), .OptionalShare = &Share}); + HttpClient::Response Resp = Client.Get("/api/test/hello"); + REQUIRE_MESSAGE(Resp.IsSuccess(), "iter ", I, " failed"); + } +} + +// HttpClientShare positive lifetime contract: client destroyed before share, +// no error fires. Negative case (share destroyed while client alive) is not +// exercised - libcurl's behavior there includes potential UAF; the dtor's +// ZEN_ERROR is the operator-facing diagnostic for that case. +TEST_CASE("httpclient.share_lifetime_correct_order") +{ + TestServerFixture Fixture; + + auto Share = std::make_unique<HttpClientShare>(); + { + HttpClient Client = + Fixture.MakeClient(HttpClientSettings{.ConnectTimeout = std::chrono::milliseconds(500), .OptionalShare = Share.get()}); + HttpClient::Response Resp = Client.Get("/api/test/hello"); + REQUIRE(Resp.IsSuccess()); + } // Client destroyed; per-client CURL handles cleaned up. + Share.reset(); // no error expected. +} + TEST_SUITE_END(); void |