aboutsummaryrefslogtreecommitdiff
path: root/src/zenhttp/httpclient_test.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'src/zenhttp/httpclient_test.cpp')
-rw-r--r--src/zenhttp/httpclient_test.cpp81
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