Files
curly.hpp/untests/curly_tests.cpp

811 lines
30 KiB
C++

/*******************************************************************************
* This file is part of the "https://github.com/blackmatov/curly.hpp"
* For conditions of distribution and use, see copyright notice in LICENSE.md
* Copyright (C) 2019, by Matvey Cherevko (blackmatov@gmail.com)
******************************************************************************/
#include <catch2/catch.hpp>
#include <fstream>
#include <utility>
#include <iostream>
#include <rapidjson/document.h>
namespace json = rapidjson;
#include <curly.hpp/curly.hpp>
namespace net = curly_hpp;
#include <promise.hpp/promise.hpp>
namespace netex = promise_hpp;
#include "png_data.h"
#include "jpeg_data.h"
namespace
{
json::Document json_parse(std::string_view data) {
json::Document d;
if ( d.Parse(data.data(), data.size()).HasParseError() ) {
throw std::logic_error("untests: failed to parse json");
}
return d;
}
class canceled_uploader : public net::upload_handler {
public:
canceled_uploader() = default;
std::size_t size() const override {
return 10;
}
std::size_t read(char* dst, std::size_t size) override {
(void)dst;
(void)size;
throw std::exception();
}
};
class canceled_downloader : public net::download_handler {
public:
canceled_downloader() = default;
std::size_t write(const char* src, std::size_t size) override {
(void)src;
(void)size;
throw std::exception();
}
};
netex::promise<net::content_t> download(std::string url) {
return netex::make_promise<net::content_t>([
url = std::move(url)
](auto resolve, auto reject){
net::request_builder(std::move(url))
.callback([resolve,reject](net::request request) mutable {
if ( !request.is_done() ) {
reject(net::exception("network error"));
return;
}
net::response response = request.take();
if ( response.is_http_error() ) {
reject(net::exception("server error"));
return;
}
resolve(std::move(response.content));
}).send();
});
}
}
TEST_CASE("curly") {
net::auto_performer performer;
performer.wait_activity(net::time_ms_t(10));
SECTION("wait") {
{
auto req = net::request_builder("https://httpbin.org/delay/1").send();
REQUIRE(req.status() == net::request::statuses::pending);
REQUIRE(req.wait() == net::request::statuses::done);
REQUIRE(req.status() == net::request::statuses::done);
auto resp = req.take();
REQUIRE(resp.http_code() == 200u);
REQUIRE(req.status() == net::request::statuses::empty);
}
{
auto req = net::request_builder("https://httpbin.org/delay/2").send();
REQUIRE(req.wait_for(net::time_sec_t(1)) == net::request::statuses::pending);
REQUIRE(req.wait_for(net::time_sec_t(5)) == net::request::statuses::done);
REQUIRE(req.take().http_code() == 200u);
}
{
auto req = net::request_builder("https://httpbin.org/delay/2").send();
REQUIRE(req.wait_until(net::time_point_t::clock::now() + net::time_sec_t(1))
== net::request::statuses::pending);
REQUIRE(req.wait_until(net::time_point_t::clock::now() + net::time_sec_t(5))
== net::request::statuses::done);
REQUIRE(req.take().http_code() == 200u);
}
}
SECTION("error") {
auto req = net::request_builder("|||").send();
REQUIRE(req.wait() == net::request::statuses::failed);
REQUIRE(req.status() == net::request::statuses::failed);
REQUIRE_FALSE(req.get_error().empty());
}
SECTION("cancel") {
{
auto req = net::request_builder("https://httpbin.org/delay/1").send();
REQUIRE(req.cancel());
REQUIRE(req.status() == net::request::statuses::canceled);
REQUIRE(req.get_error().empty());
}
{
auto req = net::request_builder("https://httpbin.org/status/200").send();
REQUIRE(req.wait() == net::request::statuses::done);
REQUIRE_FALSE(req.cancel());
REQUIRE(req.status() == net::request::statuses::done);
REQUIRE(req.get_error().empty());
}
}
SECTION("is_done/is_pending") {
{
auto req = net::request_builder(net::methods::get)
.url("https://httpbin.org/delay/1")
.send();
REQUIRE_FALSE(req.is_done());
REQUIRE(req.is_pending());
req.wait();
REQUIRE(req.is_done());
REQUIRE_FALSE(req.is_pending());
}
{
auto req = net::request_builder(net::methods::post, "http://www.httpbin.org/post")
.url("https://httpbin.org/delay/2")
.request_timeout(net::time_sec_t(1))
.send();
REQUIRE_FALSE(req.is_done());
REQUIRE(req.is_pending());
req.wait();
REQUIRE_FALSE(req.is_done());
REQUIRE_FALSE(req.is_pending());
REQUIRE(!req.get_error().empty());
}
}
SECTION("get") {
{
auto req = net::request_builder("https://httpbin.org/status/204").send();
auto resp = req.take();
REQUIRE(req.status() == net::request::statuses::empty);
REQUIRE(resp.http_code() == 204u);
}
{
auto req = net::request_builder("https://httpbin.org/delay/2").send();
REQUIRE(req.cancel());
REQUIRE_THROWS_AS(req.take(), net::exception);
REQUIRE(req.status() == net::request::statuses::canceled);
}
{
auto req = net::request_builder("https://httpbin.org/delay/2")
.response_timeout(net::time_sec_t(0))
.send();
REQUIRE(req.wait() == net::request::statuses::timeout);
REQUIRE_THROWS_AS(req.take(), net::exception);
REQUIRE(req.status() == net::request::statuses::timeout);
}
}
SECTION("http_methods") {
{
auto req0 = net::request_builder()
.url("https://httpbin.org/put")
.method(net::methods::put)
.send();
REQUIRE(req0.take().http_code() == 200u);
auto req1 = net::request_builder()
.url("https://httpbin.org/put")
.method(net::methods::get)
.send();
REQUIRE(req1.take().http_code() == 405u);
auto req2 = net::request_builder()
.url("https://httpbin.org/put")
.method(net::methods::head)
.send();
REQUIRE(req2.take().http_code() == 405u);
auto req3 = net::request_builder()
.url("https://httpbin.org/put")
.method(net::methods::post)
.send();
REQUIRE(req3.take().http_code() == 405u);
}
{
auto req0 = net::request_builder()
.url("https://httpbin.org/get")
.method(net::methods::put)
.send();
REQUIRE(req0.take().http_code() == 405u);
auto req1 = net::request_builder()
.url("https://httpbin.org/get")
.method(net::methods::get)
.send();
REQUIRE(req1.take().http_code() == 200u);
auto req2 = net::request_builder()
.url("https://httpbin.org/get")
.method(net::methods::head)
.send();
REQUIRE(req2.take().http_code() == 200u);
auto req3 = net::request_builder()
.url("https://httpbin.org/get")
.method(net::methods::post)
.send();
REQUIRE(req3.take().http_code() == 405u);
}
{
auto req0 = net::request_builder()
.url("https://httpbin.org/post")
.method(net::methods::put)
.send();
REQUIRE(req0.take().http_code() == 405u);
auto req1 = net::request_builder()
.url("https://httpbin.org/post")
.method(net::methods::get)
.send();
REQUIRE(req1.take().http_code() == 405u);
auto req2 = net::request_builder()
.url("https://httpbin.org/post")
.method(net::methods::head)
.send();
REQUIRE(req2.take().http_code() == 405u);
auto req3 = net::request_builder()
.url("https://httpbin.org/post")
.method(net::methods::post)
.send();
REQUIRE(req3.take().http_code() == 200u);
}
}
SECTION("status_codes") {
{
auto req = net::request_builder()
.url("https://httpbin.org/status/200")
.method(net::methods::put)
.send();
REQUIRE(req.take().http_code() == 200u);
}
{
auto req = net::request_builder()
.url("https://httpbin.org/status/201")
.method(net::methods::get)
.send();
REQUIRE(req.take().http_code() == 201u);
}
{
auto req = net::request_builder()
.url("https://httpbin.org/status/202")
.method(net::methods::head)
.send();
REQUIRE(req.take().http_code() == 202u);
}
{
auto req = net::request_builder()
.url("https://httpbin.org/status/203")
.method(net::methods::post)
.send();
REQUIRE(req.take().http_code() == 203u);
}
}
SECTION("request_inspection") {
auto req = net::request_builder()
.url("https://httpbin.org/headers")
.header("Custom-Header-1", "custom_header_value_1")
.header("Custom-Header-2", "custom header value 2")
.header("Custom-Header-3", std::string())
.send();
const auto resp = req.take();
const auto content_j = json_parse(resp.content.as_string_view());
REQUIRE(content_j["headers"]["Custom-Header-1"] == "custom_header_value_1");
REQUIRE(content_j["headers"]["Custom-Header-2"] == "custom header value 2");
REQUIRE(content_j["headers"]["Custom-Header-3"] == "");
}
SECTION("response_inspection") {
{
auto req = net::request_builder()
.url("https://httpbin.org/response-headers?hello=world&world=hello")
.method(net::methods::get)
.send();
const auto resp = req.take();
const auto content_j = json_parse(resp.content.as_string_view());
REQUIRE(content_j["hello"] == "world");
REQUIRE(content_j["world"] == "hello");
}
{
auto req = net::request_builder()
.url("https://httpbin.org/response-headers?hello=world&world=hello")
.method(net::methods::post)
.send();
const auto resp = req.take();
const auto content_j = json_parse(resp.content.as_string_copy());
REQUIRE(content_j["hello"] == "world");
REQUIRE(content_j["world"] == "hello");
}
}
SECTION("dynamic_data") {
{
auto req = net::request_builder()
.url("https://httpbin.org/base64/SFRUUEJJTiBpcyBhd2Vzb21l")
.send();
const auto resp = req.take();
REQUIRE(resp.content.as_string_view() == "HTTPBIN is awesome");
REQUIRE(req.get_error().empty());
}
{
auto req0 = net::request_builder()
.url("https://httpbin.org/delay/10")
.request_timeout(net::time_sec_t(0))
.send();
REQUIRE(req0.wait() == net::request::statuses::timeout);
REQUIRE_FALSE(req0.get_error().empty());
auto req1 = net::request_builder()
.url("https://httpbin.org/delay/10")
.response_timeout(net::time_sec_t(0))
.send();
REQUIRE(req1.wait() == net::request::statuses::timeout);
REQUIRE_FALSE(req1.get_error().empty());
}
{
auto req0 = net::request_builder()
.url("https://httpbin.org/delay/10")
.request_timeout(net::time_sec_t(1))
.send();
REQUIRE(req0.wait() == net::request::statuses::timeout);
REQUIRE_FALSE(req0.get_error().empty());
auto req1 = net::request_builder()
.url("https://httpbin.org/delay/10")
.response_timeout(net::time_sec_t(1))
.send();
REQUIRE(req1.wait() == net::request::statuses::timeout);
REQUIRE_FALSE(req1.get_error().empty());
}
}
SECTION("binary") {
{
auto resp = net::request_builder()
.url("https://httpbin.org/image/png")
.method(net::methods::get)
.send().take();
REQUIRE(resp.http_code() == 200u);
REQUIRE(resp.headers.count("Content-Type"));
REQUIRE(resp.headers.at("Content-Type") == "image/png");
REQUIRE(untests::png_data_length == resp.content.size());
REQUIRE(!std::memcmp(
std::move(resp.content).data().data(),
untests::png_data, untests::png_data_length));
}
{
auto resp = net::request_builder()
.url("https://httpbin.org/image/jpeg")
.method(net::methods::get)
.send().take();
REQUIRE(resp.http_code() == 200u);
REQUIRE(resp.headers.count("Content-Type"));
REQUIRE(resp.headers.at("Content-Type") == "image/jpeg");
REQUIRE(untests::jpeg_data_length == resp.content.size());
REQUIRE(!std::memcmp(
std::as_const(resp.content).data().data(),
untests::jpeg_data, untests::jpeg_data_length));
}
}
SECTION("redirects") {
{
{
auto req = net::request_builder()
.url("https://httpbin.org/redirect/2")
.method(net::methods::get)
.send();
REQUIRE(req.take().http_code() == 200u);
}
{
auto req = net::request_builder()
.url("https://httpbin.org/absolute-redirect/2")
.method(net::methods::get)
.send();
REQUIRE(req.take().http_code() == 200u);
}
{
auto req = net::request_builder()
.url("https://httpbin.org/relative-redirect/2")
.method(net::methods::get)
.send();
REQUIRE(req.take().http_code() == 200u);
}
}
{
{
auto req = net::request_builder()
.url("https://httpbin.org/redirect/3")
.method(net::methods::get)
.redirections(0)
.send();
REQUIRE(req.take().http_code() == 302u);
}
{
auto req = net::request_builder()
.url("https://httpbin.org/redirect/3")
.method(net::methods::get)
.redirections(1)
.send();
REQUIRE(req.wait() == net::request::statuses::failed);
}
{
auto req = net::request_builder()
.url("https://httpbin.org/redirect/3")
.method(net::methods::get)
.redirections(2)
.send();
REQUIRE(req.wait() == net::request::statuses::failed);
}
{
auto req = net::request_builder()
.url("https://httpbin.org/redirect/3")
.method(net::methods::get)
.redirections(3)
.send();
REQUIRE(req.take().http_code() == 200u);
}
}
}
SECTION("request_body") {
{
auto resp = net::request_builder()
.url("https://httpbin.org/anything")
.method(net::methods::put)
.header("Content-Type", "application/json")
.content(R"({"hello":"world"})")
.send().take();
const auto content_j = json_parse(resp.content.as_string_view());
REQUIRE(content_j["data"] == R"({"hello":"world"})");
}
{
auto resp = net::request_builder()
.url("https://httpbin.org/anything")
.method(net::methods::post)
.header("Content-Type", "application/json")
.content(R"({"hello":"world"})")
.send().take();
const auto content_j = json_parse(resp.content.as_string_view());
REQUIRE(content_j["data"] == R"({"hello":"world"})");
}
{
auto resp = net::request_builder()
.url("https://httpbin.org/anything")
.method(net::methods::post)
.header("Content-Type", "application/x-www-form-urlencoded")
.content("hello=world&world=hello")
.send().take();
const auto content_j = json_parse(resp.content.as_string_view());
REQUIRE(content_j["form"]["hello"] == "world");
REQUIRE(content_j["form"]["world"] == "hello");
}
}
SECTION("ssl_verification") {
{
auto req0 = net::request_builder("https://expired.badssl.com")
.method(net::methods::head)
.verification(true)
.send();
REQUIRE(req0.wait() == net::request::statuses::failed);
auto req1 = net::request_builder("https://wrong.host.badssl.com")
.method(net::methods::head)
.verification(true)
.send();
REQUIRE(req1.wait() == net::request::statuses::failed);
auto req2 = net::request_builder("https://self-signed.badssl.com")
.method(net::methods::head)
.verification(true)
.send();
REQUIRE(req2.wait() == net::request::statuses::failed);
auto req3 = net::request_builder("https://untrusted-root.badssl.com")
.method(net::methods::head)
.verification(true)
.send();
REQUIRE(req3.wait() == net::request::statuses::failed);
}
{
auto req0 = net::request_builder("https://expired.badssl.com")
.method(net::methods::head)
.verification(false)
.send();
REQUIRE(req0.wait() == net::request::statuses::done);
auto req1 = net::request_builder("https://wrong.host.badssl.com")
.method(net::methods::head)
.verification(false)
.send();
REQUIRE(req1.wait() == net::request::statuses::done);
auto req2 = net::request_builder("https://self-signed.badssl.com")
.method(net::methods::head)
.verification(false)
.send();
REQUIRE(req2.wait() == net::request::statuses::done);
auto req3 = net::request_builder("https://untrusted-root.badssl.com")
.method(net::methods::head)
.verification(false)
.send();
REQUIRE(req3.wait() == net::request::statuses::done);
}
}
SECTION("canceled_handlers") {
{
auto req = net::request_builder("https://httpbin.org/anything")
.verbose(true)
.method(net::methods::post)
.uploader<canceled_uploader>()
.send();
REQUIRE(req.wait() == net::request::statuses::canceled);
}
{
auto req = net::request_builder("https://httpbin.org/anything")
.verbose(true)
.method(net::methods::get)
.downloader<canceled_downloader>()
.send();
REQUIRE(req.wait() == net::request::statuses::canceled);
}
}
SECTION("callback") {
{
std::atomic_size_t call_once{0u};
auto req = net::request_builder("http://www.httpbin.org/get")
.callback([&call_once](net::request request){
std::this_thread::sleep_for(std::chrono::milliseconds(10));
++call_once;
REQUIRE(request.is_done());
REQUIRE(request.status() == net::request::statuses::done);
REQUIRE(request.take().http_code() == 200u);
}).send();
REQUIRE(req.wait_callback() == net::request::statuses::empty);
REQUIRE_FALSE(req.get_callback_exception());
REQUIRE(call_once.load() == 1u);
}
{
std::atomic_size_t call_once{0u};
auto req = net::request_builder("|||")
.callback([&call_once](net::request request){
std::this_thread::sleep_for(std::chrono::milliseconds(10));
++call_once;
REQUIRE_FALSE(request.is_done());
REQUIRE(request.status() == net::request::statuses::failed);
REQUIRE_FALSE(request.get_error().empty());
}).send();
REQUIRE(req.wait_callback() == net::request::statuses::failed);
REQUIRE_FALSE(req.get_callback_exception());
REQUIRE(call_once.load() == 1u);
}
{
std::atomic_size_t call_once{0u};
auto req = net::request_builder("http://www.httpbin.org/delay/2")
.response_timeout(net::time_sec_t(0))
.callback([&call_once](net::request request){
std::this_thread::sleep_for(std::chrono::milliseconds(10));
++call_once;
REQUIRE_FALSE(request.is_done());
REQUIRE(request.status() == net::request::statuses::timeout);
REQUIRE_FALSE(request.get_error().empty());
}).send();
REQUIRE(req.wait_callback() == net::request::statuses::timeout);
REQUIRE_FALSE(req.get_callback_exception());
REQUIRE(call_once.load() == 1u);
}
{
std::atomic_size_t call_once{0u};
auto req = net::request_builder("http://www.httpbin.org/delay/2")
.callback([&call_once](net::request request){
std::this_thread::sleep_for(std::chrono::milliseconds(10));
++call_once;
REQUIRE_FALSE(request.is_done());
REQUIRE(request.status() == net::request::statuses::canceled);
REQUIRE(request.get_error().empty());
}).send();
REQUIRE(req.cancel());
REQUIRE(req.wait_callback() == net::request::statuses::canceled);
REQUIRE_FALSE(req.get_callback_exception());
REQUIRE(call_once.load() == 1u);
}
}
SECTION("callback_exception") {
auto req = net::request_builder("http://www.httpbin.org/post")
.callback([](net::request request){
std::this_thread::sleep_for(std::chrono::milliseconds(10));
if ( request.take().is_http_error() ) {
throw std::logic_error("my_logic_error");
}
}).send();
REQUIRE(req.wait_callback() == net::request::statuses::empty);
REQUIRE(req.get_callback_exception());
try {
std::rethrow_exception(req.get_callback_exception());
} catch (const std::logic_error& e) {
REQUIRE(std::string_view("my_logic_error") == e.what());
}
}
}
TEST_CASE("curly_examples") {
net::auto_performer performer;
SECTION("Get Requests") {
// makes a GET request and async send it
auto request = net::request_builder()
.method(net::methods::get)
.url("http://www.httpbin.org/get")
.send();
// synchronous waits and take a response
auto response = request.take();
// prints results
std::cout << "Status code: " << response.http_code() << std::endl;
std::cout << "Content type: " << response.headers["content-type"] << std::endl;
std::cout << "Body content: " << response.content.as_string_view() << std::endl;
// Status code: 200
// Content type: application/json
// Body content: {
// "args": {},
// "headers": {
// "Accept": "*/*",
// "Host": "www.httpbin.org",
// "User-Agent": "cURL/7.54.0"
// },
// "origin": "37.195.66.134, 37.195.66.134",
// "url": "https://www.httpbin.org/get"
// }
}
SECTION("Post Requests") {
auto request = net::request_builder()
.method(net::methods::post)
.url("http://www.httpbin.org/post")
.header("Content-Type", "application/json")
.content(R"({"hello" : "world"})")
.send();
auto response = request.take();
std::cout << "Body content: " << response.content.as_string_view() << std::endl;
std::cout << "Content Length: " << response.headers["content-length"] << std::endl;
// Body content: {
// "args": {},
// "data": "{\"hello\" : \"world\"}",
// "files": {},
// "form": {},
// "headers": {
// "Accept": "*/*",
// "Content-Length": "19",
// "Content-Type": "application/json",
// "Host": "www.httpbin.org",
// "User-Agent": "cURL/7.54.0"
// },
// "json": {
// "hello": "world"
// },
// "origin": "37.195.66.134, 37.195.66.134",
// "url": "https://www.httpbin.org/post"
// }
// Content Length: 389
}
SECTION("Error Handling") {
auto request = net::request_builder()
.url("http://unavailable.site.com")
.send();
request.wait();
if ( request.is_done() ) {
auto response = request.take();
std::cout << "Status code: " << response.http_code() << std::endl;
} else {
// throws net::exception because a response is unavailable
// auto response = request.take();
std::cout << "Error message: " << request.get_error() << std::endl;
}
// Error message: Couldn't resolve host name
}
SECTION("Request Callbacks") {
auto request = net::request_builder("http://www.httpbin.org/get")
.callback([](net::request request){
if ( request.is_done() ) {
auto response = request.take();
std::cout << "Status code: " << response.http_code() << std::endl;
} else {
std::cout << "Error message: " << request.get_error() << std::endl;
}
}).send();
request.wait_callback();
// Status code: 200
}
SECTION("Streamed Requests") {
{
class file_dowloader : public net::download_handler {
public:
file_dowloader(const char* filename)
: stream_(filename, std::ofstream::binary) {}
std::size_t write(const char* src, std::size_t size) override {
stream_.write(src, size);
return size;
}
private:
std::ofstream stream_;
};
net::request_builder()
.url("https://httpbin.org/image/jpeg")
.downloader<file_dowloader>("image.jpeg")
.send().take();
}
{
class file_uploader : public net::upload_handler {
public:
file_uploader(const char* filename)
: stream_(filename, std::ifstream::binary) {
stream_.seekg(0, std::ios::end);
size_ = static_cast<std::size_t>(stream_.tellg());
stream_.seekg(0, std::ios::beg);
}
std::size_t size() const override {
return size_;
}
std::size_t read(char* dst, std::size_t size) override {
stream_.read(dst, size);
return size;
}
private:
std::size_t size_{0u};
std::ifstream stream_;
};
net::request_builder()
.method(net::methods::post)
.url("https://httpbin.org/anything")
.uploader<file_uploader>("image.jpeg")
.send().take();
}
}
SECTION("Promised Requests") {
auto promise = download("https://httpbin.org/image/png")
.then([](const net::content_t& content){
std::cout << content.size() << " bytes downloaded" << std::endl;
}).except([](std::exception_ptr e){
try {
std::rethrow_exception(e);
} catch (const std::exception& ee) {
std::cerr << "Failed to download: " << ee.what() << std::endl;
}
});
promise.wait();
// 8090 bytes downloaded
}
}