summaryrefslogtreecommitdiffhomepage
path: root/tests
diff options
context:
space:
mode:
authorRoland Reichwein <mail@reichwein.it>2023-01-09 13:15:18 +0100
committerRoland Reichwein <mail@reichwein.it>2023-01-09 13:15:18 +0100
commitdc2e2b3e293a8374a2627982b521cc6865129c49 (patch)
treebd34d6c13e330be5937aec29503cbe6649d0fa74 /tests
parentd747193e76baf689211d9f1e42335360288d43c0 (diff)
Separated out websocket
Diffstat (limited to 'tests')
-rw-r--r--tests/Makefile8
-rw-r--r--tests/test-config.cpp1
-rw-r--r--tests/test-webserver.cpp304
3 files changed, 216 insertions, 97 deletions
diff --git a/tests/Makefile b/tests/Makefile
index 5f162de..14af291 100644
--- a/tests/Makefile
+++ b/tests/Makefile
@@ -36,6 +36,7 @@ LDFLAGS+=-pie
UNITS=\
auth.cpp \
config.cpp \
+ error.cpp \
http.cpp \
https.cpp \
plugin.cpp \
@@ -43,7 +44,8 @@ UNITS=\
response.cpp \
statistics.cpp \
server.cpp \
- webserver.cpp
+ webserver.cpp \
+ websocket.cpp
TESTSRC=\
test-auth.cpp \
@@ -85,6 +87,8 @@ auth.o: ../auth.cpp
$(CXX) $(CXXFLAGS) $(CXXTESTFLAGS) -c $< -o $@
config.o: ../config.cpp
$(CXX) $(CXXFLAGS) $(CXXTESTFLAGS) -c $< -o $@
+error.o: ../error.cpp
+ $(CXX) $(CXXFLAGS) $(CXXTESTFLAGS) -c $< -o $@
http.o: ../http.cpp
$(CXX) $(CXXFLAGS) $(CXXTESTFLAGS) -c $< -o $@
https.o: ../https.cpp
@@ -101,6 +105,8 @@ server.o: ../server.cpp
$(CXX) $(CXXFLAGS) $(CXXTESTFLAGS) -c $< -o $@
webserver.o: ../webserver.cpp
$(CXX) $(CXXFLAGS) $(CXXTESTFLAGS) -c $< -o $@
+websocket.o: ../websocket.cpp
+ $(CXX) $(CXXFLAGS) $(CXXTESTFLAGS) -c $< -o $@
ADD_DEP=Makefile
diff --git a/tests/test-config.cpp b/tests/test-config.cpp
index fe482f8..eb7e9c7 100644
--- a/tests/test-config.cpp
+++ b/tests/test-config.cpp
@@ -31,6 +31,7 @@ public:
{
std::error_code ec;
fs::remove(testConfigFilename);
+ fs::remove("stats.db");
}
};
diff --git a/tests/test-webserver.cpp b/tests/test-webserver.cpp
index 7059bc6..6bbf302 100644
--- a/tests/test-webserver.cpp
+++ b/tests/test-webserver.cpp
@@ -3,11 +3,6 @@
#define BOOST_TEST_MODULE webserver_test
#include <boost/test/included/unit_test.hpp>
-// Support both boost in Debian unstable (BOOST_LATEST) and in stable (boost 1.67)
-#if BOOST_VERSION >= 107100
-#define BOOST_LATEST
-#endif
-
#include <boost/test/data/dataset.hpp>
#include <boost/test/data/monomorphic.hpp>
#include <boost/test/data/test_case.hpp>
@@ -17,9 +12,7 @@
#include <boost/beast/http.hpp>
#include <boost/beast/websocket.hpp>
#include <boost/beast/websocket/ssl.hpp>
-#ifdef BOOST_LATEST
#include <boost/beast/ssl.hpp>
-#endif
#include <boost/beast/version.hpp>
#include <boost/asio/buffer.hpp>
#include <boost/asio/buffers_iterator.hpp>
@@ -47,6 +40,7 @@
#include <unistd.h>
#include <libreichwein/file.h>
+#include <libreichwein/process.h>
#include "webserver.h"
@@ -211,31 +205,17 @@ VZTqPHmb+db0rFA3XlAg2A==
m_filebuf = 0;
}
- bool isRunning()
+ bool is_running()
{
if (m_pid == 0)
return false;
- fs::path pid_file{fmt::format("/proc/{}/stat", m_pid)};
- if (!fs::exists(pid_file))
- return false;
-
- std::string s{File::getFile(pid_file)};
-
- auto pos0{s.find(' ', 0)};
- pos0 = s.find(' ', pos0 + 1);
- pos0++;
-
- auto pos1{s.find(' ', pos0 + 1)};
-
- std::string state{s.substr(pos0, pos1 - pos0)};
-
- return state == "R" || state == "S";
+ return Reichwein::Process::is_running(m_pid);
}
std::string output()
{
- if (!isRunning())
+ if (!is_running())
throw std::runtime_error("No output/stdout available from webserver since it is not running");
if (!m_is)
@@ -255,7 +235,7 @@ private:
// child stdout
std::shared_ptr<__gnu_cxx::stdio_filebuf<char>> m_filebuf;
std::shared_ptr<std::istream> m_is;
-};
+}; // class WebserverProcess
std::pair<std::string,std::string> HTTP(const std::string& target, bool ipv6 = true, bool HTTP11 = true, boost::beast::http::verb method = boost::beast::http::verb::get)
{
@@ -331,13 +311,7 @@ std::pair<std::string,std::string> HTTPS(const std::string& target, bool ipv6 =
boost::asio::io_context ioc;
// The SSL context is required, and holds certificates
- boost::asio::ssl::context ctx(
-#ifdef BOOST_LATEST
- boost::asio::ssl::context::tlsv13_client
-#else
- boost::asio::ssl::context::tlsv12_client
-#endif
- );
+ boost::asio::ssl::context ctx(boost::asio::ssl::context::tlsv13_client);
// This holds the root certificate used for verification
load_root_certificates(ctx);
@@ -410,16 +384,19 @@ class Fixture
{
public:
Fixture(){}
- ~Fixture(){}
+ ~Fixture()
+ {
+ fs::remove("stats.db");
+ }
};
BOOST_DATA_TEST_CASE_F(Fixture, http_get, data::make({false, true}) * data::make({false, true}) * data::make({false, true}) * data::make({boost::beast::http::verb::head, boost::beast::http::verb::get}), ipv6, http11, https, method)
{
WebserverProcess serverProcess;
- BOOST_REQUIRE(serverProcess.isRunning());
+ BOOST_REQUIRE(serverProcess.is_running());
std::pair<std::string,std::string> response{https ? HTTPS("/webserver.conf", ipv6, http11, method) : HTTP("/webserver.conf", ipv6, http11, method)};
- BOOST_REQUIRE(serverProcess.isRunning());
+ BOOST_REQUIRE(serverProcess.is_running());
std::string::size_type size{File::getFile(testConfigFilename).size()};
BOOST_CHECK_GT(size, 0);
BOOST_REQUIRE_EQUAL(response.first, fmt::format("HTTP/{} 200 OK\r\nServer: Reichwein.IT Webserver " VERSION "\r\nContent-Type: application/text\r\nContent-Length: {}\r\n\r\n", http11 ? "1.1" : "1.0", method == boost::beast::http::verb::head ? 0 : size));
@@ -427,7 +404,7 @@ BOOST_DATA_TEST_CASE_F(Fixture, http_get, data::make({false, true}) * data::make
for (int i = 0; i < 10; i++) {
std::pair<std::string,std::string> response{https ? HTTPS("/webserver.conf", ipv6, http11, method) : HTTP("/webserver.conf", ipv6, http11, method)};
- BOOST_REQUIRE(serverProcess.isRunning());
+ BOOST_REQUIRE(serverProcess.is_running());
BOOST_REQUIRE_EQUAL(response.first, fmt::format("HTTP/{} 200 OK\r\nServer: Reichwein.IT Webserver " VERSION "\r\nContent-Type: application/text\r\nContent-Length: {}\r\n\r\n", http11 ? "1.1" : "1.0", method == boost::beast::http::verb::head ? 0 : size));
BOOST_REQUIRE_EQUAL(response.second, method == boost::beast::http::verb::head ? ""s : File::getFile(testConfigFilename));
}
@@ -437,92 +414,227 @@ BOOST_DATA_TEST_CASE_F(Fixture, http_get_file_not_found, data::make({false, true
{
WebserverProcess serverProcess;
- BOOST_REQUIRE(serverProcess.isRunning());
+ BOOST_REQUIRE(serverProcess.is_running());
BOOST_REQUIRE(!fs::exists("./webserver.confSUFFIX"));
auto response{(https ? HTTPS("/webserver.confSUFFIX", ipv6, http11, method) : HTTP("/webserver.confSUFFIX", ipv6, http11, method))};
- BOOST_REQUIRE(serverProcess.isRunning());
+ BOOST_REQUIRE(serverProcess.is_running());
BOOST_REQUIRE_EQUAL(response.first, fmt::format("HTTP/{} 404 Not Found\r\nServer: Reichwein.IT Webserver " VERSION "\r\nContent-Type: text/html\r\nContent-Length: {}\r\n\r\n", http11 ? "1.1" : "1.0", method == boost::beast::http::verb::head ? 0 : 36));
BOOST_REQUIRE_EQUAL(response.second, method == boost::beast::http::verb::head ? "" : "404 Not found: /webserver.confSUFFIX");
}
-BOOST_FIXTURE_TEST_CASE(websocket, Fixture)
+// Test server
+class WebsocketServerProcess
{
- WebserverProcess serverProcess;
- BOOST_REQUIRE(serverProcess.isRunning());
+public:
+ WebsocketServerProcess()
+ {
+ start();
+ }
+
+ ~WebsocketServerProcess()
+ {
+ stop();
+ }
+
+ // Echoes back all received WebSocket messages
+ void do_session(boost::asio::ip::tcp::socket socket)
+ {
+ try
+ {
+ // Construct the stream by moving in the socket
+ boost::beast::websocket::stream<boost::asio::ip::tcp::socket> ws{std::move(socket)};
+
+ // Set a decorator to change the Server of the handshake
+ ws.set_option(boost::beast::websocket::stream_base::decorator(
+ [](boost::beast::websocket::response_type& res)
+ {
+ res.set(boost::beast::http::field::server,
+ std::string("Reichwein.IT Test Websocket Server"));
+ }));
+
+ // Accept the websocket handshake
+ ws.accept();
+
+ for(;;)
+ {
+ // This buffer will hold the incoming message
+ boost::beast::flat_buffer buffer;
+
+ // Read a message
+ ws.read(buffer);
+
+ // Echo the message back
+ ws.text(ws.got_text());
+ std::string data(boost::asio::buffers_begin(buffer.data()), boost::asio::buffers_end(buffer.data()));
+ data += ": " + std::to_string(m_count++);
+ buffer.consume(buffer.size());
+ boost::beast::ostream(buffer) << data;
+ ws.write(buffer.data());
+ }
+ }
+ catch(boost::beast::system_error const& se)
+ {
+ // This indicates that the session was closed
+ if(se.code() != boost::beast::websocket::error::closed)
+ std::cerr << "Error: " << se.code().message() << std::endl;
+ }
+ catch(std::exception const& e)
+ {
+ std::cerr << "Error: " << e.what() << std::endl;
+ }
+ }
+
+ bool is_running()
+ {
+ if (m_pid == 0)
+ return false;
+
+ return Reichwein::Process::is_running(m_pid);
+ }
+
+ void start()
+ {
+ if (m_pid != 0)
+ throw std::runtime_error("Process already running, so it can't be started");
+
+ // connect stdout of new child process to stream of parent, via pipe
+ m_pid = fork();
+ if (m_pid < 0)
+ throw std::runtime_error("Fork unsuccessful.");
+
+ if (m_pid == 0) { // child process branch
+ while (true) {
+ try
+ {
+ auto const address = boost::asio::ip::make_address("localhost");
+ auto const port = static_cast<unsigned short>(9876);
+
+ // The io_context is required for all I/O
+ boost::asio::io_context ioc{1};
+
+ // The acceptor receives incoming connections
+ boost::asio::ip::tcp::acceptor acceptor{ioc, {address, port}};
+ for(;;)
+ {
+ // This will receive the new connection
+ boost::asio::ip::tcp::socket socket{ioc};
+
+ // Block until we get a connection
+ acceptor.accept(socket);
+
+ // Launch the session, transferring ownership of the socket
+ std::thread(
+ &WebsocketServerProcess::do_session, this,
+ std::move(socket)).detach();
+ }
+ }
+ catch (const std::exception& e)
+ {
+ std::cerr << "Error: " << e.what() << std::endl;
+ }
+ }
+ exit(0);
+ }
+ }
+
+ void stop()
+ {
+ if (!is_running())
+ throw std::runtime_error("Process not running, so it can't be stopped");
+
+ if (kill(m_pid, SIGKILL) != 0)
+ throw std::runtime_error("Unable to kill process");
+
+ if (int result = waitpid(m_pid, NULL, 0); result != m_pid)
+ throw std::runtime_error("waitpid returned "s + std::to_string(result));
+
+ m_pid = 0;
+ }
+private:
+ int m_pid{};
+ int m_count{};
+}; // class WebsocketServerProcess
- std::string host = "::1";
- auto const port = "8081" ;
- auto const text = "request1";
+BOOST_FIXTURE_TEST_CASE(websocket, Fixture)
+{
+ WebserverProcess serverProcess;
+ BOOST_REQUIRE(serverProcess.is_running());
+
+ WebsocketServerProcess websocketProcess;
+ BOOST_REQUIRE(websocketProcess.is_running());
- // The io_context is required for all I/O
- boost::asio::io_context ioc;
+ std::string host = "::1";
+ auto const port = "8081" ;
+ auto const text = "request1";
- // The SSL context is required, and holds certificates
- boost::asio::ssl::context ctx{boost::asio::ssl::context::tlsv13_client};
+ // The io_context is required for all I/O
+ boost::asio::io_context ioc;
- // This holds the root certificate used for verification
- load_root_certificates(ctx);
+ // The SSL context is required, and holds certificates
+ boost::asio::ssl::context ctx{boost::asio::ssl::context::tlsv13_client};
- // These objects perform our I/O
- boost::asio::ip::tcp::resolver resolver{ioc};
- boost::beast::websocket::stream<boost::beast::ssl_stream<boost::asio::ip::tcp::socket>> ws{ioc, ctx};
+ // This holds the root certificate used for verification
+ load_root_certificates(ctx);
- // Look up the domain name
- auto const results = resolver.resolve(host, port);
+ // These objects perform our I/O
+ boost::asio::ip::tcp::resolver resolver{ioc};
+ boost::beast::websocket::stream<boost::beast::ssl_stream<boost::asio::ip::tcp::socket>> ws{ioc, ctx};
- // Make the connection on the IP address we get from a lookup
- auto ep = boost::asio::connect(get_lowest_layer(ws), results);
+ // Look up the domain name
+ auto const results = resolver.resolve(host, port);
- // Set SNI Hostname (many hosts need this to handshake successfully)
- if(! SSL_set_tlsext_host_name(ws.next_layer().native_handle(), host.c_str()))
- throw boost::beast::system_error(
- boost::beast::error_code(
- static_cast<int>(::ERR_get_error()),
- boost::asio::error::get_ssl_category()),
- "Failed to set SNI Hostname");
+ // Make the connection on the IP address we get from a lookup
+ auto ep = boost::asio::connect(get_lowest_layer(ws), results);
- // Update the host_ string. This will provide the value of the
- // Host HTTP header during the WebSocket handshake.
- // See https://tools.ietf.org/html/rfc7230#section-5.4
- host += ':' + std::to_string(ep.port());
+ // Set SNI Hostname (many hosts need this to handshake successfully)
+ if(! SSL_set_tlsext_host_name(ws.next_layer().native_handle(), host.c_str()))
+ throw boost::beast::system_error(
+ boost::beast::error_code(
+ static_cast<int>(::ERR_get_error()),
+ boost::asio::error::get_ssl_category()),
+ "Failed to set SNI Hostname");
+
+ // Update the host_ string. This will provide the value of the
+ // Host HTTP header during the WebSocket handshake.
+ // See https://tools.ietf.org/html/rfc7230#section-5.4
+ host += ':' + std::to_string(ep.port());
- // Perform the SSL handshake
- ws.next_layer().handshake(boost::asio::ssl::stream_base::client);
+ // Perform the SSL handshake
+ ws.next_layer().handshake(boost::asio::ssl::stream_base::client);
- // Set a decorator to change the User-Agent of the handshake
- ws.set_option(boost::beast::websocket::stream_base::decorator(
- [](boost::beast::websocket::request_type& req)
- {
- req.set(boost::beast::http::field::user_agent,
- std::string(BOOST_BEAST_VERSION_STRING) +
- " websocket-client-coro");
- }));
+ // Set a decorator to change the User-Agent of the handshake
+ ws.set_option(boost::beast::websocket::stream_base::decorator(
+ [](boost::beast::websocket::request_type& req)
+ {
+ req.set(boost::beast::http::field::user_agent,
+ std::string("Reichwein.IT Test Websocket Client"));
+ }));
- // Perform the websocket handshake
- ws.handshake(host, "/");
+ // Perform the websocket handshake
+ ws.handshake(host, "/");
- // Send the message
- ws.write(boost::asio::buffer(std::string(text)));
+ // Send the message
+ ws.write(boost::asio::buffer(std::string(text)));
- // This buffer will hold the incoming message
- boost::beast::flat_buffer buffer;
+ // This buffer will hold the incoming message
+ boost::beast::flat_buffer buffer;
- // Read a message into our buffer
- ws.read(buffer);
- std::string data(boost::asio::buffers_begin(buffer.data()), boost::asio::buffers_end(buffer.data()));
- BOOST_CHECK_EQUAL(data, "request1: 0");
+ // Read a message into our buffer
+ ws.read(buffer);
+ std::string data(boost::asio::buffers_begin(buffer.data()), boost::asio::buffers_end(buffer.data()));
+ BOOST_CHECK_EQUAL(data, "request1: 0");
- buffer.consume(buffer.size());
+ buffer.consume(buffer.size());
- ws.write(boost::asio::buffer(std::string(text)));
- ws.read(buffer);
- data = std::string(boost::asio::buffers_begin(buffer.data()), boost::asio::buffers_end(buffer.data()));
- BOOST_CHECK_EQUAL(data, "request1: 1");
+ ws.write(boost::asio::buffer(std::string(text)));
+ ws.read(buffer);
+ data = std::string(boost::asio::buffers_begin(buffer.data()), boost::asio::buffers_end(buffer.data()));
+ BOOST_CHECK_EQUAL(data, "request1: 1");
- // Close the WebSocket connection
- ws.close(boost::beast::websocket::close_code::normal);
+ // Close the WebSocket connection
+ ws.close(boost::beast::websocket::close_code::normal);
- BOOST_REQUIRE(serverProcess.isRunning());
+ BOOST_REQUIRE(serverProcess.is_running());
}