CoreClient (IXWebSocket) : connexion + hello sequa-studio-host, requêtes corrélées par id avec délais, requêtes concurrentes, erreur immédiate à la déconnexion, rappel on_disconnect. wire : hex64, base64 RFC 4648 strict, décodage des empreintes, first_divergence. 15 cas (Debug, ASan) contre un faux serveur conforme à la spec. Écrit par un subagent (DeepSeek V4.1 Flash), relu.
121 lines
4.4 KiB
C++
121 lines
4.4 KiB
C++
// SPDX-License-Identifier: MPL-2.0
|
|
//
|
|
// Tests des aides de représentation du protocole (spec §2, §7.6) : hex64, base64 RFC 4648,
|
|
// décodage d'empreintes et first_divergence.
|
|
#include "sequa/studio/host/wire.hpp"
|
|
|
|
#include <doctest/doctest.h>
|
|
|
|
#include <cstddef>
|
|
#include <cstdint>
|
|
#include <limits>
|
|
#include <optional>
|
|
#include <span>
|
|
#include <string>
|
|
#include <string_view>
|
|
#include <vector>
|
|
|
|
using namespace sequa::studio::host;
|
|
|
|
namespace {
|
|
|
|
std::vector<std::uint8_t> bytes_of(std::string_view text)
|
|
{
|
|
std::vector<std::uint8_t> out;
|
|
out.reserve(text.size());
|
|
for (const char c : text) {
|
|
out.push_back(static_cast<std::uint8_t>(c));
|
|
}
|
|
return out;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
TEST_CASE("hostclient: hex64 aller-retour et refus stricts")
|
|
{
|
|
CHECK(to_hex64(0) == "0000000000000000");
|
|
CHECK(to_hex64(0x0123456789abcdefULL) == "0123456789abcdef");
|
|
CHECK(to_hex64(std::numeric_limits<std::uint64_t>::max()) == "ffffffffffffffff");
|
|
|
|
CHECK(from_hex64("0000000000000000") == 0U);
|
|
CHECK(from_hex64("0123456789abcdef") == 0x0123456789abcdefULL);
|
|
CHECK(from_hex64("ffffffffffffffff") == std::numeric_limits<std::uint64_t>::max());
|
|
|
|
// Refus strict : majuscules, longueur incorrecte, caractère hors alphabet.
|
|
CHECK_FALSE(from_hex64("0123456789ABCDEF").has_value());
|
|
CHECK_FALSE(from_hex64("0123456789abcde").has_value());
|
|
CHECK_FALSE(from_hex64("0123456789abcdef0").has_value());
|
|
CHECK_FALSE(from_hex64("0123456789abcdeg").has_value());
|
|
CHECK_FALSE(from_hex64("").has_value());
|
|
|
|
const std::uint64_t values[] = {1U, 255U, 65536U, 0xdeadbeefcafef00dULL};
|
|
for (const std::uint64_t value : values) {
|
|
CHECK(from_hex64(to_hex64(value)) == value);
|
|
CHECK(to_hex64(value).size() == hex64_length);
|
|
}
|
|
}
|
|
|
|
TEST_CASE("hostclient: base64 RFC 4648 (vecteurs et refus)")
|
|
{
|
|
// Vecteurs de test de la RFC 4648 §10.
|
|
CHECK(base64_encode("") == "");
|
|
CHECK(base64_encode("f") == "Zg==");
|
|
CHECK(base64_encode("fo") == "Zm8=");
|
|
CHECK(base64_encode("foo") == "Zm9v");
|
|
CHECK(base64_encode("foob") == "Zm9vYg==");
|
|
CHECK(base64_encode("fooba") == "Zm9vYmE=");
|
|
CHECK(base64_encode("foobar") == "Zm9vYmFy");
|
|
|
|
const auto decoded = base64_decode("Zm9vYmFy");
|
|
REQUIRE(decoded.has_value());
|
|
CHECK(*decoded == bytes_of("foobar"));
|
|
|
|
CHECK(base64_decode("") == std::optional<std::vector<std::uint8_t>>(std::vector<std::uint8_t>{}));
|
|
const auto one = base64_decode("Zg==");
|
|
REQUIRE(one.has_value());
|
|
CHECK(*one == bytes_of("f"));
|
|
|
|
// Refus stricts.
|
|
CHECK_FALSE(base64_decode("Zm9vYmF").has_value()); // longueur non multiple de 4
|
|
CHECK_FALSE(base64_decode("Zm9v!mFy").has_value()); // caractère hors alphabet
|
|
CHECK_FALSE(base64_decode("Zg=").has_value()); // remplissage incomplet
|
|
CHECK_FALSE(base64_decode("=Zg=").has_value()); // '=' en tête
|
|
CHECK_FALSE(base64_decode("Zg==Zg==").has_value()); // '=' au milieu
|
|
CHECK_FALSE(base64_decode("Zh==").has_value()); // bits de queue non nuls
|
|
|
|
// Aller-retour sur des octets binaires quelconques.
|
|
const std::vector<std::uint8_t> raw{0U, 1U, 2U, 250U, 255U};
|
|
const auto round_trip = base64_decode(base64_encode(std::span<const std::uint8_t>(raw)));
|
|
REQUIRE(round_trip.has_value());
|
|
CHECK(*round_trip == raw);
|
|
}
|
|
|
|
TEST_CASE("hostclient: decode_hashes et first_divergence")
|
|
{
|
|
const std::vector<std::uint64_t> a{1U, 2U, 3U, 4U};
|
|
CHECK(first_divergence(a, a) == std::nullopt);
|
|
|
|
const std::vector<std::uint64_t> b{1U, 2U, 99U, 4U};
|
|
CHECK(first_divergence(a, b) == std::size_t{2});
|
|
CHECK(first_divergence(b, a) == std::size_t{2});
|
|
|
|
const std::vector<std::uint64_t> prefix{1U, 2U};
|
|
CHECK(first_divergence(a, prefix) == std::size_t{2});
|
|
CHECK(first_divergence(prefix, a) == std::size_t{2});
|
|
|
|
// decode_hashes : tableau {1, 2} en uint64 petit-boutiste.
|
|
const std::vector<std::uint8_t> little_endian{1U, 0U, 0U, 0U, 0U, 0U, 0U, 0U,
|
|
2U, 0U, 0U, 0U, 0U, 0U, 0U, 0U};
|
|
const auto hashes = decode_hashes(base64_encode(std::span<const std::uint8_t>(little_endian)));
|
|
REQUIRE(hashes.has_value());
|
|
CHECK(*hashes == (std::vector<std::uint64_t>{1U, 2U}));
|
|
}
|
|
|
|
TEST_CASE("hostclient: decode_hashes refuse les longueurs invalides")
|
|
{
|
|
// 5 octets : pas un multiple de 8.
|
|
CHECK_FALSE(decode_hashes(base64_encode("trois")).has_value());
|
|
// Base64 invalide.
|
|
CHECK_FALSE(decode_hashes("!!").has_value());
|
|
}
|