Skip to content
6 changes: 6 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -622,6 +622,12 @@ if(BUILD_TESTS)
${CMAKE_CURRENT_SOURCE_DIR}/src/ds/test/parse_json_safe.cpp
)

add_unit_test(
public_header_helpers_test
${CMAKE_CURRENT_SOURCE_DIR}/src/ds/test/public_header_helpers.cpp
)
target_link_libraries(public_header_helpers_test PRIVATE http_parser)

add_unit_test(
state_machine_test
${CMAKE_CURRENT_SOURCE_DIR}/src/ds/test/state_machine.cpp
Expand Down
349 changes: 349 additions & 0 deletions src/ds/test/public_header_helpers.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,349 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
Comment thread
achamayou marked this conversation as resolved.
// Licensed under the Apache 2.0 License.

#include "ccf/base_endpoint_registry.h"
#include "ccf/crypto/curve.h"
#include "ccf/crypto/pem.h"
#include "ccf/crypto/san.h"
#include "ccf/ds/locking.h"
#include "ccf/http_status.h"
#include "ccf/rest_verb.h"
#include "ccf/service/tables/proposals.h"
#include "ccf/tx_id.h"
#include "ccf/tx_status.h"

#define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN
#include <array>
#include <atomic>
#include <doctest/doctest.h>
#include <limits>
#include <span>
#include <thread>

TEST_CASE("API result strings")
{
constexpr std::array api_results = {
std::pair{ccf::ApiResult::OK, "OK"},
std::pair{ccf::ApiResult::Uninitialised, "Uninitialised"},
std::pair{ccf::ApiResult::InvalidArgs, "InvalidArgs"},
std::pair{ccf::ApiResult::NotFound, "NotFound"},
std::pair{ccf::ApiResult::InternalError, "InternalError"}};
for (const auto& [result, expected] : api_results)
{
CHECK(ccf::api_result_to_str(result) == std::string_view(expected));
}
CHECK(
ccf::api_result_to_str(static_cast<ccf::ApiResult>(255)) ==
std::string_view("Unhandled ApiResult"));

constexpr std::array invalid_args_reasons = {
std::pair{ccf::InvalidArgsReason::NoReason, "NoReason"},
std::pair{ccf::InvalidArgsReason::ViewSmallerThanOne, "ViewSmallerThanOne"},
std::pair{
ccf::InvalidArgsReason::ActionAlreadyApplied, "ActionAlreadyApplied"},
std::pair{
ccf::InvalidArgsReason::StaleActionCreatedTimestamp,
"StaleActionCreatedTimestamp"}};
for (const auto& [reason, expected] : invalid_args_reasons)
{
CHECK(
ccf::invalid_args_reason_to_str(reason) == std::string_view(expected));
}
CHECK(
ccf::invalid_args_reason_to_str(static_cast<ccf::InvalidArgsReason>(255)) ==
std::string_view("Unhandled InvalidArgsReason"));
}

TEST_CASE("REST verbs")
{
CHECK(ccf::http_method_from_str("GET") == HTTP_GET);
CHECK(ccf::http_method_from_str("POST") == HTTP_POST);
CHECK_THROWS_AS(ccf::http_method_from_str("UNKNOWN"), std::logic_error);

const ccf::RESTVerb get_from_method = HTTP_GET;
const ccf::RESTVerb get_from_string = std::string("GET");
const ccf::RESTVerb post = HTTP_POST;
CHECK(get_from_method.get_http_method() == HTTP_GET);
CHECK(std::string_view(get_from_method.c_str()) == "GET");
CHECK(get_from_method == get_from_string);
CHECK(get_from_method != post);
CHECK(
(get_from_method < post) ==
(static_cast<int>(HTTP_GET) < static_cast<int>(HTTP_POST)));

nlohmann::json json = post;
CHECK(json == "post");
CHECK(json.get<ccf::RESTVerb>() == post);
CHECK(nlohmann::json("get").get<ccf::RESTVerb>() == get_from_method);
CHECK_THROWS_AS(nlohmann::json(42).get<ccf::RESTVerb>(), std::runtime_error);
CHECK_THROWS_AS(
nlohmann::json("unknown").get<ccf::RESTVerb>(), std::logic_error);
}

TEST_CASE("Transaction IDs")
{
const std::array valid_ids = {
std::pair{std::string("1.1"), ccf::TxID{1, 1}},
std::pair{std::string("42.100"), ccf::TxID{42, 100}},
std::pair{
std::to_string(std::numeric_limits<uint64_t>::max()) + "." +
std::to_string(std::numeric_limits<uint64_t>::max()),
ccf::TxID{
std::numeric_limits<uint64_t>::max(),
std::numeric_limits<uint64_t>::max()}}};

for (const auto& [input, expected] : valid_ids)
{
const auto parsed = ccf::TxID::from_str(input);
REQUIRE(parsed.has_value());
CHECK(parsed.value() == expected);
CHECK(parsed->to_str() == input);
}

constexpr std::array invalid_ids = {
"",
"1",
".1",
"0.1",
"x.1",
"1x.1",
"1.",
"1.0",
"1.x",
"1.1x",
"1.1.1",
"18446744073709551616.1",
"1.18446744073709551616"};
for (const auto input : invalid_ids)
{
CHECK_FALSE(ccf::TxID::from_str(input).has_value());
}

const ccf::TxID tx_id{2, 42};
nlohmann::json json = tx_id;
CHECK(json == "2.42");
CHECK(json.get<ccf::TxID>() == tx_id);
CHECK_THROWS_AS(nlohmann::json(42).get<ccf::TxID>(), ccf::JsonParseError);
CHECK_THROWS_AS(nlohmann::json("0.42").get<ccf::TxID>(), ccf::JsonParseError);
}

TEST_CASE("Proposal state formatting")
{
constexpr std::array proposal_states = {
std::pair{ccf::ProposalState::OPEN, "open"},
std::pair{ccf::ProposalState::ACCEPTED, "accepted"},
std::pair{ccf::ProposalState::WITHDRAWN, "withdrawn"},
std::pair{ccf::ProposalState::REJECTED, "rejected"},
std::pair{ccf::ProposalState::FAILED, "failed"},
std::pair{ccf::ProposalState::DROPPED, "dropped"}};
for (const auto& [state, expected] : proposal_states)
{
CHECK(fmt::format("{}", state) == expected);
}
CHECK_THROWS_AS(
[]() { return fmt::format("{}", static_cast<ccf::ProposalState>(255)); }(),
std::logic_error);
}

TEST_CASE("PEM helpers")
{
const std::string pem_a = "-----BEGIN A-----";
const std::string pem_b = "-----BEGIN B-----";
std::vector<uint8_t> bytes(pem_a.begin(), pem_a.end());

const ccf::crypto::Pem from_vector(bytes);
const ccf::crypto::Pem from_span{std::span<const uint8_t>(bytes)};
const ccf::crypto::Pem from_string(pem_a);
CHECK(from_vector == from_string);
CHECK(from_span == from_string);
CHECK(from_vector.raw() == bytes);
CHECK(from_vector.size() == bytes.size());
CHECK(
std::string_view(
reinterpret_cast<const char*>(from_vector.data()), from_vector.size()) ==
pem_a);

bytes.push_back('\0');
ccf::crypto::Pem with_null_terminator(bytes);
CHECK(with_null_terminator == from_string);
CHECK(
std::string_view(
reinterpret_cast<const char*>(with_null_terminator.data()),
with_null_terminator.size()) == pem_a);

const ccf::crypto::Pem empty;
const ccf::crypto::Pem later(pem_b);
CHECK(empty.empty());
CHECK_FALSE(from_string.empty());
CHECK(from_string != later);
CHECK(from_string < later);
CHECK(
std::hash<ccf::crypto::Pem>{}(from_string) ==
std::hash<std::string>{}(pem_a));

nlohmann::json json = from_string;
CHECK(json == pem_a);
CHECK(json.get<ccf::crypto::Pem>() == from_string);

nlohmann::json array_json = nlohmann::json::array();
for (const auto byte : from_string.raw())
{
array_json.push_back(byte);
}
CHECK(array_json.get<ccf::crypto::Pem>() == from_string);
CHECK_THROWS_AS(
nlohmann::json::object().get<ccf::crypto::Pem>(), std::runtime_error);
CHECK_THROWS_AS(
nlohmann::json("").get<ccf::crypto::Pem>(), std::runtime_error);
CHECK_THROWS_AS(
nlohmann::json::array().get<ccf::crypto::Pem>(), std::logic_error);
CHECK_THROWS_AS(ccf::crypto::Pem(""), std::runtime_error);
CHECK_THROWS_AS(ccf::crypto::Pem(std::string("not PEM")), std::runtime_error);
CHECK_THROWS_AS(ccf::crypto::Pem(std::vector<uint8_t>{}), std::logic_error);
CHECK_THROWS_AS(
ccf::crypto::Pem(std::vector<uint8_t>{'n', 'o', 't', ' ', 'P', 'E', 'M'}),
std::runtime_error);
}

TEST_CASE("Transaction status strings")
{
constexpr std::array statuses = {
std::pair{ccf::TxStatus::Unknown, "Unknown"},
std::pair{ccf::TxStatus::Pending, "Pending"},
std::pair{ccf::TxStatus::Committed, "Committed"},
std::pair{ccf::TxStatus::Invalid, "Invalid"}};
for (const auto& [status, expected] : statuses)
{
CHECK(ccf::tx_status_to_str(status) == std::string_view(expected));
}
CHECK(
ccf::tx_status_to_str(static_cast<ccf::TxStatus>(255)) ==
std::string_view("Unhandled value"));
}

TEST_CASE("Subject alternative names")
{
const ccf::crypto::SubjectAltName expected_ip{"127.0.0.1", true};
const ccf::crypto::SubjectAltName expected_ipv6{"2001:db8::1", true};
const ccf::crypto::SubjectAltName expected_dns{"example.com", false};
const ccf::crypto::SubjectAltName expected_wildcard{"*.example.com", false};
CHECK(ccf::crypto::san_from_string("iPAddress:127.0.0.1") == expected_ip);
CHECK(ccf::crypto::san_from_string("iPAddress:2001:db8::1") == expected_ipv6);
CHECK(ccf::crypto::san_from_string("dNSName:example.com") == expected_dns);
CHECK(
ccf::crypto::san_from_string("dNSName:*.example.com") == expected_wildcard);
CHECK(
ccf::crypto::sans_from_string_list(
{"iPAddress:127.0.0.1",
"dNSName:example.com",
"iPAddress:2001:db8::1",
"dNSName:*.example.com"}) ==
std::vector{expected_ip, expected_dns, expected_ipv6, expected_wildcard});
CHECK_THROWS_AS(
ccf::crypto::sans_from_string_list(
{"dNSName:example.com", "invalid:prefix"}),
std::logic_error);
CHECK_THROWS_AS(
ccf::crypto::san_from_string("email:test@example.com"), std::logic_error);

CHECK(fmt::format("{}", expected_ip) == "IP:127.0.0.1");
CHECK(fmt::format("{}", expected_dns) == "DNS:example.com");

nlohmann::json json = expected_dns;
CHECK(json.get<ccf::crypto::SubjectAltName>() == expected_dns);
}

TEST_CASE("Curve digest selection")
{
using ccf::crypto::CurveID;
using ccf::crypto::MDType;

CHECK(ccf::crypto::get_md_for_ec(CurveID::SECP256R1) == MDType::SHA256);
CHECK(ccf::crypto::get_md_for_ec(CurveID::SECP384R1) == MDType::SHA384);
CHECK(ccf::crypto::get_md_for_ec(CurveID::SECP521R1) == MDType::SHA512);

CHECK_THROWS_AS(ccf::crypto::get_md_for_ec(CurveID::NONE), std::logic_error);
CHECK_THROWS_AS(
ccf::crypto::get_md_for_ec(CurveID::CURVE25519), std::logic_error);
CHECK_THROWS_AS(
ccf::crypto::get_md_for_ec(CurveID::X25519), std::logic_error);
CHECK_THROWS_AS(
ccf::crypto::get_md_for_ec(static_cast<CurveID>(255)), std::logic_error);
}

TEST_CASE("Locking helpers")
{
ccf::ds::Mutex mutex;
CHECK(mutex.native_handle() != nullptr);

ccf::ds::ConditionVariable condition_variable;
std::atomic<bool> entered = false;
bool wakeup = false;
std::atomic<bool> woke = false;
std::atomic<bool> contender_checked = false;
std::atomic<bool> contender_acquired = false;
std::atomic<bool> release = false;
std::thread waiter([&]() {
ccf::ds::MutexGuard guard(mutex);
entered.store(true, std::memory_order_release);
condition_variable.wait(guard, [&]() { return wakeup; });
woke.store(true, std::memory_order_release);
while (!contender_checked.load(std::memory_order_acquire))
{
std::this_thread::yield();
}
while (!release.load(std::memory_order_acquire))
{
std::this_thread::yield();
}
});
std::thread contender([&]() {
while (!woke.load(std::memory_order_acquire))
{
std::this_thread::yield();
}
if (mutex.try_lock())
{
contender_acquired.store(true, std::memory_order_release);
mutex.unlock();
}
contender_checked.store(true, std::memory_order_release);
});

while (!entered.load(std::memory_order_acquire))
{
std::this_thread::yield();
}

{
ccf::ds::MutexGuard guard(mutex);
wakeup = true;
condition_variable.notify_one();
}

while (!contender_checked.load(std::memory_order_acquire))
{
std::this_thread::yield();
}
CHECK_FALSE(contender_acquired.load(std::memory_order_acquire));
release.store(true, std::memory_order_release);
waiter.join();
contender.join();
CHECK(woke.load(std::memory_order_acquire));
const bool acquired = mutex.try_lock();
CHECK(acquired);
if (acquired)
{
mutex.unlock();
}
}

TEST_CASE("HTTP client error status")
{
CHECK_FALSE(
ccf::is_http_status_client_error(static_cast<ccf::http_status>(399)));
CHECK(ccf::is_http_status_client_error(static_cast<ccf::http_status>(400)));
CHECK(ccf::is_http_status_client_error(static_cast<ccf::http_status>(499)));
CHECK_FALSE(
ccf::is_http_status_client_error(static_cast<ccf::http_status>(500)));
}