From cb32069a28f5aa9972223e2b59dd9f15d041f825 Mon Sep 17 00:00:00 2001 From: Juan Cruz Viotti Date: Tue, 21 Nov 2023 19:34:32 -0400 Subject: [PATCH] [WIP] Implement more tests Signed-off-by: Juan Cruz Viotti --- .../include/sourcemeta/hydra/http_request.h | 2 + .../include/sourcemeta/hydra/http_response.h | 1 + src/http/request.cc | 4 + src/http/response.cc | 8 +- src/http/stream_curl.cc | 13 +- test/http/request_1_1_test.cc | 136 +++++++++++++++++- test/http/stub.js | 25 ++-- 7 files changed, 171 insertions(+), 18 deletions(-) diff --git a/src/http/include/sourcemeta/hydra/http_request.h b/src/http/include/sourcemeta/hydra/http_request.h index 1411d8ef..b6b1e5ce 100644 --- a/src/http/include/sourcemeta/hydra/http_request.h +++ b/src/http/include/sourcemeta/hydra/http_request.h @@ -26,6 +26,8 @@ class SOURCEMETA_HYDRA_HTTP_EXPORT Request { auto capture(std::string header) -> void; auto capture(std::initializer_list headers) -> void; auto header(std::string_view key, std::string_view value) -> void; + auto header(std::string_view key, int value) -> void; + auto send() -> std::future; private: diff --git a/src/http/include/sourcemeta/hydra/http_response.h b/src/http/include/sourcemeta/hydra/http_response.h index 4415af64..3b622642 100644 --- a/src/http/include/sourcemeta/hydra/http_response.h +++ b/src/http/include/sourcemeta/hydra/http_response.h @@ -23,6 +23,7 @@ class SOURCEMETA_HYDRA_HTTP_EXPORT Response { auto status() const noexcept -> Status; auto header(const std::string &key) const -> std::optional; + auto empty() noexcept -> bool; auto body() -> std::istringstream &; private: diff --git a/src/http/request.cc b/src/http/request.cc index 6ee7838b..daaa10d1 100644 --- a/src/http/request.cc +++ b/src/http/request.cc @@ -35,6 +35,10 @@ auto Request::header(std::string_view key, std::string_view value) -> void { this->stream.header(key, value); } +auto Request::header(std::string_view key, int value) -> void { + this->stream.header(key, std::to_string(value)); +} + auto Request::send() -> std::future { std::ostringstream output; this->stream.on_data( diff --git a/src/http/response.cc b/src/http/response.cc index 555d1715..081ad8c2 100644 --- a/src/http/response.cc +++ b/src/http/response.cc @@ -1,6 +1,7 @@ #include #include +#include // assert #include // std::map #include // std::optional, std::nullopt #include // std::ostringstream, std::istringstream @@ -26,6 +27,11 @@ auto Response::header(const std::string &key) const return this->headers_.at(key); } -auto Response::body() -> std::istringstream & { return this->stream_; } +auto Response::empty() noexcept -> bool { return this->stream_.peek() == -1; } + +auto Response::body() -> std::istringstream & { + assert(!this->empty()); + return this->stream_; +} } // namespace sourcemeta::hydra::http diff --git a/src/http/stream_curl.cc b/src/http/stream_curl.cc index 3db2d5c1..c670baf2 100644 --- a/src/http/stream_curl.cc +++ b/src/http/stream_curl.cc @@ -5,11 +5,14 @@ #include +#include // std::transform #include // assert +#include // std::tolower #include // std::from_chars #include // std::size_t #include // std::uint64_t #include // std::future +#include // std::back_inserter #include // std::make_unique #include // std::optional #include // std::stringstream @@ -107,9 +110,15 @@ auto callback_on_header(const void *const data, const std::size_t size, if (request->internal->on_header) { try { assert(request->internal->status.has_value()); + + std::string_view key{line.substr(key_start, key_end - key_start)}; + std::string key_lowercase; + std::transform( + key.cbegin(), key.cend(), std::back_inserter(key_lowercase), + [](unsigned char character) { return std::tolower(character); }); + request->internal->on_header( - request->internal->status.value(), - line.substr(key_start, key_end - key_start), + request->internal->status.value(), key_lowercase, line.substr(value_start, value_end - value_start)); } catch (...) { return 0; diff --git a/test/http/request_1_1_test.cc b/test/http/request_1_1_test.cc index 5029bdff..30d773f5 100644 --- a/test/http/request_1_1_test.cc +++ b/test/http/request_1_1_test.cc @@ -1,11 +1,145 @@ +#include +#include +#include +#include + #include #include // TODO: Add more tests +// - Request body +// - Request header +// - Response header with capture +// - Response header without capture +// - Response header with partial intersection capture + +static auto body(sourcemeta::hydra::http::Response &response) -> std::string { + std::ostringstream result; + std::copy( + std::istreambuf_iterator(response.body()), + std::istreambuf_iterator(), + std::ostreambuf_iterator(result)); + return result.str(); +} + +TEST(HTTP_Request_1_1, GET_root) { + sourcemeta::hydra::http::Request request{BASE_URL}; + request.method(sourcemeta::hydra::http::Method::GET); + sourcemeta::hydra::http::Response response{request.send().get()}; + EXPECT_EQ(response.status(), sourcemeta::hydra::http::Status::OK); + EXPECT_FALSE(response.empty()); + EXPECT_EQ(body(response), "RECEIVED GET /"); +} + +TEST(HTTP_Request_1_1, HEAD_root) { + sourcemeta::hydra::http::Request request{BASE_URL}; + request.method(sourcemeta::hydra::http::Method::HEAD); + sourcemeta::hydra::http::Response response{request.send().get()}; + EXPECT_EQ(response.status(), sourcemeta::hydra::http::Status::OK); + EXPECT_TRUE(response.empty()); +} + +TEST(HTTP_Request_1_1, POST_root) { + sourcemeta::hydra::http::Request request{BASE_URL}; + request.method(sourcemeta::hydra::http::Method::POST); + sourcemeta::hydra::http::Response response{request.send().get()}; + EXPECT_EQ(response.status(), sourcemeta::hydra::http::Status::OK); + EXPECT_FALSE(response.empty()); + EXPECT_EQ(body(response), "RECEIVED POST /"); +} + +TEST(HTTP_Request_1_1, PUT_root) { + sourcemeta::hydra::http::Request request{BASE_URL}; + request.method(sourcemeta::hydra::http::Method::PUT); + sourcemeta::hydra::http::Response response{request.send().get()}; + EXPECT_EQ(response.status(), sourcemeta::hydra::http::Status::OK); + EXPECT_FALSE(response.empty()); + EXPECT_EQ(body(response), "RECEIVED PUT /"); +} + +TEST(HTTP_Request_1_1, DELETE_root) { + sourcemeta::hydra::http::Request request{BASE_URL}; + request.method(sourcemeta::hydra::http::Method::DELETE); + sourcemeta::hydra::http::Response response{request.send().get()}; + EXPECT_EQ(response.status(), sourcemeta::hydra::http::Status::OK); + EXPECT_FALSE(response.empty()); + EXPECT_EQ(body(response), "RECEIVED DELETE /"); +} + +TEST(HTTP_Request_1_1, OPTIONS_root) { + sourcemeta::hydra::http::Request request{BASE_URL}; + request.method(sourcemeta::hydra::http::Method::OPTIONS); + sourcemeta::hydra::http::Response response{request.send().get()}; + EXPECT_EQ(response.status(), sourcemeta::hydra::http::Status::OK); + EXPECT_FALSE(response.empty()); + EXPECT_EQ(body(response), "RECEIVED OPTIONS /"); +} + +TEST(HTTP_Request_1_1, TRACE_root) { + sourcemeta::hydra::http::Request request{BASE_URL}; + request.method(sourcemeta::hydra::http::Method::TRACE); + sourcemeta::hydra::http::Response response{request.send().get()}; + EXPECT_EQ(response.status(), sourcemeta::hydra::http::Status::OK); + EXPECT_FALSE(response.empty()); + EXPECT_EQ(body(response), "RECEIVED TRACE /"); +} + +TEST(HTTP_Request_1_1, PATCH_root) { + sourcemeta::hydra::http::Request request{BASE_URL}; + request.method(sourcemeta::hydra::http::Method::PATCH); + sourcemeta::hydra::http::Response response{request.send().get()}; + EXPECT_EQ(response.status(), sourcemeta::hydra::http::Status::OK); + EXPECT_FALSE(response.empty()); + EXPECT_EQ(body(response), "RECEIVED PATCH /"); +} + +TEST(HTTP_Request_1_1, GET_root_foo_bar) { + sourcemeta::hydra::http::Request request{std::string{BASE_URL} + "/foo/bar"}; + request.method(sourcemeta::hydra::http::Method::GET); + sourcemeta::hydra::http::Response response{request.send().get()}; + EXPECT_EQ(response.status(), sourcemeta::hydra::http::Status::OK); + EXPECT_FALSE(response.empty()); + EXPECT_EQ(body(response), "RECEIVED GET /foo/bar"); +} + +TEST(HTTP_Request_1_1, GET_root_custom_code_string) { + sourcemeta::hydra::http::Request request{BASE_URL}; + request.method(sourcemeta::hydra::http::Method::GET); + request.header("X-Code", "400"); + sourcemeta::hydra::http::Response response{request.send().get()}; + EXPECT_EQ(response.status(), sourcemeta::hydra::http::Status::BAD_REQUEST); + EXPECT_FALSE(response.empty()); + EXPECT_EQ(body(response), "RECEIVED GET /"); +} + +TEST(HTTP_Request_1_1, GET_root_custom_code_integer) { + sourcemeta::hydra::http::Request request{BASE_URL}; + request.method(sourcemeta::hydra::http::Method::GET); + request.header("X-Code", 400); + sourcemeta::hydra::http::Response response{request.send().get()}; + EXPECT_EQ(response.status(), sourcemeta::hydra::http::Status::BAD_REQUEST); + EXPECT_FALSE(response.empty()); + EXPECT_EQ(body(response), "RECEIVED GET /"); +} + +TEST(HTTP_Request_1_1, GET_root_response_content_type_no_capture) { + sourcemeta::hydra::http::Request request{BASE_URL}; + request.method(sourcemeta::hydra::http::Method::GET); + sourcemeta::hydra::http::Response response{request.send().get()}; + EXPECT_EQ(response.status(), sourcemeta::hydra::http::Status::OK); + EXPECT_FALSE(response.empty()); + EXPECT_EQ(body(response), "RECEIVED GET /"); + EXPECT_FALSE(response.header("content-type").has_value()); +} -TEST(HTTP_Request_1_1, XXX) { +TEST(HTTP_Request_1_1, GET_root_response_content_type_capture) { sourcemeta::hydra::http::Request request{BASE_URL}; request.method(sourcemeta::hydra::http::Method::GET); + request.capture("content-type"); sourcemeta::hydra::http::Response response{request.send().get()}; EXPECT_EQ(response.status(), sourcemeta::hydra::http::Status::OK); + EXPECT_FALSE(response.empty()); + EXPECT_EQ(body(response), "RECEIVED GET /"); + EXPECT_TRUE(response.header("content-type").has_value()); + EXPECT_EQ(response.header("content-type").value(), "text/plain"); } diff --git a/test/http/stub.js b/test/http/stub.js index 8e134ecb..98709d6a 100644 --- a/test/http/stub.js +++ b/test/http/stub.js @@ -1,20 +1,17 @@ const http = require('http'); +const PORT = 9999; -const server = http.createServer((req, res) => { - // Set the response header to indicate JSON content - res.setHeader('Content-Type', 'application/json'); - - // Define a simple JSON response - const jsonResponse = { - message: 'Hello, JSON World!', - timestamp: new Date().toISOString(), - }; +http.createServer((request, response) => { + for (const [key, value] of Object.entries(request.headers)) { + response.setHeader(`X-${key}`, value); + } - // Send the JSON response - res.end(JSON.stringify(jsonResponse)); -}); + if (request.headers['x-code']) { + response.statusCode = parseInt(request.headers['x-code'], 10); + } -const PORT = 9999; -server.listen(PORT, () => { + response.setHeader('Content-Type', 'text/plain'); + response.end(`RECEIVED ${request.method} ${request.url}`); +}).listen(PORT, () => { console.log(`Server is listening on port ${PORT}`); });