Adds a vanilla nghttp2 test case that submits a request to a client session.
This test case is useful as a complete example of how to set up a request with the C nghttp2 API.
This change also refactors some test setup for easy reuse in the future.
PiperOrigin-RevId: 376184831
diff --git a/http2/adapter/nghttp2_test.cc b/http2/adapter/nghttp2_test.cc
index c9ccd0f..f801125 100644
--- a/http2/adapter/nghttp2_test.cc
+++ b/http2/adapter/nghttp2_test.cc
@@ -1,5 +1,7 @@
#include "third_party/nghttp2/src/lib/includes/nghttp2/nghttp2.h"
+#include "absl/strings/str_cat.h"
#include "http2/adapter/mock_nghttp2_callbacks.h"
+#include "http2/adapter/nghttp2_util.h"
#include "http2/adapter/test_frame_sequence.h"
#include "http2/adapter/test_utils.h"
#include "common/platform/api/quiche_test.h"
@@ -34,17 +36,82 @@
return options;
}
+class TestDataSource {
+ public:
+ explicit TestDataSource(std::string data) : data_(std::move(data)) {}
+
+ absl::string_view ReadNext(size_t size) {
+ const size_t to_send = std::min(size, remaining_.size());
+ auto ret = remaining_.substr(0, to_send);
+ remaining_.remove_prefix(to_send);
+ return ret;
+ }
+
+ size_t SelectPayloadLength(size_t max_length) {
+ return std::min(max_length, remaining_.size());
+ }
+
+ bool empty() const { return remaining_.empty(); }
+
+ private:
+ const std::string data_;
+ absl::string_view remaining_ = data_;
+};
+
+class Nghttp2Test : public testing::Test {
+ public:
+ Nghttp2Test() : session_(MakeSessionPtr(nullptr)) {}
+
+ void SetUp() override { InitializeSession(); }
+
+ virtual Perspective GetPerspective() = 0;
+
+ void InitializeSession() {
+ auto nghttp2_callbacks = MockNghttp2Callbacks::GetCallbacks();
+ nghttp2_option* options = GetOptions();
+ nghttp2_session* ptr;
+ if (GetPerspective() == Perspective::kClient) {
+ nghttp2_session_client_new2(&ptr, nghttp2_callbacks.get(),
+ &mock_callbacks_, options);
+ } else {
+ nghttp2_session_server_new2(&ptr, nghttp2_callbacks.get(),
+ &mock_callbacks_, options);
+ }
+ nghttp2_option_del(options);
+
+ // Sets up the Send() callback to append to |serialized_|.
+ EXPECT_CALL(mock_callbacks_, Send(_, _, _))
+ .WillRepeatedly([this](const uint8_t* data, size_t length, int flags) {
+ absl::StrAppend(&serialized_, ToStringView(data, length));
+ return length;
+ });
+ // Sets up the SendData() callback to fetch and append data from a
+ // TestDataSource.
+ EXPECT_CALL(mock_callbacks_, SendData(_, _, _, _))
+ .WillRepeatedly([this](nghttp2_frame* frame, const uint8_t* framehd,
+ size_t length, nghttp2_data_source* source) {
+ QUICHE_LOG(INFO) << "Appending frame header and " << length
+ << " bytes of data";
+ auto* s = static_cast<TestDataSource*>(source->ptr);
+ absl::StrAppend(&serialized_, ToStringView(framehd, 9),
+ s->ReadNext(length));
+ return 0;
+ });
+ session_.reset(ptr);
+ }
+
+ testing::StrictMock<MockNghttp2Callbacks> mock_callbacks_;
+ nghttp2_session_unique_ptr session_;
+ std::string serialized_;
+};
+
+class Nghttp2ClientTest : public Nghttp2Test {
+ public:
+ Perspective GetPerspective() override { return Perspective::kClient; }
+};
+
// Verifies nghttp2 behavior when acting as a client.
-TEST(Nghttp2ClientTest, ClientReceivesUnexpectedHeaders) {
- testing::StrictMock<MockNghttp2Callbacks> mock_callbacks;
- auto nghttp2_callbacks = MockNghttp2Callbacks::GetCallbacks();
- nghttp2_option* options = GetOptions();
- nghttp2_session* ptr;
- nghttp2_session_client_new2(&ptr, nghttp2_callbacks.get(), &mock_callbacks,
- options);
-
- auto client_session = MakeSessionPtr(ptr);
-
+TEST_F(Nghttp2ClientTest, ClientReceivesUnexpectedHeaders) {
const std::string initial_frames = TestFrameSequence()
.ServerPreface()
.Ping(42)
@@ -52,17 +119,17 @@
.Serialize();
testing::InSequence seq;
- EXPECT_CALL(mock_callbacks, OnBeginFrame(HasFrameHeader(0, SETTINGS, 0)));
- EXPECT_CALL(mock_callbacks, OnFrameRecv(IsSettings(testing::IsEmpty())));
- EXPECT_CALL(mock_callbacks, OnBeginFrame(HasFrameHeader(0, PING, 0)));
- EXPECT_CALL(mock_callbacks, OnFrameRecv(IsPing(42)));
- EXPECT_CALL(mock_callbacks,
+ EXPECT_CALL(mock_callbacks_, OnBeginFrame(HasFrameHeader(0, SETTINGS, 0)));
+ EXPECT_CALL(mock_callbacks_, OnFrameRecv(IsSettings(testing::IsEmpty())));
+ EXPECT_CALL(mock_callbacks_, OnBeginFrame(HasFrameHeader(0, PING, 0)));
+ EXPECT_CALL(mock_callbacks_, OnFrameRecv(IsPing(42)));
+ EXPECT_CALL(mock_callbacks_,
OnBeginFrame(HasFrameHeader(0, WINDOW_UPDATE, 0)));
- EXPECT_CALL(mock_callbacks, OnFrameRecv(IsWindowUpdate(1000)));
+ EXPECT_CALL(mock_callbacks_, OnFrameRecv(IsWindowUpdate(1000)));
- nghttp2_session_mem_recv(client_session.get(),
- ToUint8Ptr(initial_frames.data()),
- initial_frames.size());
+ ssize_t result = nghttp2_session_mem_recv(
+ session_.get(), ToUint8Ptr(initial_frames.data()), initial_frames.size());
+ ASSERT_EQ(result, initial_frames.size());
const std::string unexpected_stream_frames =
TestFrameSequence()
@@ -76,15 +143,91 @@
.GoAway(5, Http2ErrorCode::ENHANCE_YOUR_CALM, "calm down!!")
.Serialize();
- EXPECT_CALL(mock_callbacks, OnBeginFrame(HasFrameHeader(1, HEADERS, _)));
- EXPECT_CALL(mock_callbacks, OnInvalidFrameRecv(IsHeaders(1, _, _), _));
+ EXPECT_CALL(mock_callbacks_, OnBeginFrame(HasFrameHeader(1, HEADERS, _)));
+ EXPECT_CALL(mock_callbacks_, OnInvalidFrameRecv(IsHeaders(1, _, _), _));
// No events from the DATA, RST_STREAM or GOAWAY.
- nghttp2_session_mem_recv(client_session.get(),
+ nghttp2_session_mem_recv(session_.get(),
ToUint8Ptr(unexpected_stream_frames.data()),
unexpected_stream_frames.size());
+}
- nghttp2_option_del(options);
+// Tests the request-sending behavior of nghttp2 when acting as a client.
+TEST_F(Nghttp2ClientTest, ClientSendsRequest) {
+ int result = nghttp2_session_send(session_.get());
+ ASSERT_EQ(result, 0);
+
+ EXPECT_THAT(serialized_, testing::StrEq(spdy::kHttp2ConnectionHeaderPrefix));
+ serialized_.clear();
+
+ const std::string initial_frames =
+ TestFrameSequence().ServerPreface().Serialize();
+ testing::InSequence s;
+
+ // Server preface (empty SETTINGS)
+ EXPECT_CALL(mock_callbacks_, OnBeginFrame(HasFrameHeader(0, SETTINGS, 0)));
+ EXPECT_CALL(mock_callbacks_, OnFrameRecv(IsSettings(testing::IsEmpty())));
+
+ ssize_t recv_result = nghttp2_session_mem_recv(
+ session_.get(), ToUint8Ptr(initial_frames.data()), initial_frames.size());
+ EXPECT_EQ(initial_frames.size(), recv_result);
+
+ // Client wants to send a SETTINGS ack.
+ EXPECT_CALL(mock_callbacks_, BeforeFrameSend(IsSettings(testing::IsEmpty())));
+ EXPECT_CALL(mock_callbacks_, OnFrameSend(IsSettings(testing::IsEmpty())));
+ EXPECT_TRUE(nghttp2_session_want_write(session_.get()));
+ result = nghttp2_session_send(session_.get());
+ EXPECT_THAT(serialized_, EqualsFrames({spdy::SpdyFrameType::SETTINGS}));
+ serialized_.clear();
+
+ EXPECT_FALSE(nghttp2_session_want_write(session_.get()));
+
+ // The following sets up the client request.
+ std::vector<std::pair<absl::string_view, absl::string_view>> headers = {
+ {":method", "POST"},
+ {":scheme", "http"},
+ {":authority", "example.com"},
+ {":path", "/this/is/request/one"}};
+ std::vector<nghttp2_nv> nvs;
+ for (const auto& h : headers) {
+ nvs.push_back({.name = ToUint8Ptr(h.first.data()),
+ .value = ToUint8Ptr(h.second.data()),
+ .namelen = h.first.size(),
+ .valuelen = h.second.size()});
+ }
+ const absl::string_view kBody = "This is an example request body.";
+ TestDataSource source{std::string(kBody)};
+ nghttp2_data_provider provider{
+ .source = {.ptr = &source},
+ .read_callback = [](nghttp2_session*, int32_t, uint8_t*, size_t length,
+ uint32_t* data_flags, nghttp2_data_source* source,
+ void*) {
+ auto* s = static_cast<TestDataSource*>(source->ptr);
+ const ssize_t ret = s->SelectPayloadLength(length);
+ if (ret < length) {
+ *data_flags |= NGHTTP2_DATA_FLAG_EOF;
+ }
+ *data_flags |= NGHTTP2_DATA_FLAG_NO_COPY;
+ return ret;
+ }};
+ // After submitting the request, the client will want to write.
+ int stream_id =
+ nghttp2_submit_request(session_.get(), nullptr /* pri_spec */, nvs.data(),
+ nvs.size(), &provider, nullptr /* stream_data */);
+ EXPECT_GT(stream_id, 0);
+ EXPECT_TRUE(nghttp2_session_want_write(session_.get()));
+
+ // We expect that the client will want to write HEADERS, then DATA.
+ EXPECT_CALL(mock_callbacks_, BeforeFrameSend(IsHeaders(stream_id, _, _)));
+ EXPECT_CALL(mock_callbacks_, OnFrameSend(IsHeaders(stream_id, _, _)));
+ EXPECT_CALL(mock_callbacks_, OnFrameSend(IsData(stream_id, kBody.size(), _)));
+ nghttp2_session_send(session_.get());
+ EXPECT_THAT(serialized_, EqualsFrames({spdy::SpdyFrameType::HEADERS,
+ spdy::SpdyFrameType::DATA}));
+ EXPECT_THAT(serialized_, testing::HasSubstr(kBody));
+
+ // Once the request is flushed, the client no longer wants to write.
+ EXPECT_FALSE(nghttp2_session_want_write(session_.get()));
}
} // namespace