Adding http request (#41)

* adding gtest framework

* http request
This commit is contained in:
Victor Vazquez 2020-03-26 11:38:07 -07:00 committed by GitHub
parent 7a02ab023b
commit 9f1493af27
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 679 additions and 76 deletions

5
.gitignore vendored
View File

@ -332,7 +332,10 @@ ASALocalRun/
# CMake
build/
/cmake-build-debug
.vscode/settings.json
#Doxygen
[Dd]ocs/
# vscode
.vscode/
.factorypath

View File

@ -7,6 +7,7 @@ list(APPEND CMAKE_MODULE_PATH "${CMAKE_SOURCE_DIR}/cmake-modules")
# Compile Options
option(WARNINGS_AS_ERRORS "Treat compiler warnings as errors" ON)
option(BUILD_CURL_TRANSPORT "Build internal http transport implementation with CURL for HTTP Pipeline" OFF)
option(BUILD_TESTING "Build test cases" OFF)
# VCPKG Integration
if(DEFINED ENV{VCPKG_ROOT} AND NOT DEFINED CMAKE_TOOLCHAIN_FILE)
@ -22,10 +23,16 @@ endif()
# Project definition
project(az LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 11)
set(CMAKE_CXX_STANDARD 14)
set(CMAKE_CXX_STANDARD_REQUIRED True)
# compiler warning flags
if(BUILD_TESTING)
# tests
enable_testing ()
add_subdirectory(sdk/core/azure-core/test)
endif()
# compiler warning flags globally
include(global_compile_options)
# sub-projects

View File

@ -1,16 +1,16 @@
{
"configurations": [
{
"name": "x64-Debug",
"generator": "Ninja",
"configurationType": "Debug",
"inheritEnvironments": [ "msvc_x64_x64" ],
"buildRoot": "${projectDir}\\out\\build\\${name}",
"installRoot": "${projectDir}\\out\\install\\${name}",
"cmakeCommandArgs": "",
"buildCommandArgs": "-v",
"ctestCommandArgs": "",
"variables": []
}
]
{
"configurations": [
{
"name": "x64-Debug",
"generator": "Ninja",
"configurationType": "Debug",
"inheritEnvironments": [ "msvc_x64_x64" ],
"buildRoot": "${projectDir}\\out\\build\\${name}",
"installRoot": "${projectDir}\\out\\install\\${name}",
"cmakeCommandArgs": "-DINSTALL_GTEST=OFF -DBUILD_TESTING=ON",
"buildCommandArgs": "-v",
"ctestCommandArgs": "",
"variables": []
}
]
}

View File

@ -0,0 +1,98 @@
#
#
# Downloads GTest and provides a helper macro to add tests. Add make check, as well, which
# gives output on failed tests without having to set an environment variable.
#
#
set(gtest_force_shared_crt ON CACHE BOOL "" FORCE)
if(CMAKE_VERSION VERSION_LESS 3.11)
set(UPDATE_DISCONNECTED_IF_AVAILABLE "UPDATE_DISCONNECTED 1")
include(DownloadProject)
download_project(PROJ googletest
GIT_REPOSITORY https://github.com/google/googletest.git
GIT_TAG master
UPDATE_DISCONNECTED 1
QUIET
)
# CMake warning suppression will not be needed in version 1.9
set(CMAKE_SUPPRESS_DEVELOPER_WARNINGS 1 CACHE BOOL "")
add_subdirectory(${googletest_SOURCE_DIR} ${googletest_SOURCE_DIR} EXCLUDE_FROM_ALL)
unset(CMAKE_SUPPRESS_DEVELOPER_WARNINGS)
else()
include(FetchContent)
FetchContent_Declare(googletest
GIT_REPOSITORY https://github.com/google/googletest.git
GIT_TAG master)
FetchContent_GetProperties(googletest)
if(NOT googletest_POPULATED)
FetchContent_Populate(googletest)
set(CMAKE_SUPPRESS_DEVELOPER_WARNINGS 1 CACHE BOOL "")
add_subdirectory(${googletest_SOURCE_DIR} ${googletest_BINARY_DIR} EXCLUDE_FROM_ALL)
unset(CMAKE_SUPPRESS_DEVELOPER_WARNINGS)
endif()
endif()
if(CMAKE_CONFIGURATION_TYPES)
add_custom_target(check COMMAND ${CMAKE_CTEST_COMMAND}
--force-new-ctest-process --output-on-failure
--build-config "$<CONFIGURATION>")
else()
add_custom_target(check COMMAND ${CMAKE_CTEST_COMMAND}
--force-new-ctest-process --output-on-failure)
endif()
set_target_properties(check PROPERTIES FOLDER "Scripts")
#include_directories(${gtest_SOURCE_DIR}/include)
# More modern way to do the last line, less messy but needs newish CMake:
#target_include_directories(gtest INTERFACE ${gtest_SOURCE_DIR}/include)
if(GOOGLE_TEST_INDIVIDUAL)
if(NOT CMAKE_VERSION VERSION_LESS 3.9)
include(GoogleTest)
else()
set(GOOGLE_TEST_INDIVIDUAL OFF)
endif()
endif()
# Target must already exist
macro(add_gtest TESTNAME)
target_link_libraries(${TESTNAME} PUBLIC gtest gmock gtest_main)
if(GOOGLE_TEST_INDIVIDUAL)
if(CMAKE_VERSION VERSION_LESS 3.10)
gtest_add_tests(TARGET ${TESTNAME}
TEST_PREFIX "${TESTNAME}."
TEST_LIST TmpTestList)
set_tests_properties(${TmpTestList} PROPERTIES FOLDER "Tests")
else()
gtest_discover_tests(${TESTNAME}
TEST_PREFIX "${TESTNAME}."
PROPERTIES FOLDER "Tests")
endif()
else()
add_test(${TESTNAME} ${TESTNAME})
set_target_properties(${TESTNAME} PROPERTIES FOLDER "Tests")
endif()
endmacro()
mark_as_advanced(
gmock_build_tests
gtest_build_samples
gtest_build_tests
gtest_disable_pthreads
gtest_force_shared_crt
gtest_hide_internal_symbols
BUILD_GMOCK
BUILD_GTEST
)
set_target_properties(gtest gtest_main gmock gmock_main
PROPERTIES FOLDER "Extern")

View File

@ -3,7 +3,8 @@ if(MSVC)
set(WARNINGS_AS_ERRORS_FLAG "/WX")
endif()
add_compile_options(/Wall ${WARNINGS_AS_ERRORS_FLAG} /wd5031 /wd4668 /wd4820 /wd4255 /wd4710 /analyze)
#https://stackoverflow.com/questions/37527946/warning-unreferenced-inline-function-has-been-removed
add_compile_options(/W4 ${WARNINGS_AS_ERRORS_FLAG} /wd5031 /wd4668 /wd4820 /wd4255 /wd4710 /analyze)
elseif(CMAKE_C_COMPILER_ID MATCHES "Clang")
if(WARNINGS_AS_ERRORS)
set(WARNINGS_AS_ERRORS_FLAG "-Werror")

View File

@ -28,6 +28,31 @@ jobs:
vcpkg.deps: ''
VCPKG_DEFAULT_TRIPLET: 'x64-osx'
# Unit testing ON
Linux_x64_with_unit_test:
vm.image: 'ubuntu-18.04'
vcpkg.deps: ''
VCPKG_DEFAULT_TRIPLET: 'x64-linux'
build.args: ' -DBUILD_TESTING=ON'
Win_x86_with_unit_test:
vm.image: 'windows-2019'
vcpkg.deps: ''
VCPKG_DEFAULT_TRIPLET: 'x86-windows-static'
CMAKE_GENERATOR: 'Visual Studio 16 2019'
CMAKE_GENERATOR_PLATFORM: Win32
build.args: ' -DBUILD_TESTING=ON'
Win_x64_with_unit_test:
vm.image: 'windows-2019'
vcpkg.deps: ''
VCPKG_DEFAULT_TRIPLET: 'x64-windows-static'
CMAKE_GENERATOR: 'Visual Studio 16 2019'
CMAKE_GENERATOR_PLATFORM: x64
build.args: ' -DBUILD_TESTING=ON'
MacOS_x64_with_unit_test:
vm.image: 'macOS-10.14'
vcpkg.deps: ''
VCPKG_DEFAULT_TRIPLET: 'x64-osx'
build.args: ' -DBUILD_TESTING=ON'
pool:
vmImage: $(vm.image)
variables:

View File

@ -4,17 +4,15 @@
cmake_minimum_required (VERSION 3.12)
project (azure-core LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 11)
set(CMAKE_CXX_STANDARD 14)
set(CMAKE_CXX_STANDARD_REQUIRED True)
add_library (
azure-core
src/http/http_request
src/http/http
src/http/request
)
# compiler warning flags
include(global_compile_options)
target_include_directories (azure-core PUBLIC $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/inc> $<INSTALL_INTERFACE:include/az_core>)
# make sure that users can consume the project as a library.

View File

@ -3,6 +3,10 @@
#pragma once
#include <algorithm>
#include <map>
#include <string>
#include <internal/contract.hpp>
namespace azure
@ -12,18 +16,52 @@ namespace core
namespace http
{
class http_request
// BodyStream is used to read data to/from a service
class BodyStream
{
private:
/* data */
public:
http_request();
~http_request();
static BodyStream* null;
// Returns the length of the data; used with the HTTP Content-Length header
virtual uint64_t Length() = 0;
// Resets the stream back to the beginning (for retries)
// Derived classes that send data in an HTTP request MUST override this and implement it properly.
virtual void Rewind()
{
throw "Not Implemented"; // TODO: Replace with best practice as defined by guideline
};
// Reads more data; EOF if return < count; throws if error/canceled
virtual uint64_t Read(/*Context& context, */ uint8_t* buffer, uint64_t offset, uint64_t count)
= 0;
// Closes the stream; typically called after all data read or if an error occurs.
virtual void Close() = 0;
};
http_request::http_request() {}
http_request::~http_request() {}
class BodyBuffer
{
public:
static BodyBuffer* null;
uint8_t const* _bodyBuffer;
uint64_t _bodyBufferSize;
BodyBuffer(uint8_t const* bodyBuffer, uint64_t bodyBufferSize)
: _bodyBuffer(bodyBuffer), _bodyBufferSize(bodyBufferSize)
{
}
};
enum class HttpMethod
{
GET,
HEAD,
POST,
PUT,
DELETE,
PATCH,
};
} // namespace http
} // namespace core

View File

@ -0,0 +1,147 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// SPDX-License-Identifier: MIT
#pragma once
#include <algorithm>
#include <iostream>
#include <map>
#include <sstream>
#include <string>
#include <vector>
#include <internal/contract.hpp>
namespace azure
{
namespace core
{
namespace http
{
class Request
{
private:
Request(Request const&) = delete;
void operator=(Request const&) = delete;
// query needs to be first or at least before url, since url might update it
std::map<std::string, std::string> _queryParameters;
HttpMethod _method;
std::string _url;
std::map<std::string, std::string> _headers;
std::map<std::string, std::string> _retryHeaders;
std::map<std::string, std::string> _retryQueryParameters;
// Request can contain no body, or either of next bodies (_bodyBuffer plus size or bodyStream)
BodyStream* _bodyStream;
BodyBuffer* _bodyBuffer;
// flag to know where to insert header
bool _retryModeEnabled;
// returns left map plus all items in right
// when duplicates, left items are preferred
static std::map<std::string, std::string> mergeMaps(
std::map<std::string, std::string> const& left,
std::map<std::string, std::string> const& right)
{
auto result = std::map<std::string, std::string>(left);
std::for_each(right.begin(), right.end(), [&](auto const& pair) { result.insert(pair); });
return result;
}
/**
* Create an stream from source string. Then use getline and separator to get as many tokens as
* possible and insert each one to result vector
*/
static std::vector<std::string> split(std::string source, char separator)
{
auto result = std::vector<std::string>();
auto stringAsStream = std::stringstream(source);
auto token = std::string();
while (std::getline(stringAsStream, token, separator))
{
result.push_back(token);
}
return result;
}
/**
* Will check if there are any query parameter in url looking for symbol '?'
* If it is found, it will insert query parameters to _queryParameters internal field
* and remove it from url
*/
std::string parseUrl(std::string url)
{
auto position = url.find('?');
if (position == std::string::npos)
{
return url; // no query parameters. Nothing else to do
}
// Get query parameters string and update
auto queryParameters = url.substr(position + 1);
url = url.substr(0, position);
// Split all query parameters (should be separated by &)
auto queryParametersVector = split(queryParameters, '&');
// insert each query parameter to internal field
std::for_each(queryParametersVector.begin(), queryParametersVector.end(), [&](auto query) {
auto parameter = split(query, '=');
// Will throw if parameter in query is not valid (i.e. arg:1)
_queryParameters.insert(std::pair<std::string, std::string>(parameter[0], parameter[1]));
});
return url;
}
Request(
HttpMethod httpMethod,
std::string const& url,
BodyStream* bodyStream,
BodyBuffer* bodyBuffer)
: _method(std::move(httpMethod)), _url(parseUrl(std::move(url))), _bodyStream(bodyStream),
_bodyBuffer(bodyBuffer), _retryModeEnabled(false)
{
// TODO: parse url
}
public:
Request(HttpMethod httpMethod, std::string const& url)
: Request(httpMethod, url, BodyStream::null, BodyBuffer::null)
{
}
Request(HttpMethod httpMethod, std::string const& url, BodyBuffer* bodyBuffer)
: Request(httpMethod, url, BodyStream::null, bodyBuffer)
{
}
Request(HttpMethod httpMethod, std::string const& url, BodyStream* bodyStream)
: Request(httpMethod, url, bodyStream, BodyBuffer::null)
{
}
// Methods used to build HTTP request
void addPath(std::string const& path);
void addQueryParameter(std::string const& name, std::string const& value);
void addHeader(std::string const& name, std::string const& value);
void startRetry(); // only called by retry policy
// Methods used by transport layer (and logger) to send request
HttpMethod getMethod();
std::string getEncodedUrl(); // should return encoded url
std::map<std::string, std::string> getHeaders();
BodyStream* getBodyStream();
BodyBuffer* getBodyBuffer();
};
} // namespace http
} // namespace core
} // namespace azure

View File

@ -0,0 +1,65 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// SPDX-License-Identifier: MIT
#pragma once
#include <algorithm>
#include <map>
#include <string>
#include <internal/contract.hpp>
namespace azure
{
namespace core
{
namespace http
{
class Response
{
private:
Response(Response const&) = delete;
void operator=(Response const&) = delete;
uint16_t _statusCode;
std::string _reasonPhrase;
std::map<std::string, std::string> _headers;
// Response can contain no body, or either of next bodies (_bodyBuffer plus size or bodyStream)
BodyBuffer& _bodyBuffer;
BodyStream& _bodyStream;
Response(
uint16_t statusCode,
std::string reasonPhrase,
BodyBuffer& const bodyBuffer,
BodyStream& const BodyStream)
: _statusCode(statusCode), _reasonPhrase(reasonPhrase), _bodyBuffer(bodyBuffer),
_bodyStream(BodyStream)
{
}
public:
Response(uint16_t statusCode, std::string reasonPhrase)
: Response(statusCode, reasonPhrase, BodyBuffer::null, BodyStream::null)
{
}
// Methods used to build HTTP response
void addHeader(std::string const& name, std::string const& value);
void setBody(BodyBuffer& const bodyBuffer);
void setBody(BodyStream& const bodyStream);
// Methods used by transport layer (and logger) to send response
uint16_t const& getStatusCode();
std::string const& getReasonPhrase();
std::map<std::string, std::string> const& getHeaders();
http::BodyStream& getBodyStream();
http::BodyBuffer& getBodyBuffer();
};
} // namespace http
} // namespace core
} // namespace azure

View File

@ -1,28 +0,0 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// SPDX-License-Identifier: MIT
#include <contract.hpp>
namespace azure
{
namespace core
{
namespace http
{
class az_http_request
{
private:
/* data */
public:
az_http_request();
~az_http_request();
};
az_http_request::az_http_request() {}
az_http_request::~az_http_request() {}
} // namespace http
} // namespace core
} // namespace azure

View File

@ -3,13 +3,7 @@
#include <http/http.hpp>
namespace azure
{
namespace core
{
namespace http
{
using namespace azure::core::http;
} // namespace http
} // namespace core
} // namespace azure
BodyStream* BodyStream::null = nullptr;
BodyBuffer* BodyBuffer::null = nullptr;

View File

@ -0,0 +1,77 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// SPDX-License-Identifier: MIT
#include <map>
#include <string>
#include <vector>
#include <http/http.hpp>
#include <http/request.hpp>
using namespace azure::core::http;
void Request::addPath(std::string const& path) { this->_url += "/" + path; }
void Request::addQueryParameter(std::string const& name, std::string const& value)
{
if (this->_retryModeEnabled)
{
// When retry mode is ON, any new value must override previous
this->_retryQueryParameters[name] = value;
}
else
{
this->_queryParameters.insert(std::pair<std::string, std::string>(name, value));
}
}
void Request::addHeader(std::string const& name, std::string const& value)
{
if (this->_retryModeEnabled)
{
// When retry mode is ON, any new value must override previous
this->_retryHeaders[name] = value;
}
else
{
this->_headers.insert(std::pair<std::string, std::string>(name, value));
}
}
void Request::startRetry()
{
this->_retryModeEnabled = true;
this->_retryHeaders.clear();
}
HttpMethod Request::getMethod() { return this->_method; }
std::string Request::getEncodedUrl()
{
if (this->_queryParameters.size() == 0 && this->_retryQueryParameters.size() == 0)
{
return _url; // no query parameters to add
}
// remove query duplicates
auto queryParameters = Request::mergeMaps(this->_retryQueryParameters, this->_queryParameters);
// build url
auto queryString = std::string("");
for (auto pair : queryParameters)
{
queryString += (queryString.empty() ? "?" : "&") + pair.first + "=" + pair.second;
}
return _url + queryString;
}
std::map<std::string, std::string> Request::getHeaders()
{
// create map with retry headers witch are the most important and we don't want
// to override them with any duplicate header
return Request::mergeMaps(this->_retryHeaders, this->_headers);
}
BodyStream* Request::getBodyStream() { return _bodyStream; }
BodyBuffer* Request::getBodyBuffer() { return _bodyBuffer; }

View File

@ -0,0 +1,24 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// SPDX-License-Identifier: MIT
#include <map>
#include <string>
#include <vector>
#include <http/http.hpp>
#include <http/response.hpp>
using namespace azure::core::http;
BodyStream& BodyStream::null = *(BodyStream*)NULL;
BodyBuffer& BodyBuffer::null = *(BodyBuffer*)NULL;
uint16_t const& Response::getStatusCode() { return _statusCode; }
std::string const& Response::getReasonPhrase() { return _reasonPhrase; }
std::map<std::string, std::string> const& Response::getHeaders() { return this->_headers; }
BodyStream& Response::getBodyStream() { return _bodyStream; }
BodyBuffer& Response::getBodyBuffer() { return _bodyBuffer; }

View File

@ -0,0 +1,19 @@
# Copyright (c) Microsoft Corporation. All rights reserved.
# SPDX-License-Identifier: MIT
cmake_minimum_required (VERSION 3.12)
include(AddGoogleTest)
project (azure-core-test LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 14)
set(CMAKE_CXX_STANDARD_REQUIRED True)
add_executable (
azure-core-test
main.cpp
)
target_link_libraries(azure-core-test PRIVATE azure-core)
add_gtest(azure-core-test)

View File

@ -1,16 +1,151 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// SPDX-License-Identifier: MIT
#include <azure.hpp>
#include <http/http.hpp>
#include <http/request.hpp>
#include <stdlib.h>
#include "gtest/gtest.h"
namespace azure
#include <string>
#include <vector>
using namespace azure::core;
TEST(Http_Request, getters)
{
namespace core
http::HttpMethod httpMethod = http::HttpMethod::GET;
std::string url = "http://test.url.com";
http::Request req(httpMethod, url);
// EXPECT_PRED works better than just EQ because it will print values in log
EXPECT_PRED2(
[](http::HttpMethod a, http::HttpMethod b) { return a == b; }, req.getMethod(), httpMethod);
EXPECT_PRED2([](std::string a, std::string b) { return a == b; }, req.getEncodedUrl(), url);
/* EXPECT_PRED2(
[](std::string a, std::string b) { return a == b; },
req.getBodyStream(),
http::BodyStream::null);
EXPECT_PRED2(
[](std::string a, std::string b) { return a == b; },
req.getBodyBuffer(),
http::BodyBuffer::null); */
uint8_t buffer[] = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };
auto bufferBody = http::BodyBuffer(buffer, sizeof(buffer));
http::Request requestWithBody(httpMethod, url, &bufferBody);
EXPECT_PRED2(
[](http::HttpMethod a, http::HttpMethod b) { return a == b; },
requestWithBody.getMethod(),
httpMethod);
EXPECT_PRED2(
[](std::string a, std::string b) { return a == b; }, requestWithBody.getEncodedUrl(), url);
/* EXPECT_PRED2(
[](std::string a, std::string b) { return a == b; },
requestWithBody.getBodyStream(),
http::BodyStream::null); */
// body with buffer
auto body = requestWithBody.getBodyBuffer();
ASSERT_EQ(body->_bodyBufferSize, 10);
for (auto i = 0; i < 10; i++)
{
ASSERT_EQ(body->_bodyBuffer[i], i);
}
EXPECT_NO_THROW(req.addHeader("name", "value"));
EXPECT_NO_THROW(req.addHeader("name2", "value2"));
auto headers = req.getHeaders();
EXPECT_TRUE(headers.count("name"));
EXPECT_TRUE(headers.count("name2"));
EXPECT_FALSE(headers.count("newHeader"));
auto value = headers.find("name");
EXPECT_PRED2([](std::string a, std::string b) { return a == b; }, value->second, "value");
auto value2 = headers.find("name2");
EXPECT_PRED2([](std::string a, std::string b) { return a == b; }, value2->second, "value2");
// now add to retry headers
req.startRetry();
// same headers first, then one new
EXPECT_NO_THROW(req.addHeader("name", "retryValue"));
EXPECT_NO_THROW(req.addHeader("name2", "retryValue2"));
EXPECT_NO_THROW(req.addHeader("newHeader", "new"));
headers = req.getHeaders();
EXPECT_TRUE(headers.count("name"));
EXPECT_TRUE(headers.count("name2"));
EXPECT_TRUE(headers.count("newHeader"));
value = headers.find("name");
EXPECT_PRED2([](std::string a, std::string b) { return a == b; }, value->second, "retryValue");
value2 = headers.find("name2");
EXPECT_PRED2([](std::string a, std::string b) { return a == b; }, value2->second, "retryValue2");
auto value3 = headers.find("newHeader");
EXPECT_PRED2([](std::string a, std::string b) { return a == b; }, value3->second, "new");
}
TEST(Http_Request, query_parameter)
{
http::HttpMethod httpMethod = http::HttpMethod::PUT;
std::string url = "http://test.com";
http::Request req(httpMethod, url);
int main() { return 0; }
EXPECT_NO_THROW(req.addQueryParameter("query", "value"));
EXPECT_PRED2(
[](std::string a, std::string b) { return a == b; },
req.getEncodedUrl(),
url + "?query=value");
} // namespace core
} // namespace azure
std::string url_with_query = "http://test.com?query=1";
http::Request req_with_query(httpMethod, url_with_query);
// ignore if adding same query parameter key that is already in url
EXPECT_NO_THROW(req_with_query.addQueryParameter("query", "value"));
EXPECT_PRED2(
[](std::string a, std::string b) { return a == b; },
req_with_query.getEncodedUrl(),
url_with_query);
// retry query params testing
req.startRetry();
// same query parameter should override previous
EXPECT_NO_THROW(req.addQueryParameter("query", "retryValue"));
EXPECT_PRED2(
[](std::string a, std::string b) { return a == b; },
req.getEncodedUrl(),
url + "?query=retryValue");
}
TEST(Http_Request, add_path)
{
http::HttpMethod httpMethod = http::HttpMethod::POST;
std::string url = "http://test.com";
http::Request req(httpMethod, url);
EXPECT_NO_THROW(req.addPath("path"));
EXPECT_PRED2(
[](std::string a, std::string b) { return a == b; }, req.getEncodedUrl(), url + "/path");
EXPECT_NO_THROW(req.addQueryParameter("query", "value"));
EXPECT_PRED2(
[](std::string a, std::string b) { return a == b; },
req.getEncodedUrl(),
url + "/path?query=value");
EXPECT_NO_THROW(req.addPath("path2"));
EXPECT_PRED2(
[](std::string a, std::string b) { return a == b; },
req.getEncodedUrl(),
url + "/path/path2?query=value");
EXPECT_NO_THROW(req.addPath("path3"));
EXPECT_PRED2(
[](std::string a, std::string b) { return a == b; },
req.getEncodedUrl(),
url + "/path/path2/path3?query=value");
}