Write unit tests for http2 connection

This unit test currently only tests a simple connect and settings frame
transfer, but should form the basis for more complex testing in the
future.

Tested: Unit tests pass

Change-Id: Ieb803dbe490129ec5fe99fb3d4505a06202e282e
Signed-off-by: Ed Tanous <ed@tanous.net>
diff --git a/http/nghttp2_adapters.hpp b/http/nghttp2_adapters.hpp
index 05ea68d..aeb18d6 100644
--- a/http/nghttp2_adapters.hpp
+++ b/http/nghttp2_adapters.hpp
@@ -152,3 +152,42 @@
   private:
     nghttp2_session* ptr = nullptr;
 };
+
+class nghttp2_hd_inflater
+{
+    nghttp2_hd_inflater* ptr = nullptr;
+
+  public:
+    nghttp2_hd_inflater()
+    {
+        if (nghttp2_hd_inflate_new(&ptr) != 0)
+        {
+            BMCWEB_LOG_ERROR("nghttp2_hd_inflater_new failed");
+        }
+    }
+
+    ssize_t hd2(nghttp2_nv* nvOut, int* inflateFlags, const uint8_t* in,
+                size_t inlen, int inFinal)
+    {
+        return nghttp2_hd_inflate_hd2(ptr, nvOut, inflateFlags, in, inlen,
+                                      inFinal);
+    }
+
+    int endHeaders()
+    {
+        return nghttp2_hd_inflate_end_headers(ptr);
+    }
+
+    nghttp2_hd_inflater(const nghttp2_hd_inflater&) = delete;
+    nghttp2_hd_inflater& operator=(const nghttp2_hd_inflater&) = delete;
+    nghttp2_hd_inflater& operator=(nghttp2_hd_inflater&&) = delete;
+    nghttp2_hd_inflater(nghttp2_hd_inflater&& other) = delete;
+
+    ~nghttp2_hd_inflater()
+    {
+        if (ptr != nullptr)
+        {
+            nghttp2_hd_inflate_del(ptr);
+        }
+    }
+};
diff --git a/meson.build b/meson.build
index f260f86..6184f62 100644
--- a/meson.build
+++ b/meson.build
@@ -422,6 +422,7 @@
 
 srcfiles_unittest = files(
   'test/http/crow_getroutes_test.cpp',
+  'test/http/http2_connection_test.cpp',
   'test/http/http_connection_test.cpp',
   'test/http/mutual_tls.cpp',
   'test/http/router_test.cpp',
diff --git a/test/http/http2_connection_test.cpp b/test/http/http2_connection_test.cpp
new file mode 100644
index 0000000..1c6ae6c
--- /dev/null
+++ b/test/http/http2_connection_test.cpp
@@ -0,0 +1,184 @@
+#include "http/http2_connection.hpp"
+#include "http/http_request.hpp"
+#include "http/http_response.hpp"
+
+#include <boost/asio/steady_timer.hpp>
+#include <boost/beast/_experimental/test/stream.hpp>
+
+#include <bit>
+#include <filesystem>
+#include <fstream>
+#include <functional>
+#include <memory>
+#include <string>
+#include <string_view>
+#include <utility>
+#include <vector>
+
+#include "gmock/gmock.h"
+#include "gtest/gtest.h"
+namespace crow
+{
+
+namespace
+{
+
+using ::testing::Pair;
+using ::testing::UnorderedElementsAre;
+
+struct FakeHandler
+{
+    bool called = false;
+    void handle(Request& req,
+                const std::shared_ptr<bmcweb::AsyncResp>& asyncResp)
+    {
+        called = true;
+        EXPECT_EQ(req.url().buffer(), "/redfish/v1/");
+        EXPECT_EQ(req.methodString(), "GET");
+        EXPECT_EQ(req.getHeaderValue(boost::beast::http::field::user_agent),
+                  "curl/8.5.0");
+        EXPECT_EQ(req.getHeaderValue(boost::beast::http::field::accept), "*/*");
+        EXPECT_EQ(req.getHeaderValue(":authority"), "localhost:18080");
+        asyncResp->res.write("StringOutput");
+    }
+};
+
+std::string getDateStr()
+{
+    return "TestTime";
+}
+
+void unpackHeaders(std::string_view dataField,
+                   std::vector<std::pair<std::string, std::string>>& headers)
+{
+    nghttp2_hd_inflater inflater;
+
+    while (!dataField.empty())
+    {
+        nghttp2_nv nv;
+        int inflateFlags = 0;
+        const uint8_t* data = std::bit_cast<const uint8_t*>(dataField.data());
+        ssize_t parsed = inflater.hd2(&nv, &inflateFlags, data,
+                                      dataField.size(), 1);
+
+        ASSERT_GT(parsed, 0);
+        dataField.remove_prefix(static_cast<size_t>(parsed));
+        if ((inflateFlags & NGHTTP2_HD_INFLATE_EMIT) > 0)
+        {
+            const char* namePtr = std::bit_cast<const char*>(nv.name);
+            std::string key(namePtr, nv.namelen);
+            const char* valPtr = std::bit_cast<const char*>(nv.value);
+            std::string value(valPtr, nv.valuelen);
+            headers.emplace_back(key, value);
+        }
+        if ((inflateFlags & NGHTTP2_HD_INFLATE_FINAL) > 0)
+        {
+            EXPECT_EQ(inflater.endHeaders(), 0);
+            break;
+        }
+    }
+    EXPECT_TRUE(dataField.empty());
+}
+
+TEST(http_connection, RequestPropogates)
+{
+    using namespace std::literals;
+    boost::asio::io_context io;
+    boost::beast::test::stream stream(io);
+    boost::beast::test::stream out(io);
+    stream.connect(out);
+    // This is a binary pre-encrypted stream captured from curl for a request to
+    // curl https://localhost:18080/redfish/v1/
+    std::string_view toSend =
+        // Hello
+        "PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n"
+        // 18 byte settings frame
+        "\x00\x00\x12\x04\x00\x00\x00\x00\x00"
+        // Settings
+        "\x00\x03\x00\x00\x00\x64\x00\x04\x00\xa0\x00\x00\x00\x02\x00\x00\x00\x00"
+        // Window update frame
+        "\x00\x00\x04\x08\x00\x00\x00\x00\x00"
+        // Window update
+        "\x3e\x7f\x00\x01"
+        // Header frame END_STREAM, END_HEADERS set
+        "\x00\x00\x29\x01\x05\x00\x00\x00"
+        // Header payload
+        "\x01\x82\x87\x41\x8b\xa0\xe4\x1d\x13\x9d\x09\xb8\x17\x80\xf0\x3f"
+        "\x04\x89\x62\xc2\xc9\x29\x91\x3b\x1d\xc2\xc7\x7a\x88\x25\xb6\x50"
+        "\xc3\xcb\xb6\xb8\x3f\x53\x03\x2a\x2f\x2a"sv;
+
+    boost::asio::write(out, boost::asio::buffer(toSend));
+
+    FakeHandler handler;
+    boost::asio::steady_timer timer(io);
+    std::function<std::string()> date(getDateStr);
+    auto conn = std::make_shared<
+        HTTP2Connection<boost::beast::test::stream, FakeHandler>>(
+        std::move(stream), &handler, date);
+    conn->start();
+
+    std::string_view expectedPrefix =
+        // Settings frame size 13
+        "\x00\x00\x0c\x04\x00\x00\x00\x00\x00"
+        // 4 max concurrent streams
+        "\x00\x03\x00\x00\x00\x04"
+        // Enable push = false
+        "\x00\x02\x00\x00\x00\x00"
+        // Settings ACK from server to client
+        "\x00\x00\x00\x04\x01\x00\x00\x00\x00"
+
+        // Start Headers frame stream 1, size 0x0346
+        "\x00\x03\x46\x01\x04\x00\x00\x00\x01"sv;
+
+    std::string_view expectedPostfix =
+        // Data Frame, Length 12, Stream 1, End Stream flag set
+        "\x00\x00\x0c\x00\x01\x00\x00\x00\x01"
+        // The body expected
+        "StringOutput"sv;
+
+    std::string_view outStr;
+    constexpr size_t headerSize = 0x346;
+
+    // Run until we receive the expected amount of data
+    while (outStr.size() <
+           expectedPrefix.size() + headerSize + expectedPostfix.size())
+    {
+        io.run_one();
+        outStr = out.str();
+    }
+    EXPECT_TRUE(handler.called);
+
+    // check the stream output against expected
+    EXPECT_TRUE(outStr.starts_with(expectedPrefix));
+    outStr.remove_prefix(expectedPrefix.size());
+    std::vector<std::pair<std::string, std::string>> headers;
+    unpackHeaders(outStr.substr(0, headerSize), headers);
+    outStr.remove_prefix(headerSize);
+
+    EXPECT_THAT(
+        headers,
+        UnorderedElementsAre(
+            Pair(":status", "200"),
+            Pair("strict-transport-security",
+                 "max-age=31536000; includeSubdomains"),
+            Pair("x-frame-options", "DENY"), Pair("pragma", "no-cache"),
+            Pair("cache-control", "no-store, max-age=0"),
+            Pair("x-content-type-options", "nosniff"),
+            Pair("referrer-policy", "no-referrer"),
+            Pair(
+                "permissions-policy",
+                "accelerometer=(),ambient-light-sensor=(),autoplay=(),battery=(),camera=(),display-capture=(),document-domain=(),encrypted-media=(),fullscreen=(),gamepad=(),geolocation=(),gyroscope=(),layout-animations=(self),legacy-image-formats=(self),magnetometer=(),microphone=(),midi=(),oversized-images=(self),payment=(),picture-in-picture=(),publickey-credentials-get=(),speaker-selection=(),sync-xhr=(self),unoptimized-images=(self),unsized-media=(self),usb=(),screen-wak-lock=(),web-share=(),xr-spatial-tracking=()"),
+            Pair("x-permitted-cross-domain-policies", "none"),
+            Pair("cross-origin-embedder-policy", "require-corp"),
+            Pair("cross-origin-opener-policy", "same-origin"),
+            Pair("cross-origin-resource-policy", "same-origin"),
+            Pair(
+                "content-security-policy",
+                "default-src 'none'; img-src 'self' data:; font-src 'self'; style-src 'self'; script-src 'self'; connect-src 'self' wss:; form-action 'none'; frame-ancestors 'none'; object-src 'none'; base-uri 'none'"),
+            Pair("date", "TestTime")));
+
+    EXPECT_EQ(outStr, expectedPostfix);
+}
+
+} // namespace
+} // namespace crow