Compare commits

...

4 commits

Author SHA1 Message Date
55ea39a89c Added tests for Iq Stanza errors
All checks were successful
PR Check / on-push-commit-check (push) Successful in 19m6s
2024-12-19 17:50:01 +00:00
2dab1ffd23 Added std::variant and Visitor for all IQ Stanza errors 2024-12-19 17:46:56 +00:00
ae9d614e8c Moved ToKebapCasName to utils 2024-12-19 17:17:28 +00:00
d06d02efc0 Fixed spdlog loglevel initialization 2024-12-19 17:16:29 +00:00
11 changed files with 373 additions and 49 deletions

View file

@ -22,6 +22,7 @@ set(BOOST_INCLUDE_LIBRARIES "pfr;asio;serialization")
option(CPM_USE_LOCAL_PACKAGES "Use local packages" ON)
option(UTEMPL_USE_LOCAL_PACKAGE "Use utempl local package" OFF)
option(BUILD_EXECUTABLE ON)
set(LOG_LEVEL 0 "Available log levels: 0=TRACE, 1=DEBUG,2= INFO,3= WARN, 4=ERROR, 5=CRITICAL, 6=OFF") # Compile program with highest available log levle to trace everything
set(UTEMPL_URL
"https://sha512sum.xyz/git/sha512sum/utempl"
CACHE STRING "utempl repository URL")
@ -259,6 +260,7 @@ if(ENABLE_TESTS)
target_sources(larra_xmpp_tests PUBLIC ${SOURCES})
target_link_libraries(larra_xmpp_tests GTest::gtest_main
larra_xmpp)
target_compile_definitions(larra_xmpp_tests PRIVATE SPDLOG_ACTIVE_LEVEL=0) # SPDLOG_LEVEL_TRACE=0. Check LOG_LEVEL variable and spdlog documentation for more details
set_property(TARGET larra_xmpp_tests PROPERTY CXX_STANDARD 23)
include(GoogleTest)
gtest_discover_tests(larra_xmpp_tests)
@ -270,6 +272,8 @@ if(ENABLE_EXAMPLES)
get_filename_component(EXAMPLE_NAME ${EXAMPLE_SRC} NAME_WE)
add_executable(${EXAMPLE_NAME} ${EXAMPLE_SRC})
target_link_libraries(${EXAMPLE_NAME} larra_xmpp)
# TODO(unknown): Fixed 'command line' error occured and uncomment below
# target_compile_definitions(${EXAMPLE_NAME} PRIVATE SPDLOG_ACTIVE_LEVEL=${LOG_LEVEL})
set_property(TARGET ${EXAMPLE_NAME} PROPERTY CXX_STANDARD 23)
set_target_properties(${EXAMPLE_NAME} PROPERTIES
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/examples/output")

View file

@ -52,6 +52,15 @@ auto Coroutine() -> boost::asio::awaitable<void> {
auto main() -> int {
spdlog::set_level(spdlog::level::trace);
#ifdef SPDLOG_ACTIVE_LEVEL
std::println("\n\tCompiled max availabel log level: {}\n\tCurrently set log level: {}",
SPDLOG_ACTIVE_LEVEL,
std::to_underlying(spdlog::get_level()));
#else
std::println("\n\tCurrently set log level: {}", std::to_underlying(spdlog::get_level()));
#endif
boost::asio::io_context io_context;
boost::asio::co_spawn(io_context, Coroutine(), boost::asio::detached);
io_context.run();

View file

@ -84,14 +84,12 @@ struct Client {
auto set_bind_response = co_await connection.template Read<Iq<::iq::Bind>>();
std::visit(utempl::Overloaded(
[](auto error) {
throw "Error response on IQ: Set::Bind: ''"; // TODO(unknown): Add exact error parsing
},
[&](::iq::ResultBind r) {
jid.resource = std::move(r.payload.jid->resource);
SPDLOG_INFO("Allocated resource: {}", jid.resource);
}),
set_bind_response);
},
IqErrVisitor<::iq::Bind>{"Error response on IQ: Set::Bind"}),
std::move(set_bind_response));
co_return;
}
@ -101,14 +99,12 @@ struct Client {
const auto get_roster_response = co_await connection.template Read<Iq<::iq::Roster>>();
std::visit(utempl::Overloaded(
[](auto error) {
throw "Error response on IQ: Get::Roster: ''"; // TODO(unknown): Add exact error parsing
},
[&](::iq::ResultRoster r) {
roster = std::move(r.payload);
SPDLOG_INFO("New roster: {}", ToString(roster));
}),
get_roster_response);
},
IqErrVisitor<::iq::Roster>{"Error response on IQ: Get::Roster"}),
std::move(get_roster_response));
co_return;
}

View file

@ -1,9 +1,12 @@
#pragma once
#include <spdlog/spdlog.h>
#include <larra/iq_error.hpp>
#include <larra/jid.hpp>
#include <larra/serialization.hpp>
#include <larra/stream_error.hpp>
#include <larra/utils.hpp>
#include <optional>
#include <stdexcept>
#include <string>
namespace larra::xmpp {
@ -115,7 +118,40 @@ using Error = BaseImplWithPayload<kErrorName, Payload>;
} // namespace iq
using IqError = iq::Error<iq::error::IqError>;
template <typename Payload>
using Iq = std::variant<iq::Get<Payload>, iq::Set<Payload>, iq::Result<Payload>, iq::Error<Payload>>;
using Iq = std::variant<iq::Get<Payload>, iq::Set<Payload>, iq::Result<Payload>, IqError>;
template <typename Payload>
struct IqErrVisitor {
constexpr IqErrVisitor(std::string_view baseErrorMsg) : baseErrorMsg(baseErrorMsg) {
}
void operator()(const iq::Get<Payload>&) {
static constexpr std::string_view getErrorMsg = ": 'Get' is an invalid type for IQ result. Expected 'Result' or 'Error'";
auto finalErrorMsg = std::string(baseErrorMsg).append(getErrorMsg);
SPDLOG_ERROR(finalErrorMsg);
throw std::runtime_error{finalErrorMsg};
}
void operator()(const iq::Set<Payload>&) {
static constexpr std::string_view getErrorMsg = ": 'Set' is an invalid type for IQ result. Expected 'Result' or 'Error'";
auto finalErrorMsg = std::string(baseErrorMsg).append(getErrorMsg);
SPDLOG_ERROR(finalErrorMsg);
throw std::runtime_error{finalErrorMsg};
}
void operator()(IqError err) {
SPDLOG_ERROR(baseErrorMsg);
std::visit(
[](auto exception) {
throw exception;
},
std::move(err.payload));
}
std::string_view baseErrorMsg;
};
} // namespace larra::xmpp

View file

@ -0,0 +1,152 @@
#pragma once
#include <libxml++/libxml++.h>
#include <larra/utils.hpp>
#include <optional>
#include <variant>
namespace larra::xmpp::iq::error {
namespace impl {
struct IqBaseError : std::exception {};
// DO NOT MOVE TO ANOTHER NAMESPACE(where no heirs). VIA friend A FUNCTION IS ADDED THAT VIA ADL WILL BE SEARCHED FOR HEIRS
// C++20 modules very unstable in clangd :(
template <typename T>
struct IqErrorImpl : IqBaseError {
static constexpr auto kDefaultName = "error";
static constexpr auto kDefaultNamespace = "urn:ietf:params:xml:ns:xmpp-stanzas";
static inline const auto kKebabCaseName = static_cast<std::string>(utils::ToKebabCaseName<T>());
static constexpr auto kErrorMessage = [] -> std::string_view {
static constexpr auto name = nameof::nameof_short_type<T>();
static constexpr auto str = [] {
return std::array{std::string_view{"Stanza IQ Error: "}, std::string_view{name}, std::string_view{"\0", 1}} | std::views::join;
};
static constexpr auto array = str() | std::ranges::to<utils::RangeToWrapper<std::array<char, std::ranges::distance(str())>>>();
return {array.data(), array.size() - 1};
}();
std::optional<std::string> by{};
std::string type;
// TODO(unknown): Add "optional text children" support for stanza error. Check "XML Stanzas" -> "Syntax" for more details
static constexpr auto TryParse(xmlpp::Element* element) -> std::optional<T> {
if(not element) {
return std::nullopt;
}
auto by = element->get_attribute("by");
auto type = element->get_attribute("type");
if(not type) {
return std::nullopt;
}
auto node = element->get_first_child(kKebabCaseName);
if(not node) {
return std::nullopt;
}
T obj;
obj.type = type->get_value();
if(by) {
obj.by = std::optional{by->get_value()};
}
return obj;
}
static constexpr auto Parse(xmlpp::Element* element) -> T {
return TryParse(element).value();
}
friend constexpr auto operator<<(xmlpp::Element* element, const T& obj) -> void {
element->set_attribute("type", obj.type);
if(obj.by) {
element->set_attribute("by", *obj.by);
}
auto node = element->add_child_element(kKebabCaseName);
node->set_namespace_declaration(kDefaultNamespace);
}
constexpr auto operator==(const IqErrorImpl<T>&) const -> bool {
return true;
};
[[nodiscard]] constexpr auto what() const noexcept -> const char* override {
return kErrorMessage.data();
}
};
// Helper class to prevent parsing response stream into an expected return type if its name is an 'error'
struct UnknownIqError : IqBaseError {
static constexpr auto kDefaultName = "stream:error";
static constexpr std::string_view kErrorMessage = "Unknown XMPP stream error";
static constexpr auto TryParse(xmlpp::Element* element) {
return std::optional{UnknownIqError{}};
}
static constexpr auto Parse(xmlpp::Element* element) {
return TryParse(element).value();
}
friend constexpr auto operator<<(xmlpp::Element* element, const UnknownIqError& obj) -> void {
throw std::format("'{}' must never be written into the real stream!", kErrorMessage);
}
constexpr auto operator==(const UnknownIqError&) const -> bool {
return true;
};
[[nodiscard]] constexpr auto what() const noexcept -> const char* override {
return kErrorMessage.data();
}
};
struct BadRequest : IqErrorImpl<BadRequest> {};
struct Conflict : IqErrorImpl<Conflict> {};
struct FeatureNotImplemented : IqErrorImpl<FeatureNotImplemented> {};
struct Forbidden : IqErrorImpl<Forbidden> {};
struct Gone : IqErrorImpl<Gone> {};
struct InternalServerError : IqErrorImpl<InternalServerError> {};
struct ItemNotFound : IqErrorImpl<ItemNotFound> {};
struct JidMalformed : IqErrorImpl<JidMalformed> {};
struct NotAcceptable : IqErrorImpl<NotAcceptable> {};
struct NotAllowed : IqErrorImpl<NotAllowed> {};
struct NotAuthorized : IqErrorImpl<NotAuthorized> {};
struct PolicyViolation : IqErrorImpl<PolicyViolation> {};
struct RecipientUnavailable : IqErrorImpl<RecipientUnavailable> {};
struct Redirect : IqErrorImpl<Redirect> {};
struct RegistrationRequired : IqErrorImpl<RegistrationRequired> {};
struct RemoteServerNotFound : IqErrorImpl<RemoteServerNotFound> {};
struct RemoteServerTimeout : IqErrorImpl<RemoteServerTimeout> {};
struct ResourceConstraint : IqErrorImpl<ResourceConstraint> {};
struct ServiceUnavailable : IqErrorImpl<ServiceUnavailable> {};
struct SubscriptionRequired : IqErrorImpl<SubscriptionRequired> {};
struct UndefinedCondition : IqErrorImpl<UndefinedCondition> {};
struct UnexpectedRequest : IqErrorImpl<UnexpectedRequest> {};
} // namespace impl
using IqError = std::variant<impl::BadRequest,
impl::Conflict,
impl::FeatureNotImplemented,
impl::Forbidden,
impl::Gone,
impl::InternalServerError,
impl::ItemNotFound,
impl::JidMalformed,
impl::NotAcceptable,
impl::NotAllowed,
impl::NotAuthorized,
impl::PolicyViolation,
impl::RecipientUnavailable,
impl::Redirect,
impl::RegistrationRequired,
impl::RemoteServerNotFound,
impl::RemoteServerTimeout,
impl::ResourceConstraint,
impl::ServiceUnavailable,
impl::SubscriptionRequired,
impl::UndefinedCondition,
impl::UnexpectedRequest,
impl::UnknownIqError>;
static_assert(!std::is_same_v<typename std::variant_alternative_t<std::variant_size_v<IqError> - 1, IqError>, IqError>,
"'UnknownIqError' must be at the end of 'IqError' variant");
} // namespace larra::xmpp::iq::error

View file

@ -2,43 +2,10 @@
#include <libxml++/libxml++.h>
#include <larra/utils.hpp>
#include <nameof.hpp>
#include <ranges>
#include <variant>
namespace larra::xmpp {
namespace impl {
// std::isupper not declared as constexpr
constexpr auto IsUpper(char ch) -> bool {
return ch >= 'A' && ch <= 'Z';
}
constexpr auto ToLower(char ch) -> char {
return (ch >= 'A' && ch <= 'Z') ? static_cast<char>(ch + ('a' - 'A')) : ch;
}
template <typename T>
constexpr auto ToKebabCaseName() -> std::string_view {
static constexpr auto rawStr = nameof::nameof_short_type<T>();
constexpr auto str = [] {
return rawStr //
| std::views::transform([](auto ch) {
return impl::IsUpper(ch) ? std::array<char, 2>{'-', impl::ToLower(ch)} : std::array<char, 2>{ch, '\0'};
}) //
| std::views::join //
| std::views::filter([](char ch) {
return ch != '\0';
}) //
| std::views::drop(1);
};
static constexpr auto arr = str() | std::ranges::to<utils::RangeToWrapper<std::array<char, std::ranges::distance(str())>>>();
return {arr.data(), arr.size()};
}
} // namespace impl
namespace error::stream {
struct BaseError : std::exception {};
@ -48,7 +15,7 @@ struct BaseError : std::exception {};
template <typename T>
struct ErrorImpl : BaseError {
static constexpr auto kDefaultName = "stream:error";
static inline const auto kKebabCaseName = static_cast<std::string>(impl::ToKebabCaseName<T>());
static inline const auto kKebabCaseName = static_cast<std::string>(utils::ToKebabCaseName<T>());
static constexpr auto kErrorMessage = [] -> std::string_view {
static constexpr auto name = nameof::nameof_short_type<T>();

View file

@ -1,5 +1,7 @@
#pragma once
#include <boost/pfr.hpp>
#include <nameof.hpp>
#include <ranges>
#include <utempl/utils.hpp>
namespace larra::xmpp::utils {
@ -309,4 +311,31 @@ auto AccumulateFieldLength(const T& obj) -> std::size_t {
return totalLength;
}
template <typename T>
constexpr auto ToKebabCaseName() -> std::string_view {
static constexpr auto rawStr = nameof::nameof_short_type<T>();
// std::isupper and std::tolower are not declared as constexpr
static constexpr auto isUpper = [](char ch) {
return ch >= 'A' && ch <= 'Z';
};
static constexpr auto toLower = [](char ch) {
return (ch >= 'A' && ch <= 'Z') ? static_cast<char>(ch + ('a' - 'A')) : ch;
};
constexpr auto str = [] {
return rawStr //
| std::views::transform([](auto ch) {
return isUpper(ch) ? std::array<char, 2>{'-', toLower(ch)} : std::array<char, 2>{ch, '\0'};
}) //
| std::views::join //
| std::views::filter([](char ch) {
return ch != '\0';
}) //
| std::views::drop(1);
};
static constexpr auto arr = str() | std::ranges::to<utils::RangeToWrapper<std::array<char, std::ranges::distance(str())>>>();
return {arr.data(), arr.size()};
}
} // namespace larra::xmpp::utils

View file

@ -2,7 +2,7 @@
namespace larra::xmpp::iq {
auto operator<<(xmlpp::Element* element, const Bind& bind) -> void {
element->set_attribute("xmlns", Bind::kDefaultNamespace);
element->set_namespace_declaration(Bind::kDefaultNamespace);
if(bind.jid) {
auto* jid_el = element->add_child_element("jid");

View file

@ -21,7 +21,7 @@ auto RosterItem::Parse(xmlpp::Element* element) -> RosterItem {
}
auto operator<<(xmlpp::Element* element, const Roster& self) -> void {
element->set_attribute("xmlns", Roster::kDefaultNamespace);
element->set_namespace_declaration(Roster::kDefaultNamespace);
S::Serialize(element, self);
}
auto Roster::Parse(xmlpp::Element* element) -> Roster {

View file

@ -1,10 +1,23 @@
#include <gtest/gtest.h>
#include <spdlog/spdlog.h>
#include <larra/iq.hpp>
#include <stdexcept>
namespace {
static constexpr auto kExpectedData = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<iq id=\"id\" type=\"get\"><some>37</some></iq>\n";
static constexpr auto kForbiddenErrorData = R"(<?xml version="1.0" encoding="UTF-8"?>
<iq id="42" to="name@server/res" type="error"><error type="auth"><forbidden xmlns="urn:ietf:params:xml:ns:xmpp-stanzas"/></error>
</iq>)";
static constexpr auto kExpectedSetData = R"(<?xml version="1.0" encoding="UTF-8"?>
<iq id="id" type="set"><some>37</some></iq>
)";
static constexpr auto kExpectedData = R"(<?xml version="1.0" encoding="UTF-8"?>
<iq id="id" type="get"><some>37</some></iq>
)";
struct SomeStruct {
int value;
constexpr auto operator==(const SomeStruct&) const -> bool = default;
@ -43,4 +56,101 @@ TEST(IQ, Parse) {
EXPECT_EQ(S::Parse(node), iq);
}
TEST(IQ, ParseForbiddenError) {
std::istringstream xml_stream(kForbiddenErrorData);
xmlpp::DomParser parser;
parser.parse_stream(xml_stream);
using S = Serialization<Iq<SomeStruct>>;
xmlpp::Document* doc = parser.get_document();
auto iqRes = S::Parse(doc->get_root_node());
ASSERT_TRUE(std::holds_alternative<IqError>(iqRes));
auto errorRes = std::get<IqError>(iqRes);
ASSERT_TRUE(std::holds_alternative<iq::error::impl::Forbidden>(errorRes.payload));
}
TEST(IQ, IqErrVisitorThrow) {
std::istringstream xml_stream(kForbiddenErrorData);
xmlpp::DomParser parser;
parser.parse_stream(xml_stream);
using S = Serialization<Iq<SomeStruct>>;
xmlpp::Document* doc = parser.get_document();
auto iqRes = S::Parse(doc->get_root_node());
ASSERT_TRUE(std::holds_alternative<IqError>(iqRes));
static constexpr auto visitorErrMsg = "Test Error";
static constexpr auto throwErrMsg = "Stanza IQ Error: Forbidden";
try {
std::visit(utempl::Overloaded([](iq::Result<SomeStruct> r) {}, IqErrVisitor<SomeStruct>{visitorErrMsg}), std::move(iqRes));
} catch(const iq::error::impl::IqBaseError& err) {
ASSERT_STREQ(throwErrMsg, err.what());
return;
} catch(const std::runtime_error& err) {
ASSERT_TRUE(false) << "Invalid throw type throw";
} catch(...) {
ASSERT_TRUE(false) << "Unexpected throw";
}
ASSERT_TRUE(false) << "Expected throwing an exception due to an error in output";
}
TEST(IQ, IqErrVisitorThrowGet) {
std::istringstream xml_stream(kExpectedData);
xmlpp::DomParser parser;
parser.parse_stream(xml_stream);
using S = Serialization<Iq<SomeStruct>>;
xmlpp::Document* doc = parser.get_document();
auto iqRes = S::Parse(doc->get_root_node());
ASSERT_TRUE(std::holds_alternative<iq::Get<SomeStruct>>(iqRes)) << "\tERROR: Unexpected parse result";
static constexpr auto visitorErrMsg = "Test Error";
static constexpr auto throwErrMsg = "Test Error: 'Get' is an invalid type for IQ result. Expected 'Result' or 'Error'";
try {
std::visit(utempl::Overloaded([](iq::Result<SomeStruct> r) {}, IqErrVisitor<SomeStruct>{"Test Error"}), std::move(iqRes));
} catch(const iq::error::impl::IqBaseError& err) {
ASSERT_TRUE(false) << "\tERROR: Invalid throw type throw";
} catch(const std::runtime_error& err) {
ASSERT_STREQ(throwErrMsg, err.what());
return;
} catch(...) {
ASSERT_TRUE(false) << "\tERROR: Unexpected throw";
}
ASSERT_TRUE(false) << "\tERROR: Expected throwing an exception due to an error in output";
}
TEST(IQ, IqErrVisitorThrowSet) {
std::istringstream xml_stream(kExpectedSetData);
xmlpp::DomParser parser;
parser.parse_stream(xml_stream);
using S = Serialization<Iq<SomeStruct>>;
xmlpp::Document* doc = parser.get_document();
auto iqRes = S::Parse(doc->get_root_node());
ASSERT_TRUE(std::holds_alternative<iq::Set<SomeStruct>>(iqRes)) << "\tERROR: Unexpected parse result";
static constexpr auto visitorErrMsg = "Test Error";
static constexpr auto throwErrMsg = "Test Error: 'Set' is an invalid type for IQ result. Expected 'Result' or 'Error'";
try {
std::visit(utempl::Overloaded([](iq::Result<SomeStruct> r) {}, IqErrVisitor<SomeStruct>{"Test Error"}), std::move(iqRes));
} catch(const iq::error::impl::IqBaseError& err) {
ASSERT_TRUE(false) << "\tERROR: Invalid throw type throw";
} catch(const std::runtime_error& err) {
ASSERT_STREQ(throwErrMsg, err.what());
return;
} catch(...) {
ASSERT_TRUE(false) << "\tERROR: Unexpected throw";
}
ASSERT_TRUE(false) << "\tERROR: Expected throwing an exception due to an error in output";
}
} // namespace larra::xmpp

21
tests/main.cpp Normal file
View file

@ -0,0 +1,21 @@
#include <gtest/gtest.h>
#include <spdlog/spdlog.h>
#include <print>
#include <utility>
class PreconfigureEnvironment : public ::testing::Environment {
public:
void SetUp() override {
spdlog::set_level(spdlog::level::trace);
std::println("\nPreconfigureEnvironment setup:\n\tCompiled max availabel log level: {}\n\tCurrently set log level: {}",
SPDLOG_ACTIVE_LEVEL,
std::to_underlying(spdlog::get_level()));
}
};
auto main(int argc, char** argv) -> int {
::testing::InitGoogleTest(&argc, argv);
::testing::AddGlobalTestEnvironment(new PreconfigureEnvironment); // NOLINT GTest takes ownership
return RUN_ALL_TESTS();
}