]> git.poda.fr Git - newsserver.git/commitdiff
First commit. Server + test client + tokens + history master
authorJulien <None>
Mon, 21 Sep 2026 17:23:33 +0000 (19:23 +0200)
committerJulien <None>
Mon, 21 Sep 2026 17:23:33 +0000 (19:23 +0200)
19 files changed:
CMakeLists.txt [new file with mode: 0644]
src/CMakeLists.txt [new file with mode: 0644]
src/Logger.cpp [new file with mode: 0644]
src/Logger.hpp [new file with mode: 0644]
src/Message.hpp [new file with mode: 0644]
src/Server.cpp [new file with mode: 0644]
src/Server.hpp [new file with mode: 0644]
src/Socket.cpp [new file with mode: 0644]
src/Socket.hpp [new file with mode: 0644]
src/main.cpp [new file with mode: 0644]
tests/CMakeLists.txt [new file with mode: 0644]
tests/Client.cpp [new file with mode: 0644]
tests/Client.hpp [new file with mode: 0644]
tests/history.txt [new file with mode: 0644]
tests/history_clean.txt [new file with mode: 0644]
tests/testClient.cpp [new file with mode: 0644]
tests/testServer.cpp [new file with mode: 0644]
tests/testSocket.cpp [new file with mode: 0644]
tests/tokens.txt [new file with mode: 0644]

diff --git a/CMakeLists.txt b/CMakeLists.txt
new file mode 100644 (file)
index 0000000..92d7736
--- /dev/null
@@ -0,0 +1,18 @@
+cmake_minimum_required(VERSION 3.31)
+
+project(NewsServer CXX)
+
+include(CTest)
+
+add_library(${PROJECT_NAME}Objects OBJECT)
+target_compile_features(${PROJECT_NAME}Objects PUBLIC cxx_std_17)
+
+add_executable(${PROJECT_NAME})
+target_link_libraries(${PROJECT_NAME} PRIVATE ${PROJECT_NAME}Objects)
+set_target_properties(${PROJECT_NAME} PROPERTIES OUTPUT_NAME "newsserver")
+
+add_subdirectory(src)
+
+if (BUILD_TESTING)
+    add_subdirectory(tests)
+endif()
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
new file mode 100644 (file)
index 0000000..3b1f0e6
--- /dev/null
@@ -0,0 +1,12 @@
+target_include_directories(${PROJECT_NAME}Objects PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
+target_sources(${PROJECT_NAME}Objects PRIVATE
+    Logger.cpp
+    Logger.hpp
+    Message.hpp
+    Server.cpp
+    Server.hpp
+    Socket.cpp
+    Socket.hpp)
+
+target_sources(${PROJECT_NAME} PRIVATE
+    main.cpp)
diff --git a/src/Logger.cpp b/src/Logger.cpp
new file mode 100644 (file)
index 0000000..bd6bba5
--- /dev/null
@@ -0,0 +1,8 @@
+#include "Logger.hpp"
+
+#include <iostream>
+
+void Logger::log(const std::string& entity, const std::string& message) {
+    std::lock_guard lock(externalMutex_);
+    std::cout << entity << ": " << message << std::endl;
+}
diff --git a/src/Logger.hpp b/src/Logger.hpp
new file mode 100644 (file)
index 0000000..281b46a
--- /dev/null
@@ -0,0 +1,21 @@
+#pragma once
+
+#include <mutex>
+#include <string>
+
+class Logger {
+public:
+    Logger() = default;
+    ~Logger() = default;
+
+    Logger(const Logger&) = delete;
+    Logger& operator=(const Logger&) = delete;
+
+    Logger(Logger&&) = delete;
+    Logger& operator=(Logger&&) = delete;
+
+    void log(const std::string& entity, const std::string& message);
+
+private:
+    std::mutex externalMutex_;
+};
diff --git a/src/Message.hpp b/src/Message.hpp
new file mode 100644 (file)
index 0000000..5d2bd55
--- /dev/null
@@ -0,0 +1,43 @@
+#pragma once
+
+#pragma pack(push, 1)
+
+namespace message {
+enum class Type : std::uint8_t {
+    News = 0,
+    Reply = 1,
+    Login = 2,
+    Request = 3,
+};
+
+enum class ReplyCode : std::uint8_t {
+    OK = 0,
+    UnknownType = 1,
+    WrongLogin = 2,
+    NotLoggedIn = 3
+};
+
+struct Header {
+    Type type{};
+    std::uint16_t size{0};
+};
+
+struct News {
+    std::uint32_t timestamp{0};
+};
+
+struct Reply {
+    ReplyCode code{};
+};
+
+struct Login {
+    std::uint64_t token{0};
+};
+
+struct Request {
+    std::uint32_t firstMissing{0};
+    std::uint32_t lastMissing{0};
+};
+}
+
+#pragma pack(pop)
diff --git a/src/Server.cpp b/src/Server.cpp
new file mode 100644 (file)
index 0000000..6b91c63
--- /dev/null
@@ -0,0 +1,506 @@
+#include "Server.hpp"
+
+#include <cstring> // memcpy
+#include <endian.h>
+#include <sstream>
+#include <unordered_map>
+
+Server::Server(Logger& logger) : logger_(logger) {}
+
+Server::~Server() {
+    stop();
+}
+
+bool Server::init(const std::filesystem::path& tokenFilePath, const std::filesystem::path& historyFilePath) {
+    std::lock_guard lock(externalMutex_);
+
+    tokens_.clear();
+    news_.clear();
+    history_.close();
+
+    if (std::filesystem::exists(tokenFilePath)) {
+        if (std::filesystem::is_directory(tokenFilePath)) {
+            logger_.log("Server", "Token file path " + tokenFilePath.string() + " is a directory.");
+            return false;
+        }
+
+        std::ifstream tokenFile(tokenFilePath);
+
+        if (!tokenFile) {
+            logger_.log("Server", "Could not open token file " + tokenFilePath.string() + " to read.");
+            return false;
+        }
+
+        std::size_t lineN{0};
+
+        for (std::string line; std::getline(tokenFile, line);) {
+            lineN++;
+
+            std::uint64_t token{0};
+
+            try {
+                token = std::stoi(line);
+            }
+            catch (const std::exception& e) {
+                logger_.log("Server",
+                            "Skipping invalid token " + line + " line " + std::to_string(lineN) + '.');
+                continue;
+            }
+
+            tokens_.insert(token);
+
+            logger_.log("Server", "Added token " + line + '.');
+        }
+    }
+
+    if (tokens_.empty()) {
+        logger_.log("Server", "No tokkens were added.");
+        return false;
+    }
+
+    if (std::filesystem::exists(historyFilePath)) {
+        if (std::filesystem::is_directory(historyFilePath)) {
+            logger_.log("Server", "History file path " + historyFilePath.string() + " is a directory.");
+            return false;
+        }
+
+        std::ifstream historyFile(historyFilePath);
+
+        if (!historyFile) {
+            logger_.log("Server", "Could not open history file " + historyFilePath.string() + " to read.");
+            return false;
+        }
+
+        std::size_t lineN{0};
+
+        for (std::string line; std::getline(historyFile, line);) {
+            lineN++;
+            std::size_t linePos = line.find('|');
+
+            if (linePos == std::string::npos or linePos == 0) {
+                logger_.log("Server", "Skipping invalid line " + std::to_string(lineN) + " in history file.");
+                continue;
+            }
+
+            std::string timestampStr = line.substr(0, linePos);
+            std::string news = line.substr(linePos + 1);
+            std::uint32_t timestamp{0};
+
+            try {
+                timestamp = std::stoi(timestampStr);
+            }
+            catch (const std::exception& e) {
+                logger_.log("Server",
+                            "Skipping invalid timestamp " + timestampStr + " line " + std::to_string(lineN) + '.');
+                continue;
+            }
+
+            if (news.empty()) {
+                logger_.log("Server", "Skipping empty news line " + std::to_string(lineN) + '.');
+            }
+
+            auto const result = news_.try_emplace(timestamp, std::move(news));
+
+            if (!result.second) {
+                logger_.log("Server", "Skipping duplicated timestamp " + std::to_string(timestamp) + '.');
+                continue;
+            }
+
+            logger_.log("Server",
+                        "Added timestamp " + std::to_string(result.first->first) + " news " + result.first->second);
+        }
+    }
+
+    history_.open(historyFilePath, std::ios::binary | std::ios_base::app);
+
+    if (!history_) {
+        logger_.log("Server", "Could not open history file " + historyFilePath.string() + " to write.");
+        return false;
+    }
+
+    return true;
+}
+
+bool Server::addNews(const std::uint32_t timestamp, std::string news) {
+    std::lock_guard lock(externalMutex_);
+
+    if (news.empty()) {
+        logger_.log("Server", "Could not add empty news.");
+        return false;
+    }
+
+    {
+    std::lock_guard lock(newsMutex_);
+    auto const result = news_.try_emplace(timestamp, news);
+
+    if (!result.second) {
+        logger_.log("Server", "Could not add news for existing timestamp " + std::to_string(timestamp) + '.');
+        return false;
+    }
+    }
+
+    history_ << timestamp << '|' << news << '\n';
+
+    {
+    std::lock_guard lock(queueMutex_);
+    sendQueue_.push_back({timestamp, news});
+    }
+
+    logger_.log("Server", "Added timestamp " + std::to_string(timestamp) + " news " + news);
+    return true;
+}
+
+bool Server::start(const std::uint16_t port) {
+    std::lock_guard lock(externalMutex_);
+
+    if (running_) {
+        logger_.log("Server", "Already running.");
+        return false;
+    }
+
+    running_ = true;
+    socketThread_ = std::thread([this, port](){ runThread(port); });
+
+    logger_.log("Server", "Started with port " + std::to_string(port) + '.');
+    return true;
+}
+
+void Server::stop() {
+    std::lock_guard lock(externalMutex_);
+
+    if (!running_) {
+        return;
+    }
+
+    logger_.log("Server", "Stop");
+
+    stopSignal_ = true;
+
+    if (socketThread_.joinable()) {
+        socketThread_.join();
+    }
+
+    stopSignal_ = false;
+    running_ = false;
+}
+
+bool Server::listening() const {
+    return listening_;
+}
+
+void Server::runThread(const std::uint16_t port)
+{
+    Socket serverSocket(logger_);
+
+    if (!serverSocket.init()) {
+        return;
+    }
+
+    if (!serverSocket.listen(port)) {
+        return;
+    }
+
+    SocketPoller poller(logger_);
+
+    if (!poller.init()) {
+        return;
+    }
+
+    if (!poller.add(serverSocket)) {
+        return;
+    }
+
+    listening_ = true;
+
+    std::unordered_map<std::uint32_t, Client> clients;
+
+    while (!stopSignal_) {
+        std::unordered_set<std::uint32_t> pollResult;
+
+        // Poll incoming events.
+        if (!poller.poll(pollResult)) {
+            break;
+        }
+
+        for (const auto fd : pollResult) {
+            // Event from listener socket.
+            if (fd == serverSocket.fileDescriptor()) {
+                Client newClient;
+                newClient.socket = std::make_unique<Socket>(logger_);
+
+                if (!serverSocket.accept(*newClient.socket)) {
+                    continue;
+                }
+
+                if (!newClient.socket->isOpen()) {
+                    continue;
+                }
+
+                if (!poller.add(*newClient.socket)) {
+                    continue;
+                }
+
+                logger_.log("Server " + std::to_string(serverSocket.fileDescriptor()),
+                            "Added client " + std::to_string(newClient.socket->fileDescriptor()) + '.');
+                clients.try_emplace(newClient.socket->fileDescriptor(), std::move(newClient));
+            }
+            // Event from a client socket.
+            else {
+                auto it = clients.find(fd);
+
+                if (it == clients.end()) {
+                    logger_.log("Server " + std::to_string(serverSocket.fileDescriptor()),
+                                "Received data from unknown source " + std::to_string(fd) + '.');
+                    continue;
+                }
+
+                if (!handleMessage(it->second)) {
+                    poller.remove(*it->second.socket);
+                    clients.erase(it);
+                }
+            }
+        } // /for each polled event.
+
+
+        // Send news from queue.
+        decltype(sendQueue_) sendQueue;
+
+        {
+        std::lock_guard lock(queueMutex_);
+        sendQueue.swap(sendQueue_);
+        }
+
+        for (auto const& [timestamp, news] : sendQueue) {
+            message::News n;
+            n.timestamp = htobe32(timestamp);
+
+            message::Header h;
+            h.type = message::Type::News;
+            h.size = htobe16(sizeof h + sizeof n + news.size());
+
+            std::vector<std::byte> buffer;
+            buffer.reserve(sizeof h + sizeof n + news.size());
+            buffer.insert(buffer.end(),
+                          reinterpret_cast<const std::byte*>(&h),
+                          reinterpret_cast<const std::byte*>(&h) + sizeof h);
+            buffer.insert(buffer.end(),
+                          reinterpret_cast<const std::byte*>(&n),
+                          reinterpret_cast<const std::byte*>(&n) + sizeof n);
+            buffer.insert(buffer.end(),
+                          reinterpret_cast<const std::byte*>(news.data()),
+                          reinterpret_cast<const std::byte*>(news.data()) + news.size());
+
+            // Send to all logged in clients.
+            for (auto& [fd, client] : clients) {
+                if (client.loggedIn && !client.socket->write(buffer)) {
+                    poller.remove(*client.socket);
+                    clients.erase(fd);
+                    continue;
+                }
+
+                logger_.log("Server " + std::to_string(serverSocket.fileDescriptor()),
+                            "Sent news timestamp " + std::to_string(timestamp)
+                            + " for client " + std::to_string(fd) + '.');
+            }
+        }
+    } // /while stop signal is false.
+
+    listening_ = false;
+}
+
+bool Server::handleMessage(Client& client) {
+    if (client.socket == nullptr) {
+        return false;
+    }
+
+    std::vector<std::byte> buffer;
+    buffer.resize(sizeof(message::Header));
+
+    if (!client.socket->read(buffer)) {
+        return false;
+    }
+
+    // TODO
+    if (buffer.size() < sizeof(message::Header)) {
+        logger_.log("Server",
+                    "ZERO READ " + std::to_string(client.socket->fileDescriptor()));
+        return true;
+    }
+
+    message::Header h;
+    std::memcpy(&h, buffer.data(), sizeof h);
+    h.size = be16toh(h.size);
+
+    logger_.log("Server",
+                "Received message type " + std::to_string(static_cast<std::uint16_t>(h.type))
+                + " size " + std::to_string(h.size)
+                + " from client " + std::to_string(client.socket->fileDescriptor()) + '.');
+
+    switch (h.type) {
+        case message::Type::Login: {
+            buffer.resize(h.size - sizeof h);
+
+            if (!client.socket->read(buffer)) {
+                return false;
+            }
+
+            message::Login l;
+            std::memcpy(&l, buffer.data(), sizeof l);
+            l.token = be64toh(l.token);
+
+            if (!handleLogin(client, l)) {
+                return false;
+            }
+        } break;
+        case message::Type::Request: {
+            buffer.resize(h.size - sizeof h);
+
+            if (!client.socket->read(buffer)) {
+                return false;
+            }
+
+            message::Request r;
+            std::memcpy(&r, buffer.data(), sizeof r);
+            r.firstMissing = be32toh(r.firstMissing);
+            r.lastMissing = be32toh(r.lastMissing);
+
+            if (!handleRequest(client, r)) {
+                return false;
+            }
+        } break;
+        default: {
+            buffer.resize(h.size - sizeof h);
+
+            if (!client.socket->read(buffer)) {
+                return false;
+            }
+
+            if (!handleUnknownMessage(client, h)) {
+                return false;
+            }
+
+        } break;
+    } // /switch message type.
+
+    return true;
+}
+
+bool Server::handleUnknownMessage(Client& client, const message::Header& message) {
+    logger_.log("Server",
+                "Received unknown message type " + std::to_string(static_cast<std::uint16_t>(message.type))
+                + " size " + std::to_string(message.size)
+                + " from client " + std::to_string(client.socket->fileDescriptor()) + '.');
+
+    message::Reply r;
+    r.code = message::ReplyCode::UnknownType;
+
+    message::Header h;
+    h.type = message::Type::Reply;
+    h.size = htobe16(sizeof h + sizeof r);
+
+    std::vector<std::byte> buffer;
+    buffer.reserve(sizeof h + sizeof r);
+    buffer.insert(buffer.end(),
+                  reinterpret_cast<const std::byte*>(&h),
+                  reinterpret_cast<const std::byte*>(&h) + sizeof h);
+    buffer.insert(buffer.end(),
+                  reinterpret_cast<const std::byte*>(&r),
+                  reinterpret_cast<const std::byte*>(&r) + sizeof r);
+
+    if (!client.socket->write(buffer)) {
+        return false;
+    }
+
+    return true;
+}
+
+bool Server::handleLogin(Client& client, const message::Login& message) {
+    logger_.log("Server",
+                "Received login with token " + std::to_string(message.token)
+                + " from client " + std::to_string(client.socket->fileDescriptor()) + '.');
+
+    bool found = tokens_.find(message.token) != tokens_.cend();
+
+    message::Reply r;
+
+    if (!found) {
+        r.code = message::ReplyCode::WrongLogin;
+    } else {
+        r.code = message::ReplyCode::OK;
+    }
+
+    message::Header h;
+    h.type = message::Type::Reply;
+    h.size = htobe16(sizeof h + sizeof r);
+
+    std::vector<std::byte> buffer;
+    buffer.reserve(sizeof h + sizeof r);
+    buffer.insert(buffer.end(),
+                  reinterpret_cast<const std::byte*>(&h),
+                  reinterpret_cast<const std::byte*>(&h) + sizeof h);
+    buffer.insert(buffer.end(),
+                  reinterpret_cast<const std::byte*>(&r),
+                  reinterpret_cast<const std::byte*>(&r) + sizeof r);
+
+    if (!client.socket->write(buffer)) {
+        return false;
+    }
+
+    if (!found) {
+        logger_.log("Server",
+                    "Rejected login with token " + std::to_string(message.token)
+                    + " from client " + std::to_string(client.socket->fileDescriptor()) + '.');
+        return true;
+    }
+
+    client.loggedIn = true;
+    logger_.log("Server",
+                "Client " + std::to_string(client.socket->fileDescriptor())
+                + " logged in with token " + std::to_string(message.token) + '.');
+
+    return true;
+}
+
+bool Server::handleRequest(Client& client, const message::Request& message) {
+    logger_.log("Server",
+                "Received request " + std::to_string(static_cast<std::uint16_t>(message.firstMissing))
+                + '-' + std::to_string(static_cast<std::uint16_t>(message.lastMissing))
+                + " from client " + std::to_string(client.socket->fileDescriptor()) + '.');
+
+    message::Reply r;
+
+    if (!client.loggedIn) {
+        r.code = message::ReplyCode::NotLoggedIn;
+    } else {
+        r.code = message::ReplyCode::OK;
+    }
+
+    message::Header h;
+    h.type = message::Type::Reply;
+    h.size = htobe16(sizeof h + sizeof r);
+
+    std::vector<std::byte> buffer;
+    buffer.reserve(sizeof h + sizeof r);
+    buffer.insert(buffer.end(),
+                  reinterpret_cast<const std::byte*>(&h),
+                  reinterpret_cast<const std::byte*>(&h) + sizeof h);
+    buffer.insert(buffer.end(),
+                  reinterpret_cast<const std::byte*>(&r),
+                  reinterpret_cast<const std::byte*>(&r) + sizeof r);
+
+    if (!client.socket->write(buffer)) {
+        return false;
+    }
+
+    if (!client.loggedIn) {
+        logger_.log("Server",
+                    "Rejected request " + std::to_string(static_cast<std::uint16_t>(message.firstMissing))
+                    + '-' + std::to_string(static_cast<std::uint16_t>(message.lastMissing))
+                    + " to unauthentified client " + std::to_string(client.socket->fileDescriptor()) + '.');
+        return true;
+    }
+
+    //TODO: SEND HISTORY.
+
+    return true;
+}
diff --git a/src/Server.hpp b/src/Server.hpp
new file mode 100644 (file)
index 0000000..8524512
--- /dev/null
@@ -0,0 +1,66 @@
+#pragma once
+
+#include <atomic>
+#include <cstdint>
+#include <deque>
+#include <filesystem>
+#include <fstream>
+#include <map>
+#include <memory>
+#include <mutex>
+#include <string>
+#include <thread>
+#include <unordered_set>
+#include <vector>
+
+#include "Logger.hpp"
+#include "Message.hpp"
+#include "Socket.hpp"
+
+class Server {
+public:
+    Server(Logger& logger);
+    ~Server();
+
+    Server(const Server&) = delete;
+    Server& operator=(const Server&) = delete;
+
+    Server(Server&&) = delete;
+    Server& operator=(Server&&) = delete;
+
+    bool init(const std::filesystem::path& tokenFilePath, const std::filesystem::path& historyFilePath);
+    bool addNews(const std::uint32_t timestamp, std::string news);
+    bool start(const std::uint16_t port);
+    void stop();
+
+    bool listening() const;
+
+private:
+    struct Client {
+        std::unique_ptr<Socket> socket;
+        bool loggedIn{false};
+    };
+
+    Logger& logger_;
+    std::mutex externalMutex_;
+
+    std::unordered_set<std::uint64_t> tokens_;
+
+    std::ofstream history_;
+    std::map<std::uint32_t, std::string> news_;
+    std::mutex newsMutex_;
+
+    std::deque<std::pair<std::uint32_t, std::string>> sendQueue_;
+    std::mutex queueMutex_;
+
+    std::thread socketThread_;
+    std::atomic<bool> running_{false};
+    std::atomic<bool> listening_{false};
+    std::atomic<bool> stopSignal_{false};
+
+    void runThread(const std::uint16_t port);
+    bool handleMessage(Client& client);
+    bool handleUnknownMessage(Client& client, const message::Header& message);
+    bool handleLogin(Client& client, const message::Login& message);
+    bool handleRequest(Client& client, const message::Request& message);
+};
diff --git a/src/Socket.cpp b/src/Socket.cpp
new file mode 100644 (file)
index 0000000..94f702c
--- /dev/null
@@ -0,0 +1,299 @@
+#include "Socket.hpp"
+
+#include <array>
+#include <cstring>
+#include <endian.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <unistd.h> // close
+#include <arpa/inet.h> // inet_pton inet_ntop
+#include <netinet/in.h> // sockaddr_in
+#include <sys/epoll.h>
+#include <sys/socket.h> // listen
+
+Socket::Socket(Logger& logger) : logger_(logger) {}
+
+Socket::~Socket() {
+    if (open_) {
+        close(fileDescriptor_);
+        logger_.log("Socket " + std::to_string(fileDescriptor_), "Closed.");
+    }
+}
+
+bool Socket::init() {
+    if (open_) {
+        close(fileDescriptor_);
+        open_ = false;
+        logger_.log("Socket " + std::to_string(fileDescriptor_), "Closed.");
+        fileDescriptor_ = 0;
+    }
+
+    std::int32_t fd = socket(AF_INET /* ipv4 */, SOCK_STREAM /* tcp */, 0 /* single protocol */);
+
+    if (fd < 0) {
+        logger_.log("Socket", "Could not create socket. Error code " + std::to_string(errno) + ": " + strerror(errno));
+        return false;
+    }
+
+    fileDescriptor_ = fd;
+    open_ = true;
+    logger_.log("Socket " + std::to_string(fileDescriptor_), "Initialized.");
+    return true;
+}
+
+void Socket::init(const std::uint32_t fileDescriptor) {
+    fileDescriptor_ = fileDescriptor;
+    open_ = true;
+}
+
+bool Socket::listen(const std::uint16_t port) {
+    if (!open_) {
+        logger_.log("Socket", "Not open.");
+        return false;
+    }
+
+    if (port == 0) {
+        logger_.log("Socket " + std::to_string(fileDescriptor_), "Invalid port value " + std::to_string(port) + '.');
+        return false;
+    }
+
+    sockaddr_in socketAddress{};
+    socketAddress.sin_addr.s_addr = INADDR_ANY; // Any incoming address from any interface.
+    socketAddress.sin_family = AF_INET; // ipv4.
+    socketAddress.sin_port = htobe16(port);
+
+    // Enable fast reusable address for this socket file descriptor used to listen to incoming connections.
+    std::uint32_t optValue = 1;
+    setsockopt(fileDescriptor_, SOL_SOCKET, SO_REUSEADDR, &optValue, sizeof optValue);
+    // Set non waiting.
+    fcntl(fileDescriptor_, F_SETFL, O_NONBLOCK);
+
+    std::int32_t bindResult = bind(fileDescriptor_, reinterpret_cast<sockaddr*>(&socketAddress), sizeof socketAddress);
+
+    if (bindResult < 0) {
+        logger_.log("Socket " + std::to_string(fileDescriptor_),
+                    "Could not bind socket " + std::to_string(fileDescriptor_) + " to port " + std::to_string(port)
+                    + ". Error code " + std::to_string(errno) + ": " + strerror(errno));
+        return false;
+    }
+
+    std::int32_t listenResult = ::listen(fileDescriptor_, SOMAXCONN);
+
+    if (listenResult < 0) {
+        logger_.log("Socket " + std::to_string(fileDescriptor_),
+                    "Could not listen with socket " + std::to_string(fileDescriptor_)
+                    + " on port " + std::to_string(port)
+                    + ". Error code " + std::to_string(errno) + ": " + strerror(errno));
+        return false;
+    }
+
+    logger_.log("Socket " + std::to_string(fileDescriptor_), "Listening to port " + std::to_string(port) + '.');
+    return true;
+}
+
+bool Socket::accept(Socket& client) {
+    sockaddr_in clientSocketAddress{};
+    socklen_t clientSocketAddressSize = sizeof clientSocketAddress;
+
+    std::int32_t fd = ::accept(fileDescriptor_,
+                               reinterpret_cast<sockaddr*>(&clientSocketAddress),
+                               &clientSocketAddressSize);
+
+    if (fd < 0) {
+        // No new client to accept.
+        if (errno == EAGAIN or errno == EWOULDBLOCK) {
+            return true;
+        }
+
+        logger_.log("Socket " + std::to_string(fileDescriptor_),
+                    "Could not create client socket. Error code " + std::to_string(errno) + ": " + strerror(errno));
+        return false;
+    }
+
+    client.init(fd);
+
+    char s[INET_ADDRSTRLEN];
+    inet_ntop(AF_INET, &clientSocketAddress.sin_addr, s, INET_ADDRSTRLEN);
+
+    logger_.log("Socket " + std::to_string(fileDescriptor_), "Accepted client socket " + std::to_string(fd)
+                + " from " + std::string(s) + ':'
+                + std::to_string(be16toh(clientSocketAddress.sin_port)) + '.');
+    return true;
+}
+
+bool Socket::connect(const std::string& address, const std::uint16_t port) {
+    if (!open_) {
+        logger_.log("Socket", "Not open.");
+        return false;
+    }
+
+    sockaddr_in socketAddress{};
+
+    if (std::int32_t r = inet_pton(AF_INET, address.c_str(), &socketAddress.sin_addr) <= 0) {
+        logger_.log("Socket " + std::to_string(fileDescriptor_),
+                    "Could not resolve address " + address + ". Error code " + std::to_string(r) + '.');
+        return false;
+    }
+
+    if (port == 0) {
+        logger_.log("Socket " + std::to_string(fileDescriptor_), "Invalid port value " + std::to_string(port) + '.');
+        return false;
+    }
+
+    socketAddress.sin_family = AF_INET;
+    socketAddress.sin_port = htobe16(port);
+
+    std::int32_t connectResult = ::connect(fileDescriptor_,
+                                           reinterpret_cast<sockaddr*>(&socketAddress),
+                                           sizeof socketAddress);
+
+    if (connectResult != 0) {
+        logger_.log("Socket " + std::to_string(fileDescriptor_),
+                    "Could not connect socket to " + address + ':' + std::to_string(port)
+                    + ". Error code " + std::to_string(errno) + ": " + strerror(errno) + '.');
+        return false;
+    }
+
+    logger_.log("Socket " + std::to_string(fileDescriptor_),
+                "Connected to " + address + ':' + std::to_string(port) + '.');
+    return true;
+}
+
+bool Socket::read(std::vector<std::byte>& buffer) {
+    std::int32_t r = ::read(fileDescriptor_, buffer.data(), buffer.size());
+
+    if (r < 0) {
+        logger_.log("Socket " + std::to_string(fileDescriptor_),
+                    "Could not read. Error code " + std::to_string(errno) + ": " + strerror(errno) + '.');
+        return false;
+    }
+
+    buffer.resize(r);
+    logger_.log("Socket " + std::to_string(fileDescriptor_), "Read " + std::to_string(r) + " bytes.");
+    return true;
+}
+
+bool Socket::write(const std::vector<std::byte>& buffer) {
+    std::int32_t r = ::write(fileDescriptor_, buffer.data(), buffer.size());
+
+    if (r < 0) {
+        logger_.log("Socket " + std::to_string(fileDescriptor_),
+                    "Could not write. Error code " + std::to_string(errno) + '.');
+        return false;
+    }
+
+    if (r < buffer.size()) {
+        logger_.log("Socket " + std::to_string(fileDescriptor_),
+                    "Could not write all data. " + std::to_string(r) + '/' + std::to_string(buffer.size()) + '.');
+        return false;
+    }
+
+    logger_.log("Socket " + std::to_string(fileDescriptor_), "Wrote " + std::to_string(r) + " bytes.");
+    return true;
+}
+
+bool Socket::isOpen() const {
+    return open_;
+}
+
+std::uint32_t Socket::fileDescriptor() const {
+    return fileDescriptor_;
+}
+
+SocketPoller::SocketPoller(Logger& logger) : logger_(logger) {}
+
+SocketPoller::~SocketPoller() {
+    if (open_) {
+        close(fileDescriptor_);
+        logger_.log("SocketPoller " + std::to_string(fileDescriptor_), "Closed.");
+    }
+}
+
+bool SocketPoller::init() {
+    if (open_) {
+        close(fileDescriptor_);
+        open_ = false;
+        logger_.log("SocketPoller " + std::to_string(fileDescriptor_), "Closed.");
+        fileDescriptor_ = 0;
+    }
+
+    std::int32_t fd = epoll_create1(0);
+
+    if (fd < 0) {
+        logger_.log("SocketPoller",
+                    "Could not create poll. Error code " + std::to_string(errno) + ": " + strerror(errno));
+        return false;
+    }
+
+    fileDescriptor_ = fd;
+    open_ = true;
+    logger_.log("SocketPoller " + std::to_string(fileDescriptor_), "Initialized.");
+    return true;
+}
+
+bool SocketPoller::add(const Socket& socket) {
+    if (!open_) {
+        logger_.log("SocketPoller", "Not open.");
+        return false;
+    }
+
+    epoll_event ev{};
+    ev.events = EPOLLIN;
+    ev.data.fd = socket.fileDescriptor();
+
+    std::int32_t r = epoll_ctl(fileDescriptor_, EPOLL_CTL_ADD, socket.fileDescriptor(), &ev);
+
+    if (r < 0) {
+        logger_.log("SocketPoller " + std::to_string(fileDescriptor_),
+                    "Could not add socket " + std::to_string(socket.fileDescriptor())
+                    + ". Error code " + std::to_string(errno) + ": " + strerror(errno) + '.');
+        return false;
+    }
+
+    logger_.log("SocketPoller " + std::to_string(fileDescriptor_),
+                "Added socket " + std::to_string(socket.fileDescriptor()) + '.');
+    return true;
+}
+
+void SocketPoller::remove(const Socket& socket) {
+    if (!open_) {
+        return;
+    }
+
+    std::int32_t r = epoll_ctl(fileDescriptor_, EPOLL_CTL_DEL, socket.fileDescriptor(), nullptr);
+
+    if (r < 0) {
+        logger_.log("SocketPoller " + std::to_string(fileDescriptor_),
+                    "Could not remove socket " + std::to_string(socket.fileDescriptor())
+                    + ". Error code " + std::to_string(errno) + ": " + strerror(errno) + '.');
+        return;
+    }
+
+    logger_.log("SocketPoller " + std::to_string(fileDescriptor_),
+                "Removed socket " + std::to_string(socket.fileDescriptor()) + '.');
+}
+
+bool SocketPoller::poll(std::unordered_set<std::uint32_t>& fileDescriptors) {
+    if (!open_) {
+        logger_.log("SocketPoller", "Not open.");
+        return false;
+    }
+
+    std::array<epoll_event, 100 /* max events */> events{};
+
+    std::int32_t r = epoll_wait(fileDescriptor_, events.data(), events.size(), 100 /* timeout in ms */);
+
+    if (r < 0) {
+        logger_.log("SocketPoller " + std::to_string(fileDescriptor_),
+                    "Could not poll. Error code " + std::to_string(errno) + ": " + strerror(errno) + '.');
+        return false;
+    }
+
+    for (std::int32_t i(0); i < r ; ++i) {
+        logger_.log("SocketPoller " + std::to_string(fileDescriptor_),
+                    "New event from socket " + std::to_string(events[i].data.fd) + '.');
+        fileDescriptors.insert(events[i].data.fd);
+    }
+
+    return true;
+}
diff --git a/src/Socket.hpp b/src/Socket.hpp
new file mode 100644 (file)
index 0000000..e946724
--- /dev/null
@@ -0,0 +1,63 @@
+#pragma once
+
+#include <cstdint>
+#include <string>
+#include <vector>
+#include <unordered_set>
+
+#include "Logger.hpp"
+
+class Socket {
+public:
+    Socket(Logger& logger);
+    ~Socket();
+
+    Socket(const Socket&) = delete;
+    Socket& operator=(const Socket&) = delete;
+
+    Socket(Socket&&) = delete;
+    Socket& operator=(Socket&&) = delete;
+
+    bool init();
+    void init(const std::uint32_t fileDescriptor);
+
+    bool listen(const std::uint16_t port);
+    bool accept(Socket& client);
+    bool connect(const std::string& address, const std::uint16_t port);
+
+    bool read(std::vector<std::byte>& buffer);
+    bool write(const std::vector<std::byte>& buffer);
+
+    bool isOpen() const;
+    std::uint32_t fileDescriptor() const;
+
+private:
+    Logger& logger_;
+
+    bool open_{false};
+    std::uint32_t fileDescriptor_{0};
+};
+
+class SocketPoller {
+public:
+    SocketPoller(Logger& logger);
+    ~SocketPoller();
+
+    SocketPoller(const SocketPoller&) = delete;
+    SocketPoller& operator=(const SocketPoller&) = delete;
+
+    SocketPoller(SocketPoller&&) = delete;
+    SocketPoller& operator=(SocketPoller&&) = delete;
+
+    bool init();
+
+    bool add(const Socket& socket);
+    void remove(const Socket& socket);
+    bool poll(std::unordered_set<std::uint32_t>& fileDescriptors);
+
+private:
+    Logger& logger_;
+
+    bool open_{false};
+    std::uint32_t fileDescriptor_{0};
+};
diff --git a/src/main.cpp b/src/main.cpp
new file mode 100644 (file)
index 0000000..ae7c94d
--- /dev/null
@@ -0,0 +1,10 @@
+#include "Logger.hpp"
+#include "Server.hpp"
+
+int main()
+{
+    Logger logger;
+    Server server(logger);
+
+    return 0;
+}
diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt
new file mode 100644 (file)
index 0000000..0db8ace
--- /dev/null
@@ -0,0 +1,39 @@
+find_package(GTest REQUIRED)
+
+configure_file(${CMAKE_CURRENT_SOURCE_DIR}/history.txt ${CMAKE_CURRENT_BINARY_DIR}/history_ref.txt COPYONLY)
+configure_file(${CMAKE_CURRENT_SOURCE_DIR}/history_clean.txt ${CMAKE_CURRENT_BINARY_DIR}/history_clean_ref.txt COPYONLY)
+
+add_library(${PROJECT_NAME}Client OBJECT)
+target_sources(${PROJECT_NAME}Client PRIVATE
+    Client.cpp
+    Client.hpp)
+target_link_libraries(${PROJECT_NAME}Client PUBLIC
+    ${PROJECT_NAME}Objects)
+
+add_executable(${PROJECT_NAME}TestClient)
+target_sources(${PROJECT_NAME}TestClient PRIVATE testClient.cpp)
+target_link_libraries(${PROJECT_NAME}TestClient PRIVATE
+    GTest::gtest_main
+    ${PROJECT_NAME}Client
+    ${PROJECT_NAME}Objects)
+set_target_properties(${PROJECT_NAME}TestClient PROPERTIES OUTPUT_NAME "testclient")
+add_test(NAME ${PROJECT_NAME}TestClient COMMAND testclient)
+
+add_executable(${PROJECT_NAME}TestServer)
+target_compile_definitions(${PROJECT_NAME}TestServer PRIVATE SOURCE_DIR="${CMAKE_CURRENT_SOURCE_DIR}")
+target_compile_definitions(${PROJECT_NAME}TestServer PRIVATE BINARY_DIR="${CMAKE_CURRENT_BINARY_DIR}")
+target_sources(${PROJECT_NAME}TestServer PRIVATE testServer.cpp)
+target_link_libraries(${PROJECT_NAME}TestServer PRIVATE
+    GTest::gtest_main
+    ${PROJECT_NAME}Client
+    ${PROJECT_NAME}Objects)
+set_target_properties(${PROJECT_NAME}TestServer PROPERTIES OUTPUT_NAME "testserver")
+add_test(NAME ${PROJECT_NAME}TestServer COMMAND testserver)
+
+add_executable(${PROJECT_NAME}TestSocket)
+target_sources(${PROJECT_NAME}TestSocket PRIVATE testSocket.cpp)
+target_link_libraries(${PROJECT_NAME}TestSocket PRIVATE
+    GTest::gtest_main
+    ${PROJECT_NAME}Objects)
+set_target_properties(${PROJECT_NAME}TestSocket PROPERTIES OUTPUT_NAME "testsocket")
+add_test(NAME ${PROJECT_NAME}TestSocket COMMAND testsocket)
diff --git a/tests/Client.cpp b/tests/Client.cpp
new file mode 100644 (file)
index 0000000..db6e9e0
--- /dev/null
@@ -0,0 +1,285 @@
+#include "Client.hpp"
+
+#include <cstring> // memcpy
+#include <endian.h>
+
+#include "Message.hpp"
+
+Client::Client(Logger& logger) : logger_(logger), socket_(logger_) {}
+
+bool Client::init() {
+    if (!socket_.init()) {
+        return false;
+    }
+
+    logger_.log("Client", "Created with socket " + std::to_string(socket_.fileDescriptor()) + '.');
+    return true;
+}
+
+bool Client::connect(const std::string& ip, const std::uint16_t port) {
+    if (!socket_.connect(ip, port)) {
+        return false;
+    }
+
+    logger_.log("Client " + std::to_string(socket_.fileDescriptor()),
+                "Connected to " + ip + ":" + std::to_string(port) + '.');
+    return true;
+}
+
+bool Client::read() {
+    std::vector<std::byte> buffer;
+    buffer.resize(sizeof(message::Header));
+
+    if (!socket_.read(buffer)) {
+        return false;
+    }
+
+    message::Header h;
+    std::memcpy(&h, buffer.data(), sizeof h);
+    h.size = be16toh(h.size);
+
+    logger_.log("Client " + std::to_string(socket_.fileDescriptor()),
+                "Received message type " + std::to_string(static_cast<std::uint16_t>(h.type))
+                + " size " + std::to_string(h.size) + '.');
+
+    switch (h.type) {
+    case message::Type::News: {
+        buffer.resize(h.size - sizeof h);
+
+        if (!socket_.read(buffer)) {
+            return false;
+        }
+
+        message::News n;
+        std::memcpy(&n, buffer.data(), sizeof n);
+        n.timestamp = be32toh(n.timestamp);
+
+        std::string news(buffer.begin() + sizeof n, buffer.end());
+
+        history_.try_emplace(n.timestamp, news);
+
+        logger_.log("Client " + std::to_string(socket_.fileDescriptor()),
+                    "Received timestamp " + std::to_string(n.timestamp) + " news " + news);
+    } break;
+    case message::Type::Reply: {
+        buffer.resize(h.size - sizeof h);
+
+        if (!socket_.read(buffer)) {
+            return false;
+        }
+
+        message::Reply r;
+        std::memcpy(&r, buffer.data(), sizeof r);
+
+        logger_.log("Client " + std::to_string(socket_.fileDescriptor()),
+                    "Received reply code " + std::to_string(static_cast<std::uint16_t>(r.code)) + '.');
+    } break;
+    default: {
+        logger_.log("Client " + std::to_string(socket_.fileDescriptor()),
+                    "Received unknown message type " + std::to_string(static_cast<std::uint16_t>(h.type))
+                    + " size " + std::to_string(h.size) + '.');
+        return false;
+    } break;
+    }
+
+    return true;
+}
+
+bool Client::unknown() {
+    message::Reply su;
+
+    message::Header sh;
+    sh.type = message::Type::Reply;
+    sh.size = htobe16(sizeof sh + sizeof su);
+
+    std::vector<std::byte> buffer;
+    buffer.reserve(sizeof sh + sizeof su);
+    buffer.insert(buffer.end(),
+                  reinterpret_cast<const std::byte*>(&sh),
+                  reinterpret_cast<const std::byte*>(&sh) + sizeof sh);
+    buffer.insert(buffer.end(),
+                  reinterpret_cast<const std::byte*>(&su),
+                  reinterpret_cast<const std::byte*>(&su) + sizeof su);
+
+    if (!socket_.write(buffer)) {
+        return false;
+    }
+
+    buffer.resize(sizeof(message::Header));
+
+    if (!socket_.read(buffer)) {
+        return false;
+    }
+
+    if (buffer.size() < sizeof(message::Header)) {
+        return false;
+    }
+
+    message::Header rh;
+    std::memcpy(&rh, buffer.data(), sizeof rh);
+    rh.size = be16toh(rh.size);
+
+    if (rh.type != message::Type::Reply) {
+        logger_.log("Client " + std::to_string(socket_.fileDescriptor()),
+                    "Received message type " + std::to_string(static_cast<std::uint16_t>(rh.type)) + '.');
+        return false;
+    }
+
+    buffer.resize(rh.size - sizeof rh);
+
+    if (!socket_.read(buffer)) {
+        return false;
+    }
+
+    if (buffer.size() < sizeof(message::Reply)) {
+        return false;
+    }
+
+    message::Reply rr;
+    std::memcpy(&rr, buffer.data(), sizeof rr);
+
+    if (rr.code != message::ReplyCode::UnknownType) {
+        logger_.log("Client " + std::to_string(socket_.fileDescriptor()),
+                    "Received reply error code " + std::to_string(static_cast<std::uint16_t>(rr.code)) + '.');
+        return false;
+    }
+
+    logger_.log("Client " + std::to_string(socket_.fileDescriptor()), "Received unknown reply.");
+    return true;
+}
+
+bool Client::login(const std::uint64_t token) {
+    message::Login sl;
+    sl.token = htobe64(token);
+
+    message::Header sh;
+    sh.type = message::Type::Login;
+    sh.size = htobe16(sizeof sh + sizeof sl);
+
+    std::vector<std::byte> buffer;
+    buffer.reserve(sizeof sh + sizeof sl);
+    buffer.insert(buffer.end(),
+                  reinterpret_cast<const std::byte*>(&sh),
+                  reinterpret_cast<const std::byte*>(&sh) + sizeof sh);
+    buffer.insert(buffer.end(),
+                  reinterpret_cast<const std::byte*>(&sl),
+                  reinterpret_cast<const std::byte*>(&sl) + sizeof sl);
+
+    if (!socket_.write(buffer)) {
+        return false;
+    }
+
+    buffer.resize(sizeof(message::Header));
+
+    if (!socket_.read(buffer)) {
+        return false;
+    }
+
+    if (buffer.size() < sizeof(message::Header)) {
+        return false;
+    }
+
+    message::Header rh;
+    std::memcpy(&rh, buffer.data(), sizeof rh);
+    rh.size = be16toh(rh.size);
+
+    if (rh.type != message::Type::Reply) {
+        logger_.log("Client " + std::to_string(socket_.fileDescriptor()),
+                    "Received message type " + std::to_string(static_cast<std::uint16_t>(rh.type)) + '.');
+        return false;
+    }
+
+    buffer.resize(rh.size - sizeof rh);
+
+    if (!socket_.read(buffer)) {
+        return false;
+    }
+
+    if (buffer.size() < sizeof(message::Reply)) {
+        return false;
+    }
+
+    message::Reply rr;
+    std::memcpy(&rr, buffer.data(), sizeof rr);
+
+    if (rr.code != message::ReplyCode::OK) {
+        logger_.log("Client " + std::to_string(socket_.fileDescriptor()),
+                    "Received reply error code " + std::to_string(static_cast<std::uint16_t>(rr.code)) + '.');
+        return false;
+    }
+
+    logger_.log("Client " + std::to_string(socket_.fileDescriptor()),
+                "Logged in with token " + std::to_string(token) + '.');
+    return true;
+}
+
+bool Client::request(const std::uint32_t firstMissing, const std::uint32_t lastMissing) {
+    message::Request sr;
+    sr.firstMissing = htobe32(firstMissing);
+    sr.lastMissing = htobe32(lastMissing);
+
+    message::Header sh;
+    sh.type = message::Type::Request;
+    sh.size = htobe16(sizeof sh + sizeof sr);
+
+    std::vector<std::byte> buffer;
+    buffer.reserve(sizeof sh + sizeof sr);
+    buffer.insert(buffer.end(),
+                  reinterpret_cast<const std::byte*>(&sh),
+                  reinterpret_cast<const std::byte*>(&sh) + sizeof sh);
+    buffer.insert(buffer.end(),
+                  reinterpret_cast<const std::byte*>(&sr),
+                  reinterpret_cast<const std::byte*>(&sr) + sizeof sr);
+
+    if (!socket_.write(buffer)) {
+        return false;
+    }
+
+    buffer.resize(sizeof(message::Header));
+
+    if (!socket_.read(buffer)) {
+        return false;
+    }
+
+    if (buffer.size() < sizeof(message::Header)) {
+        return false;
+    }
+
+    message::Header rh;
+    std::memcpy(&rh, buffer.data(), sizeof rh);
+    rh.size = be16toh(rh.size);
+
+    if (rh.type != message::Type::Reply) {
+        logger_.log("Client " + std::to_string(socket_.fileDescriptor()),
+                    "Received message type " + std::to_string(static_cast<std::uint16_t>(rh.type)) + '.');
+        return false;
+    }
+
+    buffer.resize(rh.size - sizeof rh);
+
+    if (!socket_.read(buffer)) {
+        return false;
+    }
+
+    if (buffer.size() < sizeof(message::Reply)) {
+        return false;
+    }
+
+    message::Reply rr;
+    std::memcpy(&rr, buffer.data(), sizeof rr);
+
+    if (rr.code != message::ReplyCode::OK) {
+        logger_.log("Client " + std::to_string(socket_.fileDescriptor()),
+                    "Received reply error code " + std::to_string(static_cast<std::uint16_t>(rr.code)) + '.');
+        return false;
+    }
+
+    logger_.log("Client " + std::to_string(socket_.fileDescriptor()),
+                "Requested history from " + std::to_string(firstMissing)
+                + " to " + std::to_string(lastMissing) + '.');
+    return true;
+}
+
+const std::map<std::uint32_t, std::string>& Client::history() const {
+    return history_;
+}
diff --git a/tests/Client.hpp b/tests/Client.hpp
new file mode 100644 (file)
index 0000000..5f4f514
--- /dev/null
@@ -0,0 +1,35 @@
+#pragma once
+
+#include <cstdint>
+#include <map>
+#include <string>
+
+#include "Logger.hpp"
+#include "Socket.hpp"
+
+class Client {
+public:
+    Client(Logger& logger);
+    ~Client() = default;
+
+    Client(const Client&) = delete;
+    Client& operator=(const Client&) = delete;
+
+    Client(Client&&) = delete;
+    Client& operator=(Client&&) = delete;
+
+    bool init();
+    bool connect(const std::string& ip, const std::uint16_t port);
+    bool read();
+    bool unknown();
+    bool login(const std::uint64_t token);
+    bool request(const std::uint32_t firstMissing, const std::uint32_t lastMissing);
+
+    const std::map<std::uint32_t, std::string>& history() const;
+
+private:
+    Logger& logger_;
+
+    Socket socket_;
+    std::map<std::uint32_t, std::string> history_;
+};
diff --git a/tests/history.txt b/tests/history.txt
new file mode 100644 (file)
index 0000000..f55f641
--- /dev/null
@@ -0,0 +1,5 @@
+|invalid
+invalid|invalid
+123|This is some news.
+456|
+789|Last news.
diff --git a/tests/history_clean.txt b/tests/history_clean.txt
new file mode 100644 (file)
index 0000000..dc3ef59
--- /dev/null
@@ -0,0 +1,3 @@
+123|This is some news.
+456|More news.
+789|Last news.
diff --git a/tests/testClient.cpp b/tests/testClient.cpp
new file mode 100644 (file)
index 0000000..59cacb1
--- /dev/null
@@ -0,0 +1,8 @@
+#include <gtest/gtest.h>
+
+#include "Client.hpp"
+
+TEST(Client, Client) {
+    Logger logger;
+    Client client(logger);
+}
diff --git a/tests/testServer.cpp b/tests/testServer.cpp
new file mode 100644 (file)
index 0000000..e66dfe5
--- /dev/null
@@ -0,0 +1,125 @@
+#include <gtest/gtest.h>
+
+#include <chrono>
+#include <filesystem>
+
+#include "Client.hpp"
+#include "Server.hpp"
+
+const std::filesystem::path sourceDir = SOURCE_DIR;
+const std::filesystem::path binaryDir = BINARY_DIR;
+
+namespace {
+bool waitListening(Server& server) {
+    auto start = std::chrono::steady_clock::now();
+
+    while (std::chrono::steady_clock::now() - start < std::chrono::seconds(1)) {
+        if (server.listening()) {
+            return true;
+        }
+    }
+
+    return false;
+}
+}
+
+TEST(Server, NewFile) {
+    std::filesystem::remove(binaryDir / "test.txt");
+    Logger logger;
+    Server server(logger);
+    ASSERT_TRUE(server.init(sourceDir / "tokens.txt", binaryDir / "test.txt"));
+    EXPECT_TRUE(server.addNews(1234, "EEEE"));
+    EXPECT_FALSE(server.addNews(1234, "EEEEEE"));
+}
+
+TEST(Server, ExistingFile) {
+    std::filesystem::remove(binaryDir / "history.txt");
+    ASSERT_TRUE(std::filesystem::copy_file(binaryDir / "history_ref.txt", binaryDir / "history.txt"));
+    Logger logger;
+    Server server(logger);
+    ASSERT_TRUE(server.init(sourceDir / "tokens.txt", binaryDir / "history.txt"));
+    EXPECT_FALSE(server.addNews(123, "EEEE"));
+    EXPECT_TRUE(server.addNews(1234, "EEEEEE"));
+}
+
+TEST(Server, Connect) {
+    std::filesystem::remove(binaryDir / "history_clean.txt");
+    ASSERT_TRUE(std::filesystem::copy_file(binaryDir / "history_clean_ref.txt", binaryDir / "history_clean.txt"));
+    Logger logger;
+    Server server(logger);
+    server.stop();
+    ASSERT_TRUE(server.init(sourceDir / "tokens.txt", binaryDir / "history_clean.txt"));
+    ASSERT_TRUE(server.start(2000));
+    ASSERT_TRUE(waitListening(server));
+
+    Client c1(logger);
+    ASSERT_TRUE(c1.init());
+    ASSERT_TRUE(c1.connect("127.0.0.1", 2000));
+
+    ASSERT_TRUE(c1.unknown());
+    ASSERT_FALSE(c1.login(11114444));
+    ASSERT_TRUE(c1.login(11115555));
+}
+
+TEST(Server, News) {
+    std::filesystem::remove(binaryDir / "history_clean.txt");
+    ASSERT_TRUE(std::filesystem::copy_file(binaryDir / "history_clean_ref.txt", binaryDir / "history_clean.txt"));
+    Logger logger;
+    Server server(logger);
+    server.stop();
+    ASSERT_TRUE(server.init(sourceDir / "tokens.txt", binaryDir / "history_clean.txt"));
+    ASSERT_TRUE(server.start(2000));
+    ASSERT_TRUE(waitListening(server));
+
+    Client c1(logger);
+    Client c2(logger);
+    ASSERT_TRUE(c1.init());
+    ASSERT_TRUE(c2.init());
+    ASSERT_TRUE(c1.connect("127.0.0.1", 2000));
+    ASSERT_TRUE(c2.connect("127.0.0.1", 2000));
+
+    ASSERT_TRUE(c1.login(11115555));
+
+    // This news is only going to be received by c1. c2 not logged in.
+    ASSERT_TRUE(server.addNews(1111, "Hello"));
+
+    ASSERT_TRUE(c1.read());
+    EXPECT_EQ(c1.history().size(), 1);
+    ASSERT_TRUE(c1.history().find(1111) != c1.history().cend());
+    EXPECT_EQ(c1.history().at(1111), "Hello");
+
+    ASSERT_TRUE(c2.login(11116666));
+
+    // This news is only going to be received by c1 and c2.
+    ASSERT_TRUE(server.addNews(2222, "Helloo"));
+
+    ASSERT_TRUE(c1.read());
+    ASSERT_TRUE(c2.read());
+
+    EXPECT_EQ(c1.history().size(), 2);
+    ASSERT_TRUE(c1.history().find(2222) != c1.history().cend());
+    EXPECT_EQ(c1.history().at(2222), "Helloo");
+    EXPECT_EQ(c2.history().size(), 1);
+    ASSERT_TRUE(c2.history().find(2222) != c2.history().cend());
+    EXPECT_EQ(c2.history().at(2222), "Helloo");
+}
+
+TEST(Server, Request) {
+    std::filesystem::remove(binaryDir / "history_clean.txt");
+    ASSERT_TRUE(std::filesystem::copy_file(binaryDir / "history_clean_ref.txt", binaryDir / "history_clean.txt"));
+    Logger logger;
+    Server server(logger);
+    server.stop();
+    ASSERT_TRUE(server.init(sourceDir / "tokens.txt", binaryDir / "history_clean.txt"));
+    ASSERT_TRUE(server.start(2000));
+    ASSERT_TRUE(waitListening(server));
+
+    Client c1(logger);
+    ASSERT_TRUE(c1.init());
+    ASSERT_TRUE(c1.connect("127.0.0.1", 2000));
+
+    ASSERT_FALSE(c1.request(124, 789)); // Not logged in.
+    EXPECT_TRUE(c1.history().empty());
+    ASSERT_TRUE(c1.login(11115555));
+    ASSERT_TRUE(c1.request(124, 789));
+}
diff --git a/tests/testSocket.cpp b/tests/testSocket.cpp
new file mode 100644 (file)
index 0000000..36c8e37
--- /dev/null
@@ -0,0 +1,112 @@
+#include <gtest/gtest.h>
+
+#include <chrono>
+#include <memory>
+#include <thread>
+#include <unordered_map>
+
+#include "Socket.hpp"
+
+TEST(Socket, Socket) {
+    Logger logger;
+    Socket serverSocket(logger);
+
+    ASSERT_FALSE(serverSocket.listen(2000));
+    ASSERT_TRUE(serverSocket.init());
+    ASSERT_FALSE(serverSocket.listen(0));
+    ASSERT_TRUE(serverSocket.listen(2000));
+
+    std::vector<std::byte> ref{std::byte{0}, std::byte{1}, std::byte{2}, std::byte{3}};
+
+    std::thread t([&logger, &serverSocket, &ref]() {
+        Socket client(logger);
+
+        std::chrono::time_point start = std::chrono::steady_clock::now();
+
+        while (!client.isOpen()) {
+            if (std::chrono::steady_clock::now() - start > std::chrono::seconds(1)) {
+                return;
+            }
+
+            serverSocket.accept(client);
+        }
+
+        EXPECT_TRUE(client.write(ref));
+    });
+
+    Socket clientSocket(logger);
+    ASSERT_FALSE(clientSocket.connect("127.0.0.1", 2000));
+    ASSERT_TRUE(clientSocket.init());
+    ASSERT_FALSE(clientSocket.connect("INVALID", 2000));
+    ASSERT_FALSE(clientSocket.connect("127.0.0.1", 0));
+    ASSERT_TRUE(clientSocket.connect("127.0.0.1", 2000));
+    std::vector<std::byte> result(4);
+    ASSERT_TRUE(clientSocket.read(result));
+
+    t.join();
+
+    EXPECT_EQ(result, ref);
+}
+
+TEST(Socket, SocketPoller) {
+    Logger logger;
+    Socket serverSocket(logger);
+    ASSERT_TRUE(serverSocket.init());
+    ASSERT_TRUE(serverSocket.listen(2000));
+
+    SocketPoller poller(logger);
+    ASSERT_TRUE(poller.init());
+    ASSERT_TRUE(poller.add(serverSocket));
+
+    std::vector<std::byte> messageRef{std::byte{0}, std::byte{1}, std::byte{2}, std::byte{3}};
+
+    std::thread t([&logger, &serverSocket, &poller, &messageRef]() {
+        std::uint8_t count = 2;
+        std::unordered_map<std::uint32_t, std::unique_ptr<Socket>> clientSockets;
+
+        while (count > 0) {
+            std::unordered_set<std::uint32_t> pollResult;
+            ASSERT_TRUE(poller.poll(pollResult));
+
+            for (const auto& fd : pollResult) {
+                // Event from listener socket.
+                if (fd == serverSocket.fileDescriptor()) {
+                    std::unique_ptr<Socket> newClientSocket = std::make_unique<Socket>(logger);
+                    ASSERT_TRUE(serverSocket.accept(*newClientSocket));
+                    ASSERT_TRUE(newClientSocket->isOpen());
+                    ASSERT_TRUE(poller.add(*newClientSocket));
+                    ASSERT_TRUE(clientSockets.try_emplace(newClientSocket->fileDescriptor(),
+                                                          std::move(newClientSocket)).second);
+
+                    continue;
+                }
+
+                // Event from a client socket.
+                auto it = clientSockets.find(fd);
+
+                ASSERT_TRUE(it != clientSockets.end()); // Event should be from the client socket list.
+
+                // Read data sent from client.
+                std::vector<std::byte> messageResult(4);
+                ASSERT_NE(it->second, nullptr);
+                ASSERT_TRUE(it->second->read(messageResult));
+                EXPECT_EQ(messageResult, messageRef);
+                count--;
+            }
+        }
+    });
+
+    Socket client1(logger);
+    Socket client2(logger);
+
+    ASSERT_TRUE(client1.init());
+    ASSERT_TRUE(client2.init());
+
+    ASSERT_TRUE(client1.connect("127.0.0.1", 2000));
+    ASSERT_TRUE(client2.connect("127.0.0.1", 2000));
+
+    EXPECT_TRUE(client1.write(messageRef));
+    EXPECT_TRUE(client2.write(messageRef));
+
+    t.join();
+}
diff --git a/tests/tokens.txt b/tests/tokens.txt
new file mode 100644 (file)
index 0000000..f6b2fcc
--- /dev/null
@@ -0,0 +1,2 @@
+11115555
+11116666