summaryrefslogtreecommitdiffhomepage
path: root/tests/test-webchat.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'tests/test-webchat.cpp')
-rw-r--r--tests/test-webchat.cpp255
1 files changed, 255 insertions, 0 deletions
diff --git a/tests/test-webchat.cpp b/tests/test-webchat.cpp
new file mode 100644
index 0000000..3f70bcf
--- /dev/null
+++ b/tests/test-webchat.cpp
@@ -0,0 +1,255 @@
+#include <gtest/gtest.h>
+
+#include <cstring>
+#include <filesystem>
+#include <memory>
+#include <regex>
+#include <string>
+#include <system_error>
+#include <thread>
+
+#include <boost/process/child.hpp>
+#include <boost/algorithm/string.hpp>
+#include <boost/beast/core.hpp>
+#include <boost/beast/http.hpp>
+#include <boost/beast/websocket.hpp>
+#include <boost/asio/buffer.hpp>
+#include <boost/asio/buffers_iterator.hpp>
+#include <boost/asio/connect.hpp>
+#include <boost/asio/ip/tcp.hpp>
+#include <boost/asio/local/stream_protocol.hpp>
+
+#include <unistd.h>
+
+#include "libreichwein/file.h"
+#include "libreichwein/process.h"
+
+#include "config.h"
+#include "storage.h"
+#include "whiteboard.h"
+
+namespace bp = boost::process;
+namespace fs = std::filesystem;
+using namespace Reichwein;
+using namespace std::string_literals;
+
+namespace {
+ const fs::path webserverConfigFilename{"./webserver.conf"};
+
+ const fs::path testConfigFilename{"./whiteboard.conf"};
+ const fs::path testDbFilename{"./whiteboard.db3"};
+}
+
+class WhiteboardTest: public ::testing::Test
+{
+protected:
+ WhiteboardTest(){
+ }
+
+ ~WhiteboardTest() override{
+ }
+
+ void SetUp() override
+ {
+ File::setFile(testConfigFilename, R"CONFIG(
+<config>
+ <port>::1:9876</port>
+ <datapath>.</datapath>
+ <maxage>2592000</maxage>
+ <threads>4</threads>
+ <maxconnections>3</maxconnections>
+</config>
+)CONFIG");
+ std::error_code ec;
+ fs::remove(testDbFilename, ec);
+
+ m_config = std::make_shared<Config>(testConfigFilename);
+
+ m_pid = fork();
+ if (m_pid == -1) {
+ throw std::runtime_error("Error on fork(): "s + strerror(errno));
+ } else if (m_pid == 0) { // child
+ Whiteboard whiteboard;
+ std::vector<std::string> argvv{{"whiteboard", "-c", testConfigFilename.generic_string()}};
+ char* argv[] = {argvv[0].data(), argvv[1].data(), argvv[2].data(), nullptr};
+ whiteboard.run(argvv.size(), argv);
+ exit(0);
+ }
+ Process::wait_for_pid_listening_on(m_pid, 9876);
+ std::this_thread::sleep_for(std::chrono::milliseconds(20));
+ }
+
+ void TearDown() override
+ {
+ if (m_pid == 0)
+ throw std::runtime_error("Whiteboard not running on requesting SIGTERM");
+
+ if (kill(m_pid, SIGTERM) != 0)
+ throw std::runtime_error("Unable to SIGTERM Whiteboard");
+
+ if (int result = waitpid(m_pid, NULL, 0); result != m_pid)
+ throw std::runtime_error("waitpid returned "s + std::to_string(result));
+
+ std::error_code ec;
+ fs::remove(testDbFilename, ec);
+ fs::remove(testConfigFilename, ec);
+ }
+
+ std::shared_ptr<Config> m_config;
+ pid_t m_pid{};
+};
+
+class WebsocketClient
+{
+public:
+ WebsocketClient()
+ {
+ std::string host = "::1";
+ auto const port = "9876" ;
+
+ // These objects perform our I/O
+ boost::asio::ip::tcp::resolver resolver{ioc_};
+ ws_ = std::make_unique<boost::beast::websocket::stream<boost::asio::ip::tcp::socket>>(ioc_);
+
+ // Look up the domain name
+ resolver_results_ = resolver.resolve(host, port);
+
+ connect();
+ handshake();
+ }
+
+ void connect()
+ {
+ // Make the connection on the IP address we get from a lookup
+ ep_ = boost::asio::connect(boost::beast::get_lowest_layer(*ws_), resolver_results_);
+ }
+
+ void handshake()
+ {
+ // 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
+ std::string host{"[::1]:9876"};
+
+ // 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, "/");
+
+ }
+
+ void write(const std::string& data)
+ {
+ ws_->write(boost::asio::buffer(data));
+ }
+
+ std::string read()
+ {
+ boost::beast::flat_buffer buffer;
+ ws_->read(buffer);
+ return {boost::asio::buffers_begin(buffer.data()), boost::asio::buffers_end(buffer.data())};
+ }
+
+ ~WebsocketClient()
+ {
+ }
+
+ bool is_open()
+ {
+ return ws_->is_open();
+ }
+
+private:
+ boost::asio::io_context ioc_;
+ boost::asio::ip::tcp::resolver::results_type resolver_results_;
+ std::unique_ptr<boost::beast::websocket::stream<boost::asio::ip::tcp::socket>> ws_;
+ boost::asio::ip::tcp::endpoint ep_;
+};
+
+//
+// tests via websocket server in separate process (hides coverage)
+//
+
+TEST_F(WhiteboardTest, websocket_server_connection)
+{
+ WebsocketClient wc;
+}
+
+TEST_F(WhiteboardTest, websocket_server_generate_id)
+{
+ WebsocketClient wc;
+
+ wc.write("<request><command>newid</command></request>");
+ std::string result0 {wc.read()};
+ ASSERT_TRUE(boost::algorithm::starts_with(result0, "<serverinfo><type>newid</type><id>"));
+ ASSERT_TRUE(boost::algorithm::ends_with(result0, "</id></serverinfo>"));
+ ASSERT_EQ(result0.size(), 58);
+
+ wc.write("<request><command>newid</command></request>");
+ std::string result1 {wc.read()};
+ ASSERT_TRUE(boost::algorithm::starts_with(result1, "<serverinfo><type>newid</type><id>"));
+ ASSERT_TRUE(boost::algorithm::ends_with(result1, "</id></serverinfo>"));
+ ASSERT_EQ(result1.size(), 58);
+
+ ASSERT_NE(result0, result1);
+}
+
+// check number of threads as configured
+TEST_F(WhiteboardTest, threads)
+{
+ ASSERT_GE(Process::number_of_threads(m_pid), 4);
+}
+
+TEST_F(WhiteboardTest, max_connections)
+{
+ WebsocketClient wc1;
+ std::this_thread::sleep_for(std::chrono::milliseconds(20));
+ ASSERT_TRUE(wc1.is_open());
+
+ WebsocketClient wc2;
+ std::this_thread::sleep_for(std::chrono::milliseconds(20));
+ ASSERT_TRUE(wc2.is_open());
+
+ WebsocketClient wc3;
+ std::this_thread::sleep_for(std::chrono::milliseconds(20));
+ ASSERT_TRUE(wc3.is_open());
+
+ ASSERT_THROW(WebsocketClient wc4, std::exception);
+}
+
+TEST_F(WhiteboardTest, id)
+{
+ WebsocketClient wc;
+
+ wc.write("<request><command>getfile</command><id>1</id></request>");
+ std::string result {wc.read()};
+
+ EXPECT_EQ(result, "<serverinfo><type>getfile</type><data/><revision>0</revision><pos>0</pos></serverinfo>");
+
+ wc.write("<request><command>getfile</command><id></id></request>");
+ result = wc.read();
+ EXPECT_EQ(result, "<serverinfo><type>error</type><message>Message handling error: Invalid id (empty)</message></serverinfo>");
+
+ wc.write("<request><command>getfile</command><id>01234567890123456789</id></request>");
+ result = wc.read();
+ EXPECT_EQ(result, "<serverinfo><type>error</type><message>Message handling error: Invalid id (size &gt; 16)</message></serverinfo>");
+
+ wc.write("<request><command>getfile</command><id>X</id></request>");
+ result = wc.read();
+ EXPECT_EQ(result, "<serverinfo><type>error</type><message>Message handling error: Invalid id char: X</message></serverinfo>");
+
+ wc.write("<request><command>getfile</command><id>a.</id></request>");
+ result = wc.read();
+ EXPECT_EQ(result, "<serverinfo><type>error</type><message>Message handling error: Invalid id char: .</message></serverinfo>");
+
+ wc.write("<request><command>getfile</command><id>a$b</id></request>");
+ result = wc.read();
+ EXPECT_EQ(result, "<serverinfo><type>error</type><message>Message handling error: Invalid id char: $</message></serverinfo>");
+}
+