Browse Source

init: add inital work

master
Jørgen Lien Sellæg 10 years ago
parent
commit
9f588ecab7
  1. 65
      toREST/CMakeLists.txt
  2. 13
      toREST/cmake_modules/FindLibTorrent.cmake
  3. 92
      toREST/include/http.hpp
  4. 27
      toREST/include/resource.hpp
  5. 250
      toREST/include/session.hpp
  6. 94
      toREST/include/util.hpp
  7. 83
      toREST/src/main.cxx
  8. 120
      toREST/tests/http_test.cpp
  9. 118
      toREST/tests/session_test.cpp
  10. 3
      toREST/tests/test.cpp

65
toREST/CMakeLists.txt

@ -0,0 +1,65 @@
cmake_minimum_required (VERSION 3.0.2)
set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} "${CMAKE_SOURCE_DIR}/share/cmake_modules/")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++14")
set(project_name toREST)
project (${project_name})
message(${CMAKE_MODULE_PATH})
find_package(Boost COMPONENTS regex system thread coroutine context filesystem date_time REQUIRED)
find_package(LibTorrent REQUIRED)
find_package(OpenSSL REQUIRED)
set(BT_INCLUDE_DIR ./include)
set(LIB_INCLUDE_DIR ./lib)
file(GLOB source_files "./src/*.cpp")
include_directories(
${Boost_INCLUDE_DIRS}
${OPENSSL_INCLUDE_DIR}
${LIBTORRENT_INCLUDE_DIR}
${BT_INCLUDE_DIR}
${LIB_INCLUDE_DIR}
)
set(global_libraries
${Boost_LIBRARIES}
${CMAKE_THREAD_LIBS_INIT}
${LIBTORRENT_LIBRARY}
${OPENSSL_CRYPTO_LIBRARY}
)
add_library(project_shared OBJECT ${source_files})
add_executable(${project_name} ./src/main.cxx $<TARGET_OBJECTS:project_shared>)
target_link_libraries(${project_name} ${global_libraries})
enable_testing()
file(GLOB test_files "./tests/*.cpp")
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -O0 -Wall -fprofile-arcs -ftest-coverage ")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -O0 -Wall -fprofile-arcs -ftest-coverage ")
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_CXX_FLAGS} -fprofile-arcs -ftest-coverage ")
set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_CXX_FLAGS} -fprofile-arcs -ftest-coverage ")
set(test yea)
add_executable(${test} ${test_files} $<TARGET_OBJECTS:project_shared>)
target_include_directories(${test} PUBLIC ../lib/Catch)
target_link_libraries(${test} ${global_libraries})
add_test(${test} ${test})
find_package(Doxygen)
if(DOXYGEN_FOUND)
configure_file(${CMAKE_CURRENT_SOURCE_DIR}/docs/Doxyfile.in ${CMAKE_CURRENT_BINARY_DIR}/Doxyfile @ONLY)
add_custom_target(doc
${DOXYGEN_EXECUTABLE} ${CMAKE_CURRENT_BINARY_DIR}/Doxyfile
WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/../include
COMMENT "Generating API documentation with Doxygen to ${CMAKE_CURRENT_BINARY_DIR}" VERBATIM
)
endif(DOXYGEN_FOUND)

13
toREST/cmake_modules/FindLibTorrent.cmake

@ -1,13 +0,0 @@
# Find libtorrent-rasterbar
find_path(LIBTORRENT_INCLUDE_DIR libtorrent /usr/include /usr/local/include)
find_library(LIBTORRENT_LIBRARY torrent-rasterbar /usr/lib /usr/local/lib)
if(LIBTORRENT_INCLUDE_DIR AND LIBTORRENT_LIBRARY)
set(LIBTORRENT_FOUND TRUE)
endif(LIBTORRENT_INCLUDE_DIR AND LIBTORRENT_LIBRARY)
if(LIBTORRENT_FOUND)
message(STATUS "Found libtorrent: ${LIBTORRENT_LIBRARY}")
else(LIBTORRENT_FOUND)
message(FATAL_ERROR "libtorrent not found!")
endif(LIBTORRENT_FOUND)

92
toREST/include/http.hpp

@ -0,0 +1,92 @@
#ifndef _TOREST_HTTP_HPP_
#define _TOREST_HTTP_HPP_
#include <json.hpp>
#include <unordered_map>
class http {
public:
/// http codes
enum status {
/*! Standard response for successful HTTP requests. */ ok=200,
/*! The request has been fulfilled, resulting in the creation of a new resource. */ created=201,
/*! The request has been accepted for processing, but the processing has not been completed. */ accepted=202,
/*! The server successfully processed the request and is not returning any content. */ no_content=204,
/*! The server cannot or will not process the request due to an apparent client error */ bad_request=400,
/*! Similar to 403 Forbidden, but specifically for use when authentication is required and has failed or has not yet been provided. */ unauthorized=401,
/*! Reserved for future use. The */ payment_required=402,
/*! the request was a valid request, but the server is refusing to respond to it. */ forbidden=403,
/*! The requested resource could not be found but may be available in the future. */ not_found=404,
/*! Allowed A request method is not supported for the requested resource */ method_not_allowed=405,
/*! The resource identified by the request is only capable of generating response entities which have content characteristics not acceptable according to the accept headers sent in the request. */ not_acceptable=406,
/*! A generic error message, given when an unexpected condition was encountered and no more specific message is suitable. */ internal_server_error=500,
/*! The server either does not recognize the request method, or it lacks the ability to fulfill the request. */ not_implemented=501,
/*! The server was acting as a gateway or proxy and received an invalid response from the upstream server. */ bad_gateway=502,
/*! The server is currently unavailable (because it is overloaded or down for maintenance). Generally, this is a temporary state. */ service_unavailable=503,
/*! Gateway timed out */ gateway_timeout=504,
/*! http-version not supported */ http_version_not_supported=505
};
typedef std::pair<status,std::string> code;
typedef std::pair<std::string,std::string> header;
typedef std::unordered_map<std::string,std::string> headers;
/// creates a http::code object out of a http::status code
static code http_code(status status){switch(status){case accepted:return{status,"Accepted"};case bad_gateway:return{status,"Bad gateway"};case bad_request:return{status,"Bad Request"};case created:return{status,"Created"};case forbidden:return{status,"Forbidden"};case gateway_timeout:return{status,"Gateway Timeout"};case http_version_not_supported:return{status,"HTTP Version Not Supported"};case internal_server_error:return{status,"Internal Server Error"};case method_not_allowed:return{status,"Method Not Allowed"};case not_acceptable:return{status,"Not Acceptable"};case no_content:return{status,"No Content"};case not_found:return{status,"Not Found"};case not_implemented:return{status,"Not Implemented"};case ok:return{status,"OK"};case payment_required:return{status,"Payment Required"};case service_unavailable:return{status,"Service Unavailable"};case unauthorized:return{status,"Unauthorized"};default: return {status,"UNKNOWN"};}}
class basic_response {
protected:
virtual std::ostream& do_response(std::ostream& os)const=0;
public:
virtual ~basic_response(){}
virtual void add_header(const http::header &header)=0;
virtual void set_status(http::status code)=0;
friend std::ostream& operator<<(std::ostream& os,const http::basic_response &rh){ return rh.do_response(os); }
};
class response : public basic_response {
public:
response():status_code(http::http_code(http::ok)){}
void add_header(const http::header &header) override { response_headers.emplace(header); }
void set_status(http::status code) override { status_code=http_code(code); }
void set_body(std::string content) { body=content; }
protected:
std::ostream& do_response(std::ostream &os) const override {
os << "HTTP/1.1" << " " << status_code.first << " " << status_code.second << "\r\n";
for(auto &header:response_headers)
os << header.first << ": " << header.second << "\r\n";
if(status_code.first!=http::no_content){
os << "Content-Length: ";
if(!body.empty()){
return os << body.size() << "\r\n\r\n" << body;
} else {
auto body_replace=std::to_string(status_code.first) + " " + status_code.second;
return os << body_replace.size() << "\r\n\r\n" << body_replace;
}
}
return os << "\r\n";
}
protected:
headers response_headers;
std::string body;
code status_code;
};
/// A JSON response. The default constructor sets the Content-Type header to
/// application/json, the response defaults to 200 OK.
class json_response : public response {
public:
json_response(){
add_header({"Content-Type","application/json"});
set_body({{"code",status_code.first},{"status",status_code.second}});
}
/// Method updates the http status code. set_statups also updates the body to correspond to the new status.
/// Notice: if you previously set a body, it will be overwritten by the status code JSON representation.
void set_status(http::status code) override {
response::set_status(code);
set_body({{"code",status_code.first},{"status",status_code.second}});
}
void set_body(nlohmann::json json){ response::set_body(json.dump()); }
};
};;;
#endif // _TOREST_HTTP_HPP_

27
toREST/include/resource.hpp

@ -0,0 +1,27 @@
#ifndef _TOREST_RESOURCE_HPP_
#define _TOREST_RESOURCE_HPP_
#include <http.hpp>
class resource_base {
friend std::ostream& operator<<(std::ostream& os,const resource_base& rs)
{ os << rs.json_response; return os; }
public:
virtual ~resource_base(){}
/// GET Read 200 (OK), list of customers. Use pagination, sorting and filtering to navigate big lists. 200 (OK), single customer. 404 (Not Found), if ID not found or invalid.
virtual void get(nlohmann::json data=nullptr){}
/// PATCH Update/Modify 404 (Not Found), unless you want to modify the collection itself. 200 (OK) or 204 (No Content). 404 (Not Found), if ID not found or invalid.
virtual void patch(nlohmann::json data=nullptr){}
/// POST Create 201 (Created), 'Location' header with link to /customers/{id} containing new ID. 404 (Not Found), 409 (Conflict) if resource already exists..
virtual void post(nlohmann::json data=nullptr){}
/// DELETE Delete 404 (Not Found), unless you want to delete the whole collection—not often desirable. 200 (OK). 404 (Not Found), if ID not found or invalid.
virtual void del(nlohmann::json data=nullptr){}
/// PUT Update/Replace 404 (Not Found), unless you want to update/replace every resource in the entire collection. 200 (OK) or 204 (No Content). 404 (Not Found), if ID not found or invalid.
virtual void put(nlohmann::json data=nullptr){}
/// get a reference to the underlying JSON response
http::json_response& get_response(){ return json_response; }
protected:
http::json_response json_response;
};
#endif // _TOREST_RESOURCE_HPP_

250
toREST/include/session.hpp

@ -0,0 +1,250 @@
#ifndef _TOREST_RESOURCE_SESSION_HPP_
#define _TOREST_RESOURCE_SESSION_HPP_
#include <resource.hpp>
#include <util.hpp>
#include <libtorrent/session.hpp>
namespace session {
class basic_manager {
public:
virtual ~basic_manager()=0;
virtual nlohmann::json get_json() const = 0;
virtual bool patch(const nlohmann::json &data) = 0;
virtual bool is_valid() const = 0;
virtual int listen_port() const = 0;
virtual void set_session(std::shared_ptr<libtorrent::session>) = 0;
virtual void set_listen_port() = 0;
// virtual std::shared_ptr<libtorrent::session> get_session() = 0;
};
class translate {
public:
static nlohmann::json to_json(const libtorrent::session_handle &handle){
nlohmann::json session_json;
return session_json;
}
};
class manager : public basic_manager {
public:
// std::shared_ptr<libtorrent::session> get_session() override { return handle; }
void set_session(std::shared_ptr<libtorrent::session> session) override { handle=session; }
nlohmann::json get_json() const override {
nlohmann::json session_json;
session_json["peer_id"] = handle->id().to_string();
session_json["is_paused"] = handle->is_paused();
session_json["is_listening"] = handle->is_listening();
session_json["listen_port"] = handle->listen_port();
session_json["ssl_listen_port"] = handle->ssl_listen_port();
return session_json;
};
bool is_valid() const override { return handle && handle->is_valid(); }
bool patch(const nlohmann::json &data) override {
int is_paused=data["is_paued"];
int listen_port=data["listen_port"];
auto pause = [this](int is_paused){
if(is_paused!=-1){
if(is_paused==handle->is_paused()){
return true;
}else if(is_paused==true){
handle->resume();
return true;
}else if(is_paused==false){
handle->pause();
return true;
}
}
return false;
};
auto listen = [this](int listen_port){
if(listen_port!=-1&&listen_port>6880){
libtorrent::settings_pack sp;
sp.set_str(libtorrent::settings_pack::listen_interfaces,"0.0.0.0:"+std::to_string(listen_port));
handle->apply_settings(sp);
return true;
}
return false;
};
return pause(is_paused) && listen(listen_port);
};
private:
std::shared_ptr<libtorrent::session> handle;
};
class basic_resource : public resource_base {
public:
virtual ~basic_resource() {}
virtual void set_session(std::shared_ptr<basic_manager>)=0;
};
class resource : public basic_resource {
public:
resource(){} //TODO remove?
void set_session(std::shared_ptr<basic_manager> session_manager) override { mgr=session_manager; }
virtual void get(nlohmann::json arg=nullptr) override {
if(mgr && mgr->is_valid()){
get_response().set_status(http::ok);
get_response().set_body(mgr->get_json());
}else{
get_response().set_status(http::service_unavailable);
}
}
/**
* PATCH /session
* --------------
* \brief Change values on the session.
* @param[in] data takes an JSON object with at least one `action` set.
* available `action`s are `is_paused<bool>` `listen_port<bool>`
*/
virtual void patch(const nlohmann::json data=nlohmann::json::object()) override {
if(mgr && mgr->is_valid()){
if(data.is_object() && !data.is_null()){
int is_paused=util::json::get<int>("is_paused",data,-1),
listen_port=util::json::get<int>("listen_port",data,-1);
bool at_at_least_one_option_is_set=is_paused!=-1||listen_port!=-1;
if(at_at_least_one_option_is_set){
nlohmann::json patch={{"is_paused",is_paused},{"listen_port",listen_port}};
if(mgr->patch(patch)){
return get_response().set_status(http::ok);
}else{
return get_response().set_status(http::internal_server_error);
}
}else{
return get_response().set_status(http::bad_request);
}
}
return get_response().set_status(http::bad_request);
// auto data=data.array();
}else{
return get_response().set_status(http::service_unavailable);
}
}
// @todo should we return service unavailable or just method not allowed?
virtual void del(nlohmann::json arg=nullptr) override { get_response().set_status(http::method_not_allowed); }
virtual void put(nlohmann::json arg=nullptr) override { get_response().set_status(http::method_not_allowed); }
virtual void post(nlohmann::json arg=nullptr) override { get_response().set_status(http::method_not_allowed); }
protected:
std::shared_ptr<basic_manager> mgr;
};
}
#endif // _TOREST_RESOURCE_SESSION_HPP_
/*! Resource: torrents
Other:
- If multiple headers with the same key is posted, the server uses only the last.
- Server can return 503 Service Unavailable if for some reason a service is down */
/*
class torrents : public Resource<std::shared_ptr<HttpServer::Response>,std::shared_ptr<HttpServer::Request>> {
public:
torrents(libtorrent::session &session);
};
*/
// std::function<void(StreamType,StreamType)>
/*! API-End-point: [POST] /v1/session/torrents
Requirements:
- Content-Type: application/json
* This header must be set, otherwise toREST will return 403 Forbidden.
- Data:
* A valid JSON-object is required.
* A valid format:
```json
{
"torrent": "<torrent_url>",
"save_path": "<path>"
}
```
* Fields `torrent` and `save_path` can not be empty. toREST will return 400 Bad Request
* The `torrent` field can be a `local torrent file`, a `magnet-uri` or a HTTP-resource.
toREST will Accept every file with a valid url, uri or path, but will not download
or create torrents if the path is a invalid torrent file.
* The `save_path` field can be any location on the server machine, in which the invoking
user has write permissions. If the save_path is a directory, the torrent will download
into the directory. If the save_path is a file, toREST will return 400 Bad Request.
Side-Effects:
- 202 Accepted, if the request is valid.
* The `Location` header is set if the torrent info_hash is available
* If a WebSocket is open a TORRENT_ADDED action is posted when the torrent resource is available.
*
POST=[&session](std::shared_ptr<HttpServer::Response> resp,std::shared_ptr<HttpServer::Request> req){
http::request_handler handler(req,resp);
// If session is invalid there is no point hanging around.
if(!session.is_valid())
return handler.respond(i18N::session_unavailable,http::service_unavailable);
// At least one header named with Content-Type, then check if the last one set is application/json
if(handler.header_equals("Content-Type","application/json"))
return handler.respond(i18N::content_type_not_set,http::forbidden);
// If the supplied JSON is invalid, bad request
auto request_json=util::json::parse(req->content);
if(!request_json.is_object() || request_json.is_null())
return handler.respond(nlohmann::json(i18N::unable_to_parse_json),http::bad_request);
// handle required fields
std::vector<std::string> required_fields={
request_json.value("torrent",""),
request_json.value("save_path","")
};
std::vector<std::string> empty_fields;
std::copy_if(required_fields.begin(),required_fields.end(),empty_fields.begin(),[](const std::string &field){return field.empty();});
if(!empty_fields.empty()){
std::string message;
for(auto &field:empty_fields)
message+=" The required field `"+field+"` is empty";
return handler.respond(nlohmann::json(i18N::wrong_format + message),http::bad_request);
}
auto torrent_field=required_fields.front();
libtorrent::add_torrent_params torrent_options;
boost::system::error_code ec;
if(torrent_field.substr(0,6)=="magnet"){
libtorrent::parse_magnet_uri(torrent_field,torrent_options,ec);
ECERROR(ec);
}
else if(boost::filesystem::exists(torrent_field,ec) && !boost::filesystem::is_directory(torrent_field,ec))
torrent_options.ti=boost::make_shared<libtorrent::torrent_info>(libtorrent::torrent_info(torrent_field,ec));
else if(boost::regex_match(torrent_field,util::regex))
torrent_options.url=torrent_field;
else {
ECERROR(ec);
return handler.respond(nlohmann::json(i18N::unable_to_parse_torrent_uri),http::bad_request);
}
boost::filesystem::path save_path(required_fields.back());
auto status=boost::filesystem::status(save_path,ec);
if(!ec){
if(boost::filesystem::exists(status) && !boost::filesystem::is_directory(status))
return handler.respond(nlohmann::json(i18N::wrong_format + " The save path is invalid"),http::bad_request);
else {
// TODO chroot
try {
boost::filesystem::create_directories(save_path);
} catch(const std::exception &ex){
return handler.respond(nlohmann::json(i18N::write_error),http::forbidden);
}
}
} else {
// invalid file status
ECERROR(ec);
}
torrent_options.save_path=save_path.generic_string();
session.async_add_torrent(torrent_options);
// TODO add websocket event
std::string info_hash(torrent_options.info_hash.data());
if(info_hash!="undefined"){
std::stringstream ss;
ss << torrent_options.info_hash;
info_hash=ss.str();
handler.headers["Location:"]= "/torrents/" + info_hash;
}
return handler.respond(nlohmann::json(http::http_code(http::accepted).second), http::accepted);
};
}*/

94
toREST/include/util.hpp

@ -0,0 +1,94 @@
#ifndef _BT_HELPERS_HPP_
#define _BT_HELPERS_HPP_
#include <unordered_map>
#include <boost/regex.hpp>
#include <json.hpp>
namespace i18N {
using namespace std;
static const auto content_type_not_set= "A Content-Type header set to application/json is required."s;
static const auto unable_to_parse_json= "Unable to parse JSON in body into a JSON-object."s;
static const auto session_unavailable= "A server service is down, please try again later."s;
static const auto wrong_format= "A valid JSON request was posted, but the JSON format was wrong."s;
static const auto unable_to_parse_torrent_uri= "The torrent location was not accepted, the format was wrong."s;
static const auto write_error= "Unable to write to ";
};
namespace util {
class uri {
public:
auto static parse(const std::string &request_path){
std::unordered_map<std::string,std::string> options;
std::string option, value;
bool collect=false;
for(auto &c:request_path){
switch(c){
case '&':
options.insert(std::make_pair(option,value));
option="";
collect=true;
break;
case '?':
collect=true;
break;
case '=':
collect=false;
value="";
break;
default:
if(collect)
option+=c;
else
value+=c;
break;
}
}
if(!option.empty() || !value.empty())
options.insert(std::make_pair(option,value));
return options;
}
};
const boost::regex regex("@(https?|ftp)://(-\\.)?([^\\s/?\\.#-]+\\.?)+(/[^\\s]*)?$@", boost::regex_constants::perl | boost::regex_constants::icase);
class json {
public:
/*! @brief Wrapper for nlohmann::json::parse. If the parse fails, result.is_null() will be */
static nlohmann::json parse(std::istream &istream) {
try {
return nlohmann::json::parse(istream);
} catch(const std::invalid_argument &exp) {
}
return nlohmann::json(nullptr);
}
/*! @brief wrapper around nlohmann::json::parse, but object returns null on throw */
static nlohmann::json parse(const std::string &string){
try {
return nlohmann::json::parse(string);
} catch(const std::invalid_argument &exp) {
}
return nlohmann::json(nullptr);
}
static bool has_property(const std::string &key, const nlohmann::json &object){
auto it=object.find(key);
return it!=object.end();
}
template<class T>
static auto get(const std::string &key, const nlohmann::json &object, const T &default_value){
if(has_property(key,object)){
T r;
try{
r=object[key]; //TODO fix use json parser where a non-throwable exist
} catch(const std::exception&){
return default_value;
}
return r;
}
return default_value;
}
};
}
#endif // _BT_HELPERS_HPP_

83
toREST/src/main.cxx

@ -0,0 +1,83 @@
#include <server_http.hpp>
#include <session.hpp>
typedef SimpleWeb::Server<SimpleWeb::HTTP> HttpServer;
using namespace std;
int main(int argc, char *argv[]) {
//auto config=filesystem::get_config();
//auto wsc=config["webserver"];
int http_port=8080, http_threads=4;
HttpServer http_server(http_port,http_threads);
http_server.default_resource["GET"]=[](std::shared_ptr<HttpServer::Response> resp, std::shared_ptr<HttpServer::Request> req) {
auto response = http::response();
response.set_status(http::not_found);
*resp << response;
};
session::resource session;
http_server.resource["^/v1/session$"]["GET"]=[&session](std::shared_ptr<HttpServer::Response> resp, std::shared_ptr<HttpServer::Request> req) {
session.get();
*resp << session;
};
/*
http_server.resource["^/v1/session$"]["GET"]=[&session](HttpServer::Response &response, std::shared_ptr<HttpServer::Request> request) {
http http(&response, request.get());
if(!session.is_valid()){
return http.internal_server_error();
}
auto json=translate::session::to_json(session);
http.json(json);
};
http_server.resource["^/v1/session$"]["POST"]=[&session](HttpServer::Response &response, std::shared_ptr<HttpServer::Request> request) {
http http(&response, request.get());
if(!http.is_json_request()){
return http.not_found();
}
if(!session.is_valid()){
return http.internal_server_error();
}
nlohmann::json json(request->content);
std::string action = json.value("action","");
if(action==""){
return http.bad_request("Invalid JSON request");
}
if(action=="TORRENTS_PAUSE"){
session.pause();
} else if (action=="TORRENTS_START"){
session.resume();
}
return http.json(json);
};
*/
/*
std::thread websocketserver_thread([&websocket_server]() {
websocket_server.start();
std::cout << "WebSocketServer listening on port " << config["websocket.port"] << std::endl;
});
*/
std::thread server_thread([&http_server](){
http_server.start();
});
std::cout << "HttpServer listening on port " << 8080 << std::endl;
std::string input;
while(input != "q") {
std::getline(std::cin, input);
if(input == "q") {
std::cout << "Stopping server..." << std::endl;
http_server.stop(); // TODO check if throws
std::cout << "Server stopped" << std::endl;
break;
}
}
server_thread.join();
// websocketserver_thread.join();
return 0;
};

120
toREST/tests/http_test.cpp

@ -0,0 +1,120 @@
#include <Catch/fakeit.hpp>
#include <http.hpp>
using namespace std;
TEST_CASE("Check http status"){
auto out=http::http_code(http::ok);
REQUIRE(out.first == 200);
REQUIRE(out.second == "OK");
// TODO test all
}
SCENARIO("http response"){
auto response=http::response();
std::stringstream ss;
GIVEN("we stream our response without setting given any data"){
ss << response;
THEN("the response will default to 200")
REQUIRE(ss.str()=="HTTP/1.1 200 OK\r\nContent-Length: 6\r\n\r\n200 OK"s);
}
GIVEN("we stream our data with the response status set to 404"){
response.set_status(http::not_found);
ss << response;
THEN("the default 404 response will be sent instead"){
REQUIRE(ss.str()=="HTTP/1.1 404 Not Found\r\nContent-Length: 13\r\n\r\n404 Not Found"s);
}
}
GIVEN("we stream our response with the status set to 204 No Content"){
response.set_status(http::no_content);
ss << response;
THEN("the response has no content header set")
REQUIRE(ss.str()=="HTTP/1.1 204 No Content\r\n\r\n"s);
}
GIVEN("we stream our response with the body set"){
response.set_body("Easter eggs, everywhere! <a href=\"http://stream1.gifsoup.com/view4/4086928/x-x-everywhere-o.gif\">ye</a>");
ss << response;
THEN("our response contains the appropriate headers")
REQUIRE(ss.str()=="HTTP/1.1 200 OK\r\nContent-Length: 103\r\n\r\nEaster eggs, everywhere! <a href=\"http://stream1.gifsoup.com/view4/4086928/x-x-everywhere-o.gif\">ye</a>"s);
}
}
SCENARIO("Stream http responses with a json response"){
auto response=http::json_response();
std::stringstream ss;
GIVEN("we stream our response without setting any options"){
ss << response;
THEN("our response is valid json and the response code is 200")
REQUIRE(ss.str()=="HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: 26\r\n\r\n{\"code\":200,\"status\":\"OK\"}"s);
}
GIVEN("we set the response code to 404 and stream our response"){
response.set_status(http::not_found);
ss << response;
THEN("our response is valid json and the resoinse code is 404")
REQUIRE(ss.str()=="HTTP/1.1 404 Not Found\r\nContent-Type: application/json\r\nContent-Length: 33\r\n\r\n{\"code\":404,\"status\":\"Not Found\"}"s);
}
GIVEN("we stream our response with the status set to 204 No Content"){
response.set_status(http::no_content);
ss << response;
THEN("the response has no content header set")
REQUIRE(ss.str()=="HTTP/1.1 204 No Content\r\nContent-Type: application/json\r\n\r\n"s);
}
GIVEN("we stream our response with the body set"){
response.set_body(nlohmann::json::object());
ss << response;
THEN("our response contains the appropriate headers")
REQUIRE(ss.str()=="HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: 2\r\n\r\n{}"s);
}
}
/*
TEST(http_response_handler, header){
ServerTest s;
auto req=s.create_request("GET /v1/session/torrents HTTP/1.1\r\nHost: localhost:8080\r\nFoo: bar\r\nFoo: bars\r\nContent-Type: application/json\r\n\r\n");
std::shared_ptr<ServerTest::Response> resp(new ServerTest::Response(nullptr));
http::request_handler out(req,resp);
ASSERT_TRUE(out.header_set("Content-Type"));
ASSERT_EQ(out.find_last_header_value("Content-Type"),"application/json");
ASSERT_EQ(out.find_last_header_value("Host"),"localhost:8080");
ASSERT_EQ(out.find_last_header_value("Foo"),"bars");
ASSERT_NE(out.find_last_header_value("Foo"), "bar");
ASSERT_FALSE(out.header_set("Content-Length"));
ASSERT_TRUE(out.header_equals("Content-Type","application/json"));
}
TEST(http_response_handler, json_response){
ServerTest s;
auto req=s.create_request("GET /v1/session/torrents HTTP/1.1\r\nHost: localhost:8080\r\nContent-Type: application/json\r\n\r\n");
std::shared_ptr<ServerTest::Response> resp(new ServerTest::Response(nullptr));
http::request_handler out(req,resp);
out.respond(nlohmann::json("{}"));
std::stringstream ss;
ss << resp->rdbuf();
std::string string_response(ss.str());
ASSERT_EQ(string_response,"HTTP/1.1 200 OK\r\nContent-Length: 4\r\nContent-Type: application/json\r\n\r\n\"{}\"");
}
TEST(http_response_handler, string_response){
ServerTest s;
auto req=s.create_request("GET /v1/session/torrents HTTP/1.1\r\nHost: localhost:8080\r\nContent-Type: application/json\r\n\r\n");
std::shared_ptr<ServerTest::Response> resp(new ServerTest::Response(nullptr));
http::request_handler out(req,resp);
out.respond(std::string("{}"));
std::stringstream ss;
ss << resp->rdbuf();
std::string string_response(ss.str());
ASSERT_EQ(string_response,"HTTP/1.1 200 OK\r\nContent-Length: 2\r\n\r\n{}");
}
TEST(http_response_handler, empty_string){
ServerTest s;
auto req=s.create_request("GET /v1/session/torrents HTTP/1.1\r\nHost: localhost:8080\r\nContent-Type: application/json\r\n\r\n");
std::shared_ptr<ServerTest::Response> resp(new ServerTest::Response(nullptr));
http::request_handler out(req,resp);
out.respond(std::string(""));
std::stringstream ss;
ss << resp->rdbuf();
std::string string_response(ss.str());
ASSERT_EQ(string_response,"HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n");
}
*/

118
toREST/tests/session_test.cpp

@ -0,0 +1,118 @@
#include <Catch/fakeit.hpp>
#include <session.hpp>
using namespace fakeit;
const std::string method_not_allowed_json ="HTTP/1.1 405 Method Not Allowed\r\nContent-Type: application/json\r\nContent-Length: 42\r\n\r\n{\"code\":405,\"status\":\"Method Not Allowed\"}";
const std::string service_unavailable_json="HTTP/1.1 503 Service Unavailable\r\nContent-Type: application/json\r\nContent-Length: 43\r\n\r\n{\"code\":503,\"status\":\"Service Unavailable\"}";
const std::string internal_server_error_json="HTTP/1.1 500 Internal Server Error\r\nContent-Type: application/json\r\nContent-Length: 45\r\n\r\n{\"code\":500,\"status\":\"Internal Server Error\"}";
const std::string not_found ="HTTP/1.1 404 Not Found\r\nContent-Type: application/json\r\nContent-Length: 36\r\n\r\n{\"code\":404,\"status\":\"Not Found\"}";
const std::string ok_nullptr_json ="HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: 4\r\n\r\nnull";
const std::string bad_request ="HTTP/1.1 400 Bad Request\r\nContent-Type: application/json\r\nContent-Length: 35\r\n\r\n{\"code\":400,\"status\":\"Bad Request\"}";
using namespace session;
SCENARIO("test of responses by session_manager on /v1/session"){
session::resource sr;
std::stringstream ss;
GIVEN("the session is invalid"){
WHEN("we receive a GET request"){
sr.get();
ss << sr;
THEN("the response is a 503 Service Unavailable JSON object")
REQUIRE(ss.str()==service_unavailable_json);
}
WHEN("we receive a PATCH request"){
sr.patch();
ss << sr;
THEN("the response is a 503 Service Unavailable JSON object")
REQUIRE(ss.str()==service_unavailable_json);
}
WHEN("we receive a POST request"){
sr.post();
ss << sr;
THEN("the response is a 405 Method Not Allowed JSON object")
REQUIRE(ss.str()==method_not_allowed_json);
}
WHEN("we receive a DELETE request"){
sr.del();
ss << sr;
THEN("the response is a 405 Method Not Allowed JSON object")
REQUIRE(ss.str()==method_not_allowed_json);
}
WHEN("we receive a PUT request"){
sr.put();
ss << sr;
THEN("the response is a 405 Method Not Allowed JSON object")
REQUIRE(ss.str()==method_not_allowed_json);
}
}
GIVEN("the session is valid"){
Mock<basic_manager> mock_session;
When(Method(mock_session,basic_manager::is_valid)).AlwaysReturn(true);
When(Method(mock_session,basic_manager::get_json)).AlwaysReturn(nullptr);
auto session=std::shared_ptr<basic_manager>(&(mock_session.get()),[](...){});
sr.set_session(session);
WHEN("we receive a GET request"){
sr.get();
ss << sr;
THEN("the response is a 200 OK JSON object ")
REQUIRE(ss.str()==ok_nullptr_json);
}
WHEN("we receive a PATCH request with invalid data"){
GIVEN("the JSON is invalid"){
sr.patch(nullptr);
ss << sr;
THEN("the response is a 400 Bad Request JSON object")
REQUIRE(ss.str()==bad_request);
}
GIVEN("the JSON is valid, but contains no data"){
sr.patch({});
ss << sr;
THEN("the response is a 400 Bad Request JSON object")
REQUIRE(ss.str()==bad_request);
}
GIVEN("the JSON is valid, but the port number is to low"){
When(Method(mock_session,basic_manager::patch)).AlwaysReturn(false);
sr.patch({{"listen_port",1}});
ss << sr;
THEN("the response is a 500 Internal Server Error JSON object")
REQUIRE(ss.str()==internal_server_error_json);
}
GIVEN("the JSON is valid, but listen_port is a string"){
sr.patch({{"listen_port","1"}});
ss << sr;
THEN("the response is a 400 Bad Request JSON object")
REQUIRE(ss.str()==bad_request);
}
GIVEN("the JSON is valid, but is_paused is a string"){
sr.patch({{"is_paused","1"}});
ss << sr;
THEN("the response is a 400 Bad Request JSON object")
REQUIRE(ss.str()==bad_request);
}
WHEN("we receive a POST request"){
sr.post();
ss << sr;
THEN("the response is a 405 Method Not Allowed JSON object")
REQUIRE(ss.str()==method_not_allowed_json);
}
WHEN("we receive a DELETE request"){
sr.del();
ss << sr;
THEN("the response is a 405 Method Not Allowed JSON object")
REQUIRE(ss.str()==method_not_allowed_json);
}
WHEN("we receive a PUT request"){
sr.put();
ss << sr;
THEN("the response is a 405 Method Not Allowed JSON object")
REQUIRE(ss.str()==method_not_allowed_json);
}
}
}
}

3
toREST/tests/test.cpp

@ -0,0 +1,3 @@
#define CATCH_CONFIG_MAIN
#include <Catch/fakeit.hpp>
Loading…
Cancel
Save