Adds simple decoder event handling to OgHttp2Session.

The session mostly passes events through to the visitor, although this change does contain some minimal state tracking.

PiperOrigin-RevId: 368932801
Change-Id: I006a9c3d2e156b64f294866de6fc3e92fab9dfe2
diff --git a/http2/adapter/http2_util.cc b/http2/adapter/http2_util.cc
index 40deeb2..e054a82 100644
--- a/http2/adapter/http2_util.cc
+++ b/http2/adapter/http2_util.cc
@@ -36,5 +36,38 @@
   }
 }
 
+Http2ErrorCode TranslateErrorCode(spdy::SpdyErrorCode code) {
+  switch (code) {
+    case spdy::ERROR_CODE_NO_ERROR:
+      return Http2ErrorCode::NO_ERROR;
+    case spdy::ERROR_CODE_PROTOCOL_ERROR:
+      return Http2ErrorCode::PROTOCOL_ERROR;
+    case spdy::ERROR_CODE_INTERNAL_ERROR:
+      return Http2ErrorCode::INTERNAL_ERROR;
+    case spdy::ERROR_CODE_FLOW_CONTROL_ERROR:
+      return Http2ErrorCode::FLOW_CONTROL_ERROR;
+    case spdy::ERROR_CODE_SETTINGS_TIMEOUT:
+      return Http2ErrorCode::SETTINGS_TIMEOUT;
+    case spdy::ERROR_CODE_STREAM_CLOSED:
+      return Http2ErrorCode::STREAM_CLOSED;
+    case spdy::ERROR_CODE_FRAME_SIZE_ERROR:
+      return Http2ErrorCode::FRAME_SIZE_ERROR;
+    case spdy::ERROR_CODE_REFUSED_STREAM:
+      return Http2ErrorCode::REFUSED_STREAM;
+    case spdy::ERROR_CODE_CANCEL:
+      return Http2ErrorCode::CANCEL;
+    case spdy::ERROR_CODE_COMPRESSION_ERROR:
+      return Http2ErrorCode::COMPRESSION_ERROR;
+    case spdy::ERROR_CODE_CONNECT_ERROR:
+      return Http2ErrorCode::CONNECT_ERROR;
+    case spdy::ERROR_CODE_ENHANCE_YOUR_CALM:
+      return Http2ErrorCode::ENHANCE_YOUR_CALM;
+    case spdy::ERROR_CODE_INADEQUATE_SECURITY:
+      return Http2ErrorCode::INADEQUATE_SECURITY;
+    case spdy::ERROR_CODE_HTTP_1_1_REQUIRED:
+      return Http2ErrorCode::HTTP_1_1_REQUIRED;
+  }
+}
+
 }  // namespace adapter
 }  // namespace http2
diff --git a/http2/adapter/http2_util.h b/http2/adapter/http2_util.h
index e9ae2a5..3ace28b 100644
--- a/http2/adapter/http2_util.h
+++ b/http2/adapter/http2_util.h
@@ -8,6 +8,7 @@
 namespace adapter {
 
 spdy::SpdyErrorCode TranslateErrorCode(Http2ErrorCode code);
+Http2ErrorCode TranslateErrorCode(spdy::SpdyErrorCode code);
 
 }  // namespace adapter
 }  // namespace http2
diff --git a/http2/adapter/oghttp2_adapter_test.cc b/http2/adapter/oghttp2_adapter_test.cc
index fef15aa..799c9bf 100644
--- a/http2/adapter/oghttp2_adapter_test.cc
+++ b/http2/adapter/oghttp2_adapter_test.cc
@@ -1,6 +1,7 @@
 #include "http2/adapter/oghttp2_adapter.h"
 
 #include "http2/adapter/mock_http2_visitor.h"
+#include "http2/adapter/test_frame_sequence.h"
 #include "http2/adapter/test_utils.h"
 #include "common/platform/api/quiche_test.h"
 #include "common/platform/api/quiche_test_helpers.h"
@@ -13,7 +14,7 @@
 class OgHttp2AdapterTest : public testing::Test {
  protected:
   void SetUp() override {
-    OgHttp2Adapter::Options options;
+    OgHttp2Adapter::Options options{.perspective = Perspective::kServer};
     adapter_ = OgHttp2Adapter::Create(http2_visitor_, options);
   }
 
@@ -22,7 +23,11 @@
 };
 
 TEST_F(OgHttp2AdapterTest, ProcessBytes) {
-  EXPECT_QUICHE_BUG(adapter_->ProcessBytes("fake data"), "Not implemented");
+  EXPECT_CALL(http2_visitor_, OnSettingsStart());
+  EXPECT_CALL(http2_visitor_, OnSettingsEnd());
+  EXPECT_CALL(http2_visitor_, OnPing(17, false));
+  adapter_->ProcessBytes(
+      TestFrameSequence().ClientPreface().Ping(17).Serialize());
 }
 
 TEST_F(OgHttp2AdapterTest, SubmitMetadata) {
@@ -30,9 +35,7 @@
 }
 
 TEST_F(OgHttp2AdapterTest, GetPeerConnectionWindow) {
-  int peer_window = 0;
-  EXPECT_QUICHE_BUG(peer_window = adapter_->GetPeerConnectionWindow(),
-                    "Not implemented");
+  const int peer_window = adapter_->GetPeerConnectionWindow();
   EXPECT_GT(peer_window, 0);
 }
 
@@ -42,7 +45,7 @@
 }
 
 TEST_F(OgHttp2AdapterTest, TestSerialize) {
-  EXPECT_FALSE(adapter_->session().want_read());
+  EXPECT_TRUE(adapter_->session().want_read());
   EXPECT_FALSE(adapter_->session().want_write());
 
   adapter_->SubmitSettings(
diff --git a/http2/adapter/oghttp2_session.cc b/http2/adapter/oghttp2_session.cc
index bcb47d6..f434c3c 100644
--- a/http2/adapter/oghttp2_session.cc
+++ b/http2/adapter/oghttp2_session.cc
@@ -3,9 +3,58 @@
 namespace http2 {
 namespace adapter {
 
+void OgHttp2Session::PassthroughHeadersHandler::OnHeaderBlockStart() {
+  visitor_.OnBeginHeadersForStream(stream_id_);
+}
+
+void OgHttp2Session::PassthroughHeadersHandler::OnHeader(
+    absl::string_view key,
+    absl::string_view value) {
+  visitor_.OnHeaderForStream(stream_id_, key, value);
+}
+
+void OgHttp2Session::PassthroughHeadersHandler::OnHeaderBlockEnd(
+    size_t /* uncompressed_header_bytes */,
+    size_t /* compressed_header_bytes */) {
+  visitor_.OnEndHeadersForStream(stream_id_);
+}
+
+OgHttp2Session::OgHttp2Session(Http2VisitorInterface& visitor, Options options)
+    : visitor_(visitor), headers_handler_(visitor), options_(options) {
+  decoder_.set_visitor(this);
+  if (options_.perspective == Perspective::kServer) {
+    remaining_preface_ = {spdy::kHttp2ConnectionHeaderPrefix,
+                          spdy::kHttp2ConnectionHeaderPrefixSize};
+  }
+}
+
+OgHttp2Session::~OgHttp2Session() {}
+
 ssize_t OgHttp2Session::ProcessBytes(absl::string_view bytes) {
-  QUICHE_BUG(oghttp2_process_bytes) << "Not implemented";
-  return 0;
+  ssize_t preface_consumed = 0;
+  if (!remaining_preface_.empty()) {
+    QUICHE_VLOG(2) << "Preface bytes remaining: " << remaining_preface_.size();
+    // decoder_ does not understand the client connection preface.
+    size_t min_size = std::min(remaining_preface_.size(), bytes.size());
+    if (!absl::StartsWith(remaining_preface_, bytes.substr(0, min_size))) {
+      // Preface doesn't match!
+      QUICHE_DLOG(INFO) << "Preface doesn't match! Expected: ["
+                        << absl::CEscape(remaining_preface_) << "], actual: ["
+                        << absl::CEscape(bytes) << "]";
+      visitor_.OnConnectionError();
+      return -1;
+    }
+    remaining_preface_.remove_prefix(min_size);
+    bytes.remove_prefix(min_size);
+    if (!remaining_preface_.empty()) {
+      QUICHE_VLOG(2) << "Preface bytes remaining: "
+                     << remaining_preface_.size();
+      return min_size;
+    }
+    preface_consumed = min_size;
+  }
+  ssize_t result = decoder_.ProcessInput(bytes.data(), bytes.size());
+  return result < 0 ? result : result + preface_consumed;
 }
 
 int OgHttp2Session::Consume(Http2StreamId stream_id, size_t num_bytes) {
@@ -40,5 +89,136 @@
   return serialized;
 }
 
+void OgHttp2Session::OnError(http2::Http2DecoderAdapter::SpdyFramerError error,
+                             std::string detailed_error) {
+  QUICHE_VLOG(1) << "Error: "
+                 << http2::Http2DecoderAdapter::SpdyFramerErrorToString(error)
+                 << " details: " << detailed_error;
+  visitor_.OnConnectionError();
+}
+
+void OgHttp2Session::OnCommonHeader(spdy::SpdyStreamId /*stream_id*/,
+                                    size_t /*length*/,
+                                    uint8_t /*type*/,
+                                    uint8_t /*flags*/) {}
+
+void OgHttp2Session::OnDataFrameHeader(spdy::SpdyStreamId stream_id,
+                                       size_t length,
+                                       bool fin) {
+  visitor_.OnBeginDataForStream(stream_id, length);
+}
+
+void OgHttp2Session::OnStreamFrameData(spdy::SpdyStreamId stream_id,
+                                       const char* data,
+                                       size_t len) {
+  visitor_.OnDataForStream(stream_id, absl::string_view(data, len));
+}
+
+void OgHttp2Session::OnStreamEnd(spdy::SpdyStreamId stream_id) {
+  visitor_.OnEndStream(stream_id);
+}
+
+void OgHttp2Session::OnStreamPadLength(spdy::SpdyStreamId /*stream_id*/,
+                                       size_t /*value*/) {}
+
+void OgHttp2Session::OnStreamPadding(spdy::SpdyStreamId stream_id, size_t len) {
+}
+
+spdy::SpdyHeadersHandlerInterface* OgHttp2Session::OnHeaderFrameStart(
+    spdy::SpdyStreamId stream_id) {
+  headers_handler_.set_stream_id(stream_id);
+  return &headers_handler_;
+}
+
+void OgHttp2Session::OnHeaderFrameEnd(spdy::SpdyStreamId stream_id) {
+  headers_handler_.set_stream_id(0);
+}
+
+void OgHttp2Session::OnRstStream(spdy::SpdyStreamId stream_id,
+                                 spdy::SpdyErrorCode error_code) {
+  visitor_.OnRstStream(stream_id, TranslateErrorCode(error_code));
+  visitor_.OnAbortStream(stream_id, TranslateErrorCode(error_code));
+}
+
+void OgHttp2Session::OnSettings() {
+  visitor_.OnSettingsStart();
+}
+
+void OgHttp2Session::OnSetting(spdy::SpdySettingsId id, uint32_t value) {
+  visitor_.OnSetting({id, value});
+}
+
+void OgHttp2Session::OnSettingsEnd() {
+  visitor_.OnSettingsEnd();
+}
+
+void OgHttp2Session::OnSettingsAck() {
+  visitor_.OnSettingsAck();
+}
+
+void OgHttp2Session::OnPing(spdy::SpdyPingId unique_id, bool is_ack) {
+  visitor_.OnPing(unique_id, is_ack);
+}
+
+void OgHttp2Session::OnGoAway(spdy::SpdyStreamId last_accepted_stream_id,
+                              spdy::SpdyErrorCode error_code) {
+  received_goaway_ = true;
+  visitor_.OnGoAway(last_accepted_stream_id, TranslateErrorCode(error_code),
+                    "");
+}
+
+bool OgHttp2Session::OnGoAwayFrameData(const char* goaway_data, size_t len) {
+  // Opaque data is currently ignored.
+  return true;
+}
+
+void OgHttp2Session::OnHeaders(spdy::SpdyStreamId stream_id,
+                               bool has_priority,
+                               int weight,
+                               spdy::SpdyStreamId parent_stream_id,
+                               bool exclusive,
+                               bool fin,
+                               bool end) {}
+
+void OgHttp2Session::OnWindowUpdate(spdy::SpdyStreamId stream_id,
+                                    int delta_window_size) {
+  if (stream_id == 0) {
+    peer_window_ += delta_window_size;
+  } else {
+    auto it = stream_map_.find(stream_id);
+    if (it == stream_map_.end()) {
+      QUICHE_VLOG(1) << "Stream " << stream_id << " not found!";
+    } else {
+      it->second.send_window += delta_window_size;
+    }
+  }
+  visitor_.OnWindowUpdate(stream_id, delta_window_size);
+}
+
+void OgHttp2Session::OnPushPromise(spdy::SpdyStreamId stream_id,
+                                   spdy::SpdyStreamId promised_stream_id,
+                                   bool end) {}
+
+void OgHttp2Session::OnContinuation(spdy::SpdyStreamId stream_id, bool end) {}
+
+void OgHttp2Session::OnAltSvc(spdy::SpdyStreamId /*stream_id*/,
+                              absl::string_view /*origin*/,
+                              const spdy::SpdyAltSvcWireFormat::
+                                  AlternativeServiceVector& /*altsvc_vector*/) {
+}
+
+void OgHttp2Session::OnPriority(spdy::SpdyStreamId stream_id,
+                                spdy::SpdyStreamId parent_stream_id,
+                                int weight,
+                                bool exclusive) {}
+
+void OgHttp2Session::OnPriorityUpdate(spdy::SpdyStreamId prioritized_stream_id,
+                                      absl::string_view priority_field_value) {}
+
+bool OgHttp2Session::OnUnknownFrame(spdy::SpdyStreamId stream_id,
+                                    uint8_t frame_type) {
+  return true;
+}
+
 }  // namespace adapter
 }  // namespace http2
diff --git a/http2/adapter/oghttp2_session.h b/http2/adapter/oghttp2_session.h
index dfd0220..b0bc28b 100644
--- a/http2/adapter/oghttp2_session.h
+++ b/http2/adapter/oghttp2_session.h
@@ -4,23 +4,26 @@
 #include <list>
 
 #include "http2/adapter/http2_session.h"
+#include "http2/adapter/http2_util.h"
 #include "http2/adapter/http2_visitor_interface.h"
 #include "http2/adapter/window_manager.h"
 #include "common/platform/api/quiche_bug_tracker.h"
+#include "spdy/core/http2_frame_decoder_adapter.h"
 #include "spdy/core/spdy_framer.h"
 
 namespace http2 {
 namespace adapter {
 
 // This class manages state associated with a single multiplexed HTTP/2 session.
-class OgHttp2Session : public Http2Session {
+class OgHttp2Session : public Http2Session,
+                       public spdy::SpdyFramerVisitorInterface {
  public:
   struct Options {
-    Perspective context;
+    Perspective perspective = Perspective::kClient;
   };
 
-  OgHttp2Session(Http2VisitorInterface& /*visitor*/, Options /*options*/) {}
-  ~OgHttp2Session() override {}
+  OgHttp2Session(Http2VisitorInterface& visitor, Options /*options*/);
+  ~OgHttp2Session() override;
 
   // Enqueues a frame for transmission to the peer.
   void EnqueueFrame(std::unique_ptr<spdy::SpdyFrameIR> frame);
@@ -32,27 +35,104 @@
   // From Http2Session.
   ssize_t ProcessBytes(absl::string_view bytes) override;
   int Consume(Http2StreamId stream_id, size_t num_bytes) override;
-  bool want_read() const override { return false; }
+  bool want_read() const override { return !received_goaway_; }
   bool want_write() const override {
     return !frames_.empty() || !serialized_prefix_.empty();
   }
   int GetRemoteWindowSize() const override {
-    QUICHE_BUG(peer_window_not_updated) << "Not implemented";
     return peer_window_;
   }
 
+  // From SpdyFramerVisitorInterface
+  void OnError(http2::Http2DecoderAdapter::SpdyFramerError error,
+               std::string detailed_error) override;
+  void OnCommonHeader(spdy::SpdyStreamId /*stream_id*/,
+                      size_t /*length*/,
+                      uint8_t /*type*/,
+                      uint8_t /*flags*/) override;
+  void OnDataFrameHeader(spdy::SpdyStreamId stream_id,
+                         size_t length,
+                         bool fin) override;
+  void OnStreamFrameData(spdy::SpdyStreamId stream_id,
+                         const char* data,
+                         size_t len) override;
+  void OnStreamEnd(spdy::SpdyStreamId stream_id) override;
+  void OnStreamPadLength(spdy::SpdyStreamId /*stream_id*/,
+                         size_t /*value*/) override;
+  void OnStreamPadding(spdy::SpdyStreamId stream_id, size_t len) override;
+  spdy::SpdyHeadersHandlerInterface* OnHeaderFrameStart(
+      spdy::SpdyStreamId stream_id) override;
+  void OnHeaderFrameEnd(spdy::SpdyStreamId stream_id) override;
+  void OnRstStream(spdy::SpdyStreamId stream_id,
+                   spdy::SpdyErrorCode error_code) override;
+  void OnSettings() override;
+  void OnSetting(spdy::SpdySettingsId id, uint32_t value) override;
+  void OnSettingsEnd() override;
+  void OnSettingsAck() override;
+  void OnPing(spdy::SpdyPingId unique_id, bool is_ack) override;
+  void OnGoAway(spdy::SpdyStreamId last_accepted_stream_id,
+                spdy::SpdyErrorCode error_code) override;
+  bool OnGoAwayFrameData(const char* goaway_data, size_t len);
+  void OnHeaders(spdy::SpdyStreamId stream_id,
+                 bool has_priority,
+                 int weight,
+                 spdy::SpdyStreamId parent_stream_id,
+                 bool exclusive,
+                 bool fin,
+                 bool end) override;
+  void OnWindowUpdate(spdy::SpdyStreamId stream_id,
+                      int delta_window_size) override;
+  void OnPushPromise(spdy::SpdyStreamId stream_id,
+                     spdy::SpdyStreamId promised_stream_id,
+                     bool end) override;
+  void OnContinuation(spdy::SpdyStreamId stream_id, bool end) override;
+  void OnAltSvc(spdy::SpdyStreamId /*stream_id*/,
+                absl::string_view /*origin*/,
+                const spdy::SpdyAltSvcWireFormat::
+                    AlternativeServiceVector& /*altsvc_vector*/);
+  void OnPriority(spdy::SpdyStreamId stream_id,
+                  spdy::SpdyStreamId parent_stream_id,
+                  int weight,
+                  bool exclusive) override;
+  void OnPriorityUpdate(spdy::SpdyStreamId prioritized_stream_id,
+                        absl::string_view priority_field_value) override;
+  bool OnUnknownFrame(spdy::SpdyStreamId stream_id,
+                      uint8_t frame_type) override;
+
  private:
   struct StreamState {
     WindowManager window_manager;
+    int32_t send_window = 65535;
     bool half_closed_local = false;
     bool half_closed_remote = false;
   };
 
+  class PassthroughHeadersHandler : public spdy::SpdyHeadersHandlerInterface {
+   public:
+    explicit PassthroughHeadersHandler(Http2VisitorInterface& visitor)
+        : visitor_(visitor) {}
+    void set_stream_id(Http2StreamId stream_id) { stream_id_ = stream_id; }
+    void OnHeaderBlockStart() override;
+    void OnHeader(absl::string_view key, absl::string_view value) override;
+    void OnHeaderBlockEnd(size_t /* uncompressed_header_bytes */,
+                          size_t /* compressed_header_bytes */) override;
+
+   private:
+    Http2VisitorInterface& visitor_;
+    Http2StreamId stream_id_ = 0;
+  };
+
+  Http2VisitorInterface& visitor_;
   spdy::SpdyFramer framer_{spdy::SpdyFramer::ENABLE_COMPRESSION};
+  http2::Http2DecoderAdapter decoder_;
   absl::flat_hash_map<Http2StreamId, StreamState> stream_map_;
   std::list<std::unique_ptr<spdy::SpdyFrameIR>> frames_;
+  PassthroughHeadersHandler headers_handler_;
   std::string serialized_prefix_;
+  absl::string_view remaining_preface_;
   int peer_window_ = 65535;
+  Options options_;
+  bool received_goaway_ = false;
 };
 
 }  // namespace adapter
diff --git a/http2/adapter/oghttp2_session_test.cc b/http2/adapter/oghttp2_session_test.cc
new file mode 100644
index 0000000..7de1514
--- /dev/null
+++ b/http2/adapter/oghttp2_session_test.cc
@@ -0,0 +1,146 @@
+#include "http2/adapter/oghttp2_session.h"
+
+#include "http2/adapter/mock_http2_visitor.h"
+#include "http2/adapter/test_frame_sequence.h"
+#include "common/platform/api/quiche_test.h"
+
+namespace http2 {
+namespace adapter {
+namespace test {
+
+TEST(OgHttp2SessionTest, ClientConstruction) {
+  testing::StrictMock<MockHttp2Visitor> visitor;
+  OgHttp2Session session(
+      visitor, OgHttp2Session::Options{.perspective = Perspective::kClient});
+  EXPECT_TRUE(session.want_read());
+  EXPECT_FALSE(session.want_write());
+  EXPECT_EQ(session.GetRemoteWindowSize(), kDefaultInitialStreamWindowSize);
+}
+
+TEST(OgHttp2SessionTest, ClientHandlesFrames) {
+  testing::StrictMock<MockHttp2Visitor> visitor;
+  OgHttp2Session session(
+      visitor, OgHttp2Session::Options{.perspective = Perspective::kClient});
+
+  const std::string initial_frames = TestFrameSequence()
+                                         .ServerPreface()
+                                         .Ping(42)
+                                         .WindowUpdate(0, 1000)
+                                         .Serialize();
+  testing::InSequence s;
+
+  // Server preface (empty SETTINGS)
+  EXPECT_CALL(visitor, OnSettingsStart());
+  EXPECT_CALL(visitor, OnSettingsEnd());
+
+  EXPECT_CALL(visitor, OnPing(42, false));
+  EXPECT_CALL(visitor, OnWindowUpdate(0, 1000));
+
+  const ssize_t initial_result = session.ProcessBytes(initial_frames);
+  EXPECT_EQ(initial_frames.size(), initial_result);
+
+  EXPECT_EQ(session.GetRemoteWindowSize(),
+            kDefaultInitialStreamWindowSize + 1000);
+
+  // Should OgHttp2Session require that streams 1 and 3 have been created?
+
+  const std::string stream_frames =
+      TestFrameSequence()
+          .Headers(1,
+                   {{":status", "200"},
+                    {"server", "my-fake-server"},
+                    {"date", "Tue, 6 Apr 2021 12:54:01 GMT"}},
+                   /*fin=*/false)
+          .Data(1, "This is the response body.")
+          .RstStream(3, Http2ErrorCode::INTERNAL_ERROR)
+          .GoAway(5, Http2ErrorCode::ENHANCE_YOUR_CALM, "calm down!!")
+          .Serialize();
+
+  EXPECT_CALL(visitor, OnBeginHeadersForStream(1));
+  EXPECT_CALL(visitor, OnHeaderForStream(1, ":status", "200"));
+  EXPECT_CALL(visitor, OnHeaderForStream(1, "server", "my-fake-server"));
+  EXPECT_CALL(visitor,
+              OnHeaderForStream(1, "date", "Tue, 6 Apr 2021 12:54:01 GMT"));
+  EXPECT_CALL(visitor, OnEndHeadersForStream(1));
+  EXPECT_CALL(visitor, OnBeginDataForStream(1, 26));
+  EXPECT_CALL(visitor, OnDataForStream(1, "This is the response body."));
+  EXPECT_CALL(visitor, OnRstStream(3, Http2ErrorCode::INTERNAL_ERROR));
+  EXPECT_CALL(visitor, OnAbortStream(3, Http2ErrorCode::INTERNAL_ERROR));
+  EXPECT_CALL(visitor, OnGoAway(5, Http2ErrorCode::ENHANCE_YOUR_CALM, ""));
+  const ssize_t stream_result = session.ProcessBytes(stream_frames);
+  EXPECT_EQ(stream_frames.size(), stream_result);
+}
+
+TEST(OgHttp2SessionTest, ServerConstruction) {
+  testing::StrictMock<MockHttp2Visitor> visitor;
+  OgHttp2Session session(
+      visitor, OgHttp2Session::Options{.perspective = Perspective::kServer});
+  EXPECT_TRUE(session.want_read());
+  EXPECT_FALSE(session.want_write());
+  EXPECT_EQ(session.GetRemoteWindowSize(), kDefaultInitialStreamWindowSize);
+}
+
+TEST(OgHttp2SessionTest, ServerHandlesFrames) {
+  testing::StrictMock<MockHttp2Visitor> visitor;
+  OgHttp2Session session(
+      visitor, OgHttp2Session::Options{.perspective = Perspective::kServer});
+
+  const std::string frames = TestFrameSequence()
+                                 .ClientPreface()
+                                 .Ping(42)
+                                 .WindowUpdate(0, 1000)
+                                 .Headers(1,
+                                          {{":method", "POST"},
+                                           {":scheme", "https"},
+                                           {":authority", "example.com"},
+                                           {":path", "/this/is/request/one"}},
+                                          /*fin=*/false)
+                                 .WindowUpdate(1, 2000)
+                                 .Data(1, "This is the request body.")
+                                 .Headers(3,
+                                          {{":method", "GET"},
+                                           {":scheme", "http"},
+                                           {":authority", "example.com"},
+                                           {":path", "/this/is/request/two"}},
+                                          /*fin=*/true)
+                                 .RstStream(3, Http2ErrorCode::CANCEL)
+                                 .Ping(47)
+                                 .Serialize();
+  testing::InSequence s;
+
+  // Client preface (empty SETTINGS)
+  EXPECT_CALL(visitor, OnSettingsStart());
+  EXPECT_CALL(visitor, OnSettingsEnd());
+
+  EXPECT_CALL(visitor, OnPing(42, false));
+  EXPECT_CALL(visitor, OnWindowUpdate(0, 1000));
+  EXPECT_CALL(visitor, OnBeginHeadersForStream(1));
+  EXPECT_CALL(visitor, OnHeaderForStream(1, ":method", "POST"));
+  EXPECT_CALL(visitor, OnHeaderForStream(1, ":scheme", "https"));
+  EXPECT_CALL(visitor, OnHeaderForStream(1, ":authority", "example.com"));
+  EXPECT_CALL(visitor, OnHeaderForStream(1, ":path", "/this/is/request/one"));
+  EXPECT_CALL(visitor, OnEndHeadersForStream(1));
+  EXPECT_CALL(visitor, OnWindowUpdate(1, 2000));
+  EXPECT_CALL(visitor, OnBeginDataForStream(1, 25));
+  EXPECT_CALL(visitor, OnDataForStream(1, "This is the request body."));
+  EXPECT_CALL(visitor, OnBeginHeadersForStream(3));
+  EXPECT_CALL(visitor, OnHeaderForStream(3, ":method", "GET"));
+  EXPECT_CALL(visitor, OnHeaderForStream(3, ":scheme", "http"));
+  EXPECT_CALL(visitor, OnHeaderForStream(3, ":authority", "example.com"));
+  EXPECT_CALL(visitor, OnHeaderForStream(3, ":path", "/this/is/request/two"));
+  EXPECT_CALL(visitor, OnEndHeadersForStream(3));
+  EXPECT_CALL(visitor, OnEndStream(3));
+  EXPECT_CALL(visitor, OnRstStream(3, Http2ErrorCode::CANCEL));
+  EXPECT_CALL(visitor, OnAbortStream(3, Http2ErrorCode::CANCEL));
+  EXPECT_CALL(visitor, OnPing(47, false));
+
+  const ssize_t result = session.ProcessBytes(frames);
+  EXPECT_EQ(frames.size(), result);
+
+  EXPECT_EQ(session.GetRemoteWindowSize(),
+            kDefaultInitialStreamWindowSize + 1000);
+}
+
+}  // namespace test
+}  // namespace adapter
+}  // namespace http2