Files
logos-verified-proxy-module/tests/test_beacon_client.cpp
T
Dario Gabriel LipicarandClaude Opus 5 a1a17acbc5 feat: wire the heartbeat's health signal, and add fetchFinalizedRoot
Three fields — head.blockNumber, head.updatedAt and heartbeatFailures — were
read by statusSnapshot() and never assigned, and State::Degraded appeared only
in stateName(). The heartbeat was fire-and-forget with a comment pointing at a
pollHeartbeat() that does not exist, so nothing ever observed its outcome:
status().head stayed "" for the life of the process and a proxy whose sync had
died still reported "running".

CallSlot now carries a Kind, so the trampoline can tell a user call from a
heartbeat or a head probe. Three consecutive heartbeat failures degrade the
proxy and one success clears it; head is refreshed by a separate
eth_blockNumber probe every fifth beat, since eth_syncing answers a hardcoded
`false` and cannot report it. live() joins Running and Degraded for callers
making lifecycle decisions, leaving running() strict for health.

fetchFinalizedRoot() is new, and lives here rather than in the panel because
Basecamp sandboxes ui_qml plugins away from the network: an XMLHttpRequest from
a view is refused outright. It is a convenience, not a trust anchor, and says
so. Adds libcurl, used for that one request and nothing else.

Tests spin on the condition rather than sleeping a fixed interval — the first
draft was green on an idle machine and red under a parallel build.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-26 14:51:11 -03:00

74 lines
2.7 KiB
C++

// URL construction and response parsing for the finalized-root helper.
//
// Both halves are pure, so the whole shape of the request and the whole shape
// of what we accept back are testable without a beacon node or a socket.
#include <string>
#include <logos_test.h>
#include "beacon_client.h"
namespace bc = beacon_client;
LOGOS_TEST(beacon_url_appends_the_finalized_headers_path) {
LOGOS_ASSERT_EQ(bc::finalizedHeaderUrl("https://example.org"),
"https://example.org/eth/v1/beacon/headers/finalized");
}
LOGOS_TEST(beacon_url_tolerates_trailing_slashes) {
// A URL pasted from a browser very often carries one.
LOGOS_ASSERT_EQ(bc::finalizedHeaderUrl("https://example.org///"),
"https://example.org/eth/v1/beacon/headers/finalized");
}
LOGOS_TEST(beacon_url_keeps_a_path_prefix) {
LOGOS_ASSERT_EQ(bc::finalizedHeaderUrl("https://example.org/beacon"),
"https://example.org/beacon/eth/v1/beacon/headers/finalized");
}
LOGOS_TEST(beacon_accepts_only_http_schemes) {
// ws:// and wss:// are valid beacon transports for the library itself, but
// cannot serve the REST GET this helper makes.
LOGOS_ASSERT_TRUE(bc::isHttpUrl("http://example.org"));
LOGOS_ASSERT_TRUE(bc::isHttpUrl("https://example.org"));
LOGOS_ASSERT_FALSE(bc::isHttpUrl("wss://example.org"));
LOGOS_ASSERT_FALSE(bc::isHttpUrl("ws://example.org"));
LOGOS_ASSERT_FALSE(bc::isHttpUrl("file:///etc/passwd"));
LOGOS_ASSERT_FALSE(bc::isHttpUrl(""));
}
LOGOS_TEST(beacon_trims_surrounding_whitespace) {
LOGOS_ASSERT_EQ(bc::trim(" https://example.org \n"), "https://example.org");
LOGOS_ASSERT_EQ(bc::trim(" "), "");
}
LOGOS_TEST(beacon_parses_root_and_slot) {
const std::string body = R"({
"data": {
"root": "0xabc",
"header": { "message": { "slot": "12345" } }
}
})";
std::string slot;
LOGOS_ASSERT_EQ(bc::parseFinalizedRoot(body, slot), "0xabc");
LOGOS_ASSERT_EQ(slot, "12345");
}
LOGOS_TEST(beacon_parse_tolerates_a_missing_slot) {
// The slot is informational; only the root is load-bearing.
std::string slot = "stale";
LOGOS_ASSERT_EQ(bc::parseFinalizedRoot(R"({"data":{"root":"0xdef"}})", slot), "0xdef");
LOGOS_ASSERT_EQ(slot, "");
}
LOGOS_TEST(beacon_parse_rejects_malformed_payloads) {
std::string slot;
LOGOS_ASSERT_EQ(bc::parseFinalizedRoot("not json", slot), "");
LOGOS_ASSERT_EQ(bc::parseFinalizedRoot("[]", slot), "");
LOGOS_ASSERT_EQ(bc::parseFinalizedRoot("{}", slot), "");
LOGOS_ASSERT_EQ(bc::parseFinalizedRoot(R"({"data":{}})", slot), "");
// A non-string root is the shape an error page could plausibly take.
LOGOS_ASSERT_EQ(bc::parseFinalizedRoot(R"({"data":{"root":42}})", slot), "");
}