diff options
Diffstat (limited to 'tests/test_server.cpp')
| -rw-r--r-- | tests/test_server.cpp | 207 |
1 files changed, 207 insertions, 0 deletions
diff --git a/tests/test_server.cpp b/tests/test_server.cpp new file mode 100644 index 0000000..dbb9511 --- /dev/null +++ b/tests/test_server.cpp @@ -0,0 +1,207 @@ +#include "test_server.hpp" + +#include <arpa/inet.h> +#include <netinet/in.h> +#include <netinet/tcp.h> +#include <poll.h> +#include <sys/socket.h> +#include <unistd.h> + +#include <cerrno> +#include <cstring> +#include <stdexcept> + +namespace cepis::testing { +namespace { + +std::string reason_phrase(int status) { + switch (status) { + case 200: + return "OK"; + case 400: + return "Bad Request"; + case 404: + return "Not Found"; + case 429: + return "Too Many Requests"; + case 500: + return "Internal Server Error"; + case 502: + return "Bad Gateway"; + case 503: + return "Service Unavailable"; + case 504: + return "Gateway Timeout"; + default: + return "Status"; + } +} + +bool write_all(int fd, const std::string& data) { + std::size_t sent = 0; + while (sent < data.size()) { + const ssize_t written = ::send(fd, data.data() + sent, data.size() - sent, MSG_NOSIGNAL); + if (written <= 0) { + return false; + } + sent += static_cast<std::size_t>(written); + } + return true; +} + +} // namespace + +Server::Server() { + listen_fd_ = ::socket(AF_INET, SOCK_STREAM, 0); + if (listen_fd_ < 0) { + throw std::runtime_error("socket: " + std::string(std::strerror(errno))); + } + const int enable = 1; + ::setsockopt(listen_fd_, SOL_SOCKET, SO_REUSEADDR, &enable, sizeof(enable)); + + sockaddr_in address{}; + address.sin_family = AF_INET; + address.sin_addr.s_addr = ::htonl(INADDR_LOOPBACK); + address.sin_port = 0; + if (::bind(listen_fd_, reinterpret_cast<sockaddr*>(&address), sizeof(address)) != 0) { + ::close(listen_fd_); + throw std::runtime_error("bind: " + std::string(std::strerror(errno))); + } + if (::listen(listen_fd_, 32) != 0) { + ::close(listen_fd_); + throw std::runtime_error("listen: " + std::string(std::strerror(errno))); + } + + sockaddr_in bound{}; + socklen_t length = sizeof(bound); + if (::getsockname(listen_fd_, reinterpret_cast<sockaddr*>(&bound), &length) != 0) { + ::close(listen_fd_); + throw std::runtime_error("getsockname: " + std::string(std::strerror(errno))); + } + port_ = ::ntohs(bound.sin_port); + + thread_ = std::thread([this] { serve(); }); +} + +Server::~Server() { + stopping_.store(true); + if (thread_.joinable()) { + thread_.join(); + } + if (listen_fd_ >= 0) { + ::close(listen_fd_); + } +} + +std::string Server::url() const { return "http://127.0.0.1:" + std::to_string(port_); } + +void Server::respond(std::string path, Reply reply) { + respond(std::move(path), std::vector<Reply>{std::move(reply)}); +} + +void Server::respond(std::string path, std::vector<Reply> replies) { + const std::scoped_lock guard(mutex_); + routes_[std::move(path)] = std::move(replies); +} + +void Server::handle(std::function<Reply(const std::string&)> handler) { + const std::scoped_lock guard(mutex_); + handler_ = std::move(handler); +} + +int Server::count(std::string_view prefix) const { + const std::scoped_lock guard(mutex_); + int total = 0; + for (const std::string& call : calls_) { + if (call.rfind(prefix, 0) == 0) { + ++total; + } + } + return total; +} + +std::vector<std::string> Server::calls() const { + const std::scoped_lock guard(mutex_); + return calls_; +} + +Reply Server::next(const std::string& path) { + std::function<Reply(const std::string&)> handler; + { + const std::scoped_lock guard(mutex_); + calls_.push_back(path); + handler = handler_; + if (!handler) { + const auto found = routes_.find(path); + if (found == routes_.end()) { + return Reply{404, R"({"erro":"cep_nao_encontrado"})", {}}; + } + std::vector<Reply>& queue = found->second; + if (queue.size() > 1) { + Reply reply = queue.front(); + queue.erase(queue.begin()); + return reply; + } + return queue.front(); + } + } + return handler(path); +} + +void Server::serve() { + while (!stopping_.load()) { + pollfd waiting{listen_fd_, POLLIN, 0}; + const int ready = ::poll(&waiting, 1, 10); + if (ready <= 0) { + continue; + } + + const int client = ::accept(listen_fd_, nullptr, nullptr); + if (client < 0) { + continue; + } + + std::string request; + char buffer[4096]; + while (request.find("\r\n\r\n") == std::string::npos) { + const ssize_t read_bytes = ::recv(client, buffer, sizeof(buffer), 0); + if (read_bytes <= 0) { + break; + } + request.append(buffer, static_cast<std::size_t>(read_bytes)); + } + + const std::size_t first_space = request.find(' '); + const std::size_t second_space = + first_space == std::string::npos ? std::string::npos : request.find(' ', first_space + 1); + if (second_space == std::string::npos) { + ::close(client); + continue; + } + const std::string path = request.substr(first_space + 1, second_space - first_space - 1); + + const Reply reply = next(path); + std::string out = "HTTP/1.1 "; + out += std::to_string(reply.status); + out += ' '; + out += reason_phrase(reply.status); + out += "\r\n"; + for (const auto& [name, value] : reply.headers) { + out += name; + out += ": "; + out += value; + out += "\r\n"; + } + out += "Content-Length: "; + out += std::to_string(reply.body.size()); + out += "\r\n"; + out += "Connection: close\r\n\r\n"; + out += reply.body; + + write_all(client, out); + ::shutdown(client, SHUT_WR); + ::close(client); + } +} + +} // namespace cepis::testing |