Skip to content

Commit

Permalink
[WIP] Implement more tests
Browse files Browse the repository at this point in the history
Signed-off-by: Juan Cruz Viotti <[email protected]>
  • Loading branch information
jviotti committed Nov 22, 2023
1 parent ae16866 commit 34668da
Show file tree
Hide file tree
Showing 9 changed files with 259 additions and 22 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -107,10 +107,10 @@ jobs:
# Run stubs
- name: Run HTTP stub (Windows)
run: cmd /c "start /b node test\http\stub.js" && Start-Sleep -Seconds 2
run: cmd /c "start /b node test\http\stub.js" && Start-Sleep -Seconds 10
if: runner.os == 'windows'
- name: Run HTTP stub (*nix)
run: (node test/http/stub.js &) && sleep 2
run: (node test/http/stub.js &) && sleep 10
if: runner.os != 'windows'

# Not every CTest version supports the --test-dir option. If such option
Expand Down
1 change: 1 addition & 0 deletions src/http/include/sourcemeta/hydra/http_request.h
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@

namespace sourcemeta::hydra::http {

// TODO: Support passing a request body
class SOURCEMETA_HYDRA_HTTP_EXPORT Request {
public:
Request(std::string url);
Expand Down
1 change: 1 addition & 0 deletions src/http/include/sourcemeta/hydra/http_response.h
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ class SOURCEMETA_HYDRA_HTTP_EXPORT Response {

auto status() const noexcept -> Status;
auto header(const std::string &key) const -> std::optional<std::string>;
auto empty() noexcept -> bool;
auto body() -> std::istringstream &;

private:
Expand Down
1 change: 1 addition & 0 deletions src/http/include/sourcemeta/hydra/http_stream.h
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@

namespace sourcemeta::hydra::http {

// TODO: Unit test the streaming interface
class SOURCEMETA_HYDRA_HTTP_EXPORT Stream {
public:
Stream(std::string url);
Expand Down
8 changes: 7 additions & 1 deletion src/http/response.cc
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#include <sourcemeta/hydra/http_response.h>
#include <sourcemeta/hydra/http_status.h>

#include <cassert> // assert
#include <map> // std::map
#include <optional> // std::optional, std::nullopt
#include <sstream> // std::ostringstream, std::istringstream
Expand All @@ -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
13 changes: 11 additions & 2 deletions src/http/stream_curl.cc
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,14 @@

#include <curl/curl.h>

#include <algorithm> // std::transform
#include <cassert> // assert
#include <cctype> // std::tolower
#include <charconv> // std::from_chars
#include <cstddef> // std::size_t
#include <cstdint> // std::uint64_t
#include <future> // std::future
#include <iterator> // std::back_inserter
#include <memory> // std::make_unique
#include <optional> // std::optional
#include <sstream> // std::stringstream
Expand Down Expand Up @@ -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;
Expand Down
3 changes: 2 additions & 1 deletion test/http/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
# Tests
# TODO: Add HTTP/2 tests

add_executable(sourcemeta_hydra_http_unit
request_1_1_test.cc)
sourcemeta_hydra_add_compile_options(sourcemeta_hydra_http_unit)
Expand Down
225 changes: 223 additions & 2 deletions test/http/request_1_1_test.cc
Original file line number Diff line number Diff line change
@@ -1,11 +1,232 @@
#include <algorithm>
#include <iterator>
#include <sstream>
#include <string>

#include <gtest/gtest.h>
#include <sourcemeta/hydra/http.h>

// TODO: Add more tests
static auto body(sourcemeta::hydra::http::Response &response) -> std::string {
std::ostringstream result;
std::copy(
std::istreambuf_iterator<std::ostringstream::char_type>(response.body()),
std::istreambuf_iterator<std::ostringstream::char_type>(),
std::ostreambuf_iterator<std::ostringstream::char_type>(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, 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");
}

TEST(HTTP_Request_1_1, GET_root_missing_header_with_capture) {
sourcemeta::hydra::http::Request request{BASE_URL};
request.method(sourcemeta::hydra::http::Method::GET);
request.capture("x-foo-bar");
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("x-foo-bar").has_value());
}

TEST(HTTP_Request_1_1, GET_root_multiple_captures_match) {
sourcemeta::hydra::http::Request request{BASE_URL};
request.method(sourcemeta::hydra::http::Method::GET);
request.capture("content-type");
request.capture("content-length");
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_TRUE(response.header("content-length").has_value());
EXPECT_EQ(response.header("content-type").value(), "text/plain");
EXPECT_EQ(response.header("content-length").value(), "14");
}

TEST(HTTP_Request_1_1, GET_root_multiple_captures_partial_match) {
sourcemeta::hydra::http::Request request{BASE_URL};
request.method(sourcemeta::hydra::http::Method::GET);
request.capture("content-type");
request.capture("x-foo-bar");
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_FALSE(response.header("x-foo-bar").has_value());
EXPECT_EQ(response.header("content-type").value(), "text/plain");
}

TEST(HTTP_Request_1_1, GET_root_multiple_captures_match_initializer_list) {
sourcemeta::hydra::http::Request request{BASE_URL};
request.method(sourcemeta::hydra::http::Method::GET);
request.capture({"content-type", "content-length"});
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_TRUE(response.header("content-length").has_value());
EXPECT_EQ(response.header("content-type").value(), "text/plain");
EXPECT_EQ(response.header("content-length").value(), "14");
}

TEST(HTTP_Request_1_1,
GET_root_multiple_captures_partial_match_initializer_list) {
sourcemeta::hydra::http::Request request{BASE_URL};
request.method(sourcemeta::hydra::http::Method::GET);
request.capture({"content-type", "x-foo-bar"});
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_FALSE(response.header("x-foo-bar").has_value());
EXPECT_EQ(response.header("content-type").value(), "text/plain");
}

TEST(HTTP_Request_1_1, GET_root_request_header_echo) {
sourcemeta::hydra::http::Request request{BASE_URL};
request.method(sourcemeta::hydra::http::Method::GET);
request.header("x-foo-bar", "foo");
request.capture("x-x-foo-bar");
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("x-x-foo-bar").has_value());
EXPECT_EQ(response.header("x-x-foo-bar").value(), "foo");
}

TEST(HTTP_Request_1_1, XXX) {
TEST(HTTP_Request_1_1, GET_root_request_header_mixed_case_echo) {
sourcemeta::hydra::http::Request request{BASE_URL};
request.method(sourcemeta::hydra::http::Method::GET);
request.header("X-Foo-Bar", "foo");
request.capture("x-x-foo-bar");
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("x-x-foo-bar").has_value());
EXPECT_EQ(response.header("x-x-foo-bar").value(), "foo");
}
25 changes: 11 additions & 14 deletions test/http/stub.js
Original file line number Diff line number Diff line change
@@ -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}`);
});

0 comments on commit 34668da

Please sign in to comment.