Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
104 changes: 90 additions & 14 deletions tests/http_client_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@

#include <arpa/inet.h>
#include <netinet/in.h>
#include <netdb.h>
#include <sys/socket.h>
#include <unistd.h>

Expand All @@ -28,6 +29,7 @@
#include <array>
#endif
#include <atomic>
#include <chrono>
#include <cstring>
#if SCYLLADB_ALTERNATOR_CLIENT_CPP_HAS_ZLIB
#include <limits>
Expand All @@ -46,34 +48,51 @@ namespace {

class LocalHttpServer {
public:
explicit LocalHttpServer(std::string body = "[\"node1.local\"]", std::string content_encoding = {})
explicit LocalHttpServer(
std::string body = "[\"node1.local\"]",
std::string content_encoding = {},
int address_family = AF_INET)
: body_(std::move(body))
, content_encoding_(std::move(content_encoding)) {
fd_ = socket(AF_INET, SOCK_STREAM, 0);
fd_ = socket(address_family, SOCK_STREAM, 0);
if (fd_ < 0) {
throw std::runtime_error("socket failed");
}

int yes = 1;
setsockopt(fd_, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(yes));

sockaddr_in addr{};
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
addr.sin_port = 0;
if (bind(fd_, reinterpret_cast<sockaddr*>(&addr), sizeof(addr)) != 0) {
throw std::runtime_error("bind failed");
if (address_family == AF_INET6) {
sockaddr_in6 addr{};
addr.sin6_family = AF_INET6;
addr.sin6_addr = in6addr_loopback;
addr.sin6_port = 0;
if (bind(fd_, reinterpret_cast<sockaddr*>(&addr), sizeof(addr)) != 0) {
throw std::runtime_error("IPv6 bind failed");
}
socklen_t len = sizeof(addr);
if (getsockname(fd_, reinterpret_cast<sockaddr*>(&addr), &len) != 0) {
throw std::runtime_error("IPv6 getsockname failed");
}
port_ = ntohs(addr.sin6_port);
} else {
sockaddr_in addr{};
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
addr.sin_port = 0;
if (bind(fd_, reinterpret_cast<sockaddr*>(&addr), sizeof(addr)) != 0) {
throw std::runtime_error("bind failed");
}
socklen_t len = sizeof(addr);
if (getsockname(fd_, reinterpret_cast<sockaddr*>(&addr), &len) != 0) {
throw std::runtime_error("getsockname failed");
}
port_ = ntohs(addr.sin_port);
}
if (listen(fd_, 1) != 0) {
throw std::runtime_error("listen failed");
}

socklen_t len = sizeof(addr);
if (getsockname(fd_, reinterpret_cast<sockaddr*>(&addr), &len) != 0) {
throw std::runtime_error("getsockname failed");
}
port_ = ntohs(addr.sin_port);

worker_ = std::thread([this] {
int client = accept(fd_, nullptr, nullptr);
if (client < 0) {
Expand Down Expand Up @@ -331,6 +350,18 @@ class TestContentEncodingDecoder final : public HttpContentEncodingDecoder {

} // namespace

bool LocalhostHasAddressFamily(int family) {
addrinfo hints{};
hints.ai_family = family;
hints.ai_socktype = SOCK_STREAM;
addrinfo* raw = nullptr;
if (getaddrinfo("localhost", nullptr, &hints, &raw) != 0) {
return false;
}
freeaddrinfo(raw);
return true;
}

TEST(HttpClient, PerformsPlainHttpGet) {
LocalHttpServer server;

Expand All @@ -346,6 +377,51 @@ TEST(HttpClient, PerformsPlainHttpGet) {
EXPECT_EQ(server.Request().find("Accept-Encoding:"), std::string::npos);
}

TEST(HttpClient, PerformsPlainHttpGetOverIPv6Literal) {
LocalHttpServer server("[\"::1\"]", {}, AF_INET6);

Config cfg;
cfg.scheme = "http";
CurlHttpClient client(cfg);
const auto url = Url("http", "::1", server.Port()).WithPathAndQuery("/localnodes");

const auto response = client.Get(url);

EXPECT_EQ(response.status_code, 200);
EXPECT_EQ(response.body, "[\"::1\"]");
EXPECT_EQ(url.Authority(), "[::1]:" + std::to_string(server.Port()));
EXPECT_EQ(url.ToString(), "http://[::1]:" + std::to_string(server.Port()) + "/localnodes");
EXPECT_NE(server.Request().find("Host: [::1]:" + std::to_string(server.Port())), std::string::npos);
}

TEST(HttpClient, DualStackDnsFallsBackToReachableIPv4) {
if (!LocalhostHasAddressFamily(AF_INET) || !LocalhostHasAddressFamily(AF_INET6)) {
GTEST_SKIP() << "localhost does not resolve to both IPv4 and IPv6";
}
LocalHttpServer server;

Config cfg;
cfg.scheme = "http";
cfg.connect_timeout = std::chrono::milliseconds(500);
CurlHttpClient client(cfg);

EXPECT_EQ(client.Get(Url("http", "localhost", server.Port()).WithPathAndQuery("/localnodes")).status_code, 200);
}

TEST(HttpClient, DualStackDnsFallsBackToReachableIPv6) {
if (!LocalhostHasAddressFamily(AF_INET) || !LocalhostHasAddressFamily(AF_INET6)) {
GTEST_SKIP() << "localhost does not resolve to both IPv4 and IPv6";
}
LocalHttpServer server("[\"::1\"]", {}, AF_INET6);

Config cfg;
cfg.scheme = "http";
cfg.connect_timeout = std::chrono::milliseconds(500);
CurlHttpClient client(cfg);

EXPECT_EQ(client.Get(Url("http", "localhost", server.Port()).WithPathAndQuery("/localnodes")).status_code, 200);
}

TEST(HttpClient, RequestsAndDecodesGzipResponse) {
#if SCYLLADB_ALTERNATOR_CLIENT_CPP_HAS_ZLIB
const std::string body = "[\"node1.local\"]";
Expand Down
25 changes: 25 additions & 0 deletions tests/live_nodes_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,31 @@ TEST(AlternatorLiveNodes, DnsEntrypointDiscoversDnsNodeRecords) {
EXPECT_EQ(Hosts(nodes.GetNodes()), std::vector<std::string>({"localhost", "node-a.internal"}));
}

TEST(AlternatorLiveNodes, IPv6LiteralDiscoversIPv6NodeRecords) {
std::vector<std::string> requested_urls;
auto http = std::make_shared<FakeHttpClient>([&](const Url& url) {
requested_urls.push_back(url.ToString());
if (url.host == "2001:db8::10") {
return HttpResponse{200, R"(["2001:db8::20","node-a.internal"])"};
}
return HttpResponse{200, "[]"};
});

Config cfg;
cfg.scheme = "https";
cfg.port = 8043;
cfg.routing_scope = std::make_shared<ClusterScope>();
AlternatorLiveNodes nodes({"2001:db8::10"}, cfg, http);

nodes.UpdateLiveNodes();

EXPECT_EQ(requested_urls, std::vector<std::string>({"https://[2001:db8::10]:8043/localnodes"}));
EXPECT_EQ(Hosts(nodes.GetNodes()), std::vector<std::string>({"2001:db8::20", "node-a.internal"}));
const auto discovered = nodes.GetNodes();
ASSERT_EQ(discovered.size(), 2U);
EXPECT_EQ(discovered[0].ToString(), "https://[2001:db8::20]:8043");
}

TEST(AlternatorLiveNodes, ClusterScopeRefreshUsesConfiguredSeedNodes) {
Config cfg;
cfg.routing_scope = NewClusterScope();
Expand Down
Loading