summaryrefslogtreecommitdiffstats
path: root/tests/test_server.cpp
diff options
context:
space:
mode:
authorMurilo Ijanc <[email protected]>2026-07-26 12:28:10 -0300
committerMurilo Ijanc <[email protected]>2026-07-26 12:28:10 -0300
commitb71faa6e95a0736f1ff171e006b171467b65db90 (patch)
tree7580c3ab515e8f50726dc68dbc240995ff4a1a4c /tests/test_server.cpp
downloadcpp-b71faa6e95a0736f1ff171e006b171467b65db90.tar.gz
Initial importv0.1.0
Diffstat (limited to 'tests/test_server.cpp')
-rw-r--r--tests/test_server.cpp207
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