forked from boostorg/mqtt5
Validate subscribe requests
Summary: resolves T13305 Reviewers: ivica Reviewed By: ivica Subscribers: miljen, iljazovic Maniphest Tasks: T13305 Differential Revision: https://repo.mireo.local/D26954
This commit is contained in:
@ -22,11 +22,11 @@ may complete with, along with the reasons for their occurrence.
|
||||
]]
|
||||
[[`boost::asio::no_recovery`] [
|
||||
An non-recoverable error occurred during the attempt by the [reflink2 mqtt_client `mqtt_client`]
|
||||
to establish a connection with the Broker. The cause of this error may be attributed to the connection
|
||||
to establish a connection with the Server. The cause of this error may be attributed to the connection
|
||||
related parameters used during the initialization of the [reflink2 mqtt_client `mqtt_client`].
|
||||
]]
|
||||
[[`async_mqtt5::client::error::session_expired`][
|
||||
The Client has established a successful connection with a Broker, but either the session does not exist or has expired.
|
||||
The Client has established a successful connection with a Server, but either the session does not exist or has expired.
|
||||
In cases where the Client had previously set up subscriptions to Topics, these subscriptions are also expired.
|
||||
Therefore, the Client should re-subscribe.
|
||||
This error code is exclusive to completion handlers associated with [refmem mqtt_client async_receive] calls.
|
||||
@ -62,6 +62,22 @@ may complete with, along with the reasons for their occurrence.
|
||||
Client has attempted to use them. See __TOPIC_ALIAS_MAX__.
|
||||
This error code is exclusive to completion handlers associated with [refmem mqtt_client async_publish] calls.
|
||||
]]
|
||||
[[`async_mqtt5::client::error::wildcard_subscription_not_available`] [
|
||||
The Client has attempted to subscribe to multiple Topics using Wildcard Character (`+` and/or `#`).
|
||||
However, the Server does not support Wildcard Subscriptions.
|
||||
This error code is exclusive to completion handlers associated with [refmem mqtt_client async_subscribe] calls.
|
||||
]]
|
||||
[[`async_mqtt5::client::error::subscription_identifier_not_available`] [
|
||||
The Client has attempted to associate a subscription with a Subscription Identifier.
|
||||
However, the Server either does not support Subscription Identifiers or the Subscription Identifier provided
|
||||
is out of range (the Subscription Identifier can have a value of 1 to 268,435,455).
|
||||
This error code is exclusive to completion handlers associated with [refmem mqtt_client async_subscribe] calls.
|
||||
]]
|
||||
[[`async_mqtt5::client::error::shared_subscription_not_available`] [
|
||||
The Client has attempted to establish a Shared Subscription.
|
||||
However, the Server does not support Shared Subscriptions.
|
||||
This error code is exclusive to completion handlers associated with [refmem mqtt_client async_subscribe] calls.
|
||||
]]
|
||||
]
|
||||
|
||||
[endsect]
|
||||
|
104
include/async_mqtt5/detail/topic_validation.hpp
Normal file
104
include/async_mqtt5/detail/topic_validation.hpp
Normal file
@ -0,0 +1,104 @@
|
||||
#ifndef ASYNC_MQTT5_TOPIC_VALIDATION_HPP
|
||||
#define ASYNC_MQTT5_TOPIC_VALIDATION_HPP
|
||||
|
||||
#include <string>
|
||||
|
||||
#include <async_mqtt5/detail/utf8_mqtt.hpp>
|
||||
|
||||
namespace async_mqtt5::detail {
|
||||
|
||||
inline bool is_utf8_no_wildcard(validation_result result) {
|
||||
return result == validation_result::valid;
|
||||
}
|
||||
|
||||
inline bool is_not_empty(size_t sz) {
|
||||
return sz != 0;
|
||||
}
|
||||
|
||||
inline bool is_valid_topic_size(size_t sz) {
|
||||
return is_not_empty(sz) && is_valid_string_size(sz);
|
||||
}
|
||||
|
||||
inline validation_result validate_topic_name(std::string_view str) {
|
||||
return validate_impl(str, is_valid_topic_size, is_utf8_no_wildcard);
|
||||
}
|
||||
|
||||
inline validation_result validate_share_name(std::string_view str) {
|
||||
return validate_impl(str, is_not_empty, is_utf8_no_wildcard);
|
||||
}
|
||||
|
||||
inline validation_result validate_topic_filter(std::string_view str) {
|
||||
if (!is_valid_topic_size(str.size()))
|
||||
return validation_result::invalid;
|
||||
|
||||
constexpr int multi_lvl_wildcard = '#';
|
||||
constexpr int single_lvl_wildcard = '+';
|
||||
|
||||
// must be the last character preceded by '/' or stand alone
|
||||
// #, .../#
|
||||
if (str.back() == multi_lvl_wildcard) {
|
||||
str.remove_suffix(1);
|
||||
|
||||
if (!str.empty() && str.back() != '/')
|
||||
return validation_result::invalid;
|
||||
}
|
||||
|
||||
int last_c = -1;
|
||||
validation_result result;
|
||||
while (!str.empty()) {
|
||||
int c = pop_front_unichar(str);
|
||||
|
||||
// can be used at any level, but must occupy an entire level
|
||||
// +, +/..., .../+/..., .../+
|
||||
bool is_valid_single_lvl = (c == single_lvl_wildcard) &&
|
||||
(str.empty() || str.front() == '/') &&
|
||||
(last_c == -1 || last_c == '/');
|
||||
|
||||
result = validate_mqtt_utf8_char(c);
|
||||
if (
|
||||
result == validation_result::valid ||
|
||||
is_valid_single_lvl
|
||||
) {
|
||||
last_c = c;
|
||||
continue;
|
||||
}
|
||||
|
||||
return validation_result::invalid;
|
||||
}
|
||||
|
||||
return validation_result::valid;
|
||||
}
|
||||
|
||||
inline validation_result validate_shared_topic_filter(
|
||||
std::string_view str, bool wildcard_allowed = true
|
||||
) {
|
||||
constexpr std::string_view shared_sub_id = "$share/";
|
||||
|
||||
if (!is_valid_topic_size(str.size()))
|
||||
return validation_result::invalid;
|
||||
|
||||
if (str.compare(0, shared_sub_id.size(), shared_sub_id) != 0)
|
||||
return validation_result::invalid;
|
||||
|
||||
str.remove_prefix(shared_sub_id.size());
|
||||
|
||||
size_t share_name_end = str.find_first_of('/');
|
||||
if (share_name_end == std::string::npos)
|
||||
return validation_result::invalid;
|
||||
|
||||
validation_result result;
|
||||
result = validate_share_name(str.substr(0, share_name_end));
|
||||
|
||||
if (result != validation_result::valid)
|
||||
return validation_result::invalid;
|
||||
|
||||
auto topic_filter = str.substr(share_name_end + 1);
|
||||
return wildcard_allowed ?
|
||||
validate_topic_filter(topic_filter) :
|
||||
validate_topic_name(topic_filter)
|
||||
;
|
||||
}
|
||||
|
||||
} // end namespace async_mqtt5::detail
|
||||
|
||||
#endif //ASYNC_MQTT5_TOPIC_VALIDATION_HPP
|
@ -5,6 +5,12 @@
|
||||
|
||||
namespace async_mqtt5::detail {
|
||||
|
||||
enum class validation_result : uint8_t {
|
||||
valid = 0,
|
||||
has_wildcard_character,
|
||||
invalid
|
||||
};
|
||||
|
||||
inline int pop_front_unichar(std::string_view& s) {
|
||||
// assuming that s.length() is > 0
|
||||
|
||||
@ -32,20 +38,26 @@ inline int pop_front_unichar(std::string_view& s) {
|
||||
return ch;
|
||||
}
|
||||
|
||||
inline bool is_valid_mqtt_utf8_char(int c) {
|
||||
inline validation_result validate_mqtt_utf8_char(int c) {
|
||||
constexpr int fe_flag = 0xFE;
|
||||
constexpr int ff_flag = 0xFF;
|
||||
|
||||
return c > 0x001F && // U+0000...U+001F control characters
|
||||
constexpr int multi_lvl_wildcard = '#';
|
||||
constexpr int single_lvl_wildcard = '+';
|
||||
|
||||
if (c == multi_lvl_wildcard || c == single_lvl_wildcard)
|
||||
return validation_result::has_wildcard_character;
|
||||
|
||||
if (c > 0x001F && // U+0000...U+001F control characters
|
||||
(c < 0x007F || c > 0x009F) && // U+007F...0+009F control characters
|
||||
(c < 0xD800 || c > 0xDFFF) && // U+D800...U+DFFF surrogates
|
||||
(c < 0xFDD0 || c > 0xFDEF) && // U+FDD0...U+FDEF non-characters
|
||||
(c & fe_flag) != fe_flag && // non-characters
|
||||
(c & ff_flag) != ff_flag;
|
||||
}
|
||||
(c & ff_flag) != ff_flag
|
||||
)
|
||||
return validation_result::valid;
|
||||
|
||||
inline bool is_valid_mqtt_utf8_non_wildcard_char(int c) {
|
||||
return c != '+' && c != '#' && is_valid_mqtt_utf8_char(c);
|
||||
return validation_result::invalid;
|
||||
}
|
||||
|
||||
inline bool is_valid_string_size(size_t sz) {
|
||||
@ -53,74 +65,33 @@ inline bool is_valid_string_size(size_t sz) {
|
||||
return sz <= max_sz;
|
||||
}
|
||||
|
||||
inline bool is_valid_topic_size(size_t sz) {
|
||||
constexpr size_t min_sz = 1;
|
||||
return min_sz <= sz && is_valid_string_size(sz);
|
||||
inline bool is_utf8(validation_result result) {
|
||||
return result == validation_result::valid ||
|
||||
result == validation_result::has_wildcard_character;
|
||||
}
|
||||
|
||||
template <typename ValidationFun>
|
||||
bool is_valid_impl(
|
||||
std::string_view str, ValidationFun&& validation_fun
|
||||
template <typename ValidSizeCondition, typename ValidCondition>
|
||||
validation_result validate_impl(
|
||||
std::string_view str,
|
||||
ValidSizeCondition&& size_condition, ValidCondition&& condition
|
||||
) {
|
||||
while (!str.empty()) {
|
||||
int c = pop_front_unichar(str);
|
||||
bool is_valid = validation_fun(c);
|
||||
if (!size_condition(str.size()))
|
||||
return validation_result::invalid;
|
||||
|
||||
if (!is_valid)
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
inline bool is_valid_mqtt_utf8(std::string_view str) {
|
||||
return is_valid_string_size(str.size()) &&
|
||||
is_valid_impl(str, is_valid_mqtt_utf8_char);
|
||||
}
|
||||
|
||||
inline bool is_valid_topic_name(std::string_view str) {
|
||||
return is_valid_topic_size(str.size()) &&
|
||||
is_valid_impl(str, is_valid_mqtt_utf8_non_wildcard_char);
|
||||
}
|
||||
|
||||
inline bool is_valid_topic_filter(std::string_view str) {
|
||||
if (!is_valid_topic_size(str.size()))
|
||||
return false;
|
||||
|
||||
constexpr int multi_lvl_wildcard = '#';
|
||||
constexpr int single_lvl_wildcard = '+';
|
||||
|
||||
// must be the last character preceded by '/' or stand alone
|
||||
// #, .../#
|
||||
if (str.back() == multi_lvl_wildcard) {
|
||||
str.remove_suffix(1);
|
||||
|
||||
if (!str.empty() && str.back() != '/')
|
||||
return false;
|
||||
}
|
||||
|
||||
int last_c = -1;
|
||||
validation_result result;
|
||||
while (!str.empty()) {
|
||||
int c = pop_front_unichar(str);
|
||||
|
||||
// can be used at any level, but must occupy an entire level
|
||||
// +, +/..., .../+/..., .../+
|
||||
bool is_valid_single_lvl = (c == single_lvl_wildcard) &&
|
||||
(str.empty() || str.front() == '/') &&
|
||||
(last_c == -1 || last_c == '/');
|
||||
|
||||
bool is_valid_mqtt_utf_8 = is_valid_mqtt_utf8_non_wildcard_char(c);
|
||||
|
||||
|
||||
if (is_valid_mqtt_utf_8 || is_valid_single_lvl) {
|
||||
last_c = c;
|
||||
continue;
|
||||
}
|
||||
|
||||
return false;
|
||||
result = validate_mqtt_utf8_char(c);
|
||||
if (!condition(result))
|
||||
return result;
|
||||
}
|
||||
|
||||
return true;
|
||||
return validation_result::valid;
|
||||
}
|
||||
|
||||
inline validation_result is_valid_mqtt_utf8(std::string_view str) {
|
||||
return validate_impl(str, is_valid_string_size, is_utf8);
|
||||
}
|
||||
|
||||
} // namespace async_mqtt5::detail
|
||||
|
@ -71,11 +71,21 @@ enum class error : int {
|
||||
/** The Server does not support the specified \ref qos_e. */
|
||||
qos_not_supported,
|
||||
|
||||
/** The Server dos not support retained messages. */
|
||||
/** The Server does not support retained messages. */
|
||||
retain_not_available,
|
||||
|
||||
/** The Client attempted to send a Topic Alias that is greater than Topic Alias Maximum. */
|
||||
topic_alias_maximum_reached
|
||||
topic_alias_maximum_reached,
|
||||
|
||||
// subscribe
|
||||
/** The Server does not support Wildcard Subscriptions. */
|
||||
wildcard_subscription_not_available,
|
||||
|
||||
/** The Server does not support this Subscription Identifier. */
|
||||
subscription_identifier_not_available,
|
||||
|
||||
/** The Server does not support Shared Subscriptions. */
|
||||
shared_subscription_not_available,
|
||||
};
|
||||
|
||||
|
||||
@ -97,6 +107,12 @@ inline std::string client_error_to_string(error err) {
|
||||
case error::topic_alias_maximum_reached:
|
||||
return "The Client attempted to send a Topic Alias "
|
||||
"that is greater than Topic Alias Maximum.";
|
||||
case error::wildcard_subscription_not_available:
|
||||
return "The Server does not support Wildcard Subscriptions.";
|
||||
case error::subscription_identifier_not_available:
|
||||
return "The Server does not support this Subscription Identifier.";
|
||||
case error::shared_subscription_not_available:
|
||||
return "The Server does not support Shared Subscriptions.";
|
||||
default:
|
||||
return "Unknown client error";
|
||||
}
|
||||
|
@ -1,6 +1,8 @@
|
||||
#ifndef ASYNC_MQTT5_CLIENT_SERVICE_HPP
|
||||
#define ASYNC_MQTT5_CLIENT_SERVICE_HPP
|
||||
|
||||
#include <utility>
|
||||
|
||||
#include <boost/asio/experimental/basic_concurrent_channel.hpp>
|
||||
|
||||
#include <async_mqtt5/detail/channel_traits.hpp>
|
||||
@ -62,14 +64,15 @@ public:
|
||||
template <typename Prop>
|
||||
decltype(auto) connack_prop(Prop p) {
|
||||
std::shared_lock reader_lock(_mqtt_context.ca_mtx);
|
||||
return _mqtt_context.ca_props[p];
|
||||
return std::as_const(_mqtt_context.ca_props[p]);
|
||||
}
|
||||
|
||||
template <typename Prop0, typename ...Props>
|
||||
decltype(auto) connack_props(Prop0 p0, Props ...props) {
|
||||
std::shared_lock reader_lock(_mqtt_context.ca_mtx);
|
||||
return std::make_tuple(
|
||||
_mqtt_context.ca_props[p0], _mqtt_context.ca_props[props]...
|
||||
std::as_const(_mqtt_context.ca_props[p0]),
|
||||
std::as_const(_mqtt_context.ca_props[props])...
|
||||
);
|
||||
}
|
||||
|
||||
@ -119,14 +122,15 @@ public:
|
||||
template <typename Prop>
|
||||
decltype(auto) connack_prop(Prop p) {
|
||||
std::shared_lock reader_lock(_mqtt_context.ca_mtx);
|
||||
return _mqtt_context.ca_props[p];
|
||||
return std::as_const(_mqtt_context.ca_props[p]);
|
||||
}
|
||||
|
||||
template <typename Prop0, typename ...Props>
|
||||
decltype(auto) connack_props(Prop0 p0, Props ...props) {
|
||||
std::shared_lock reader_lock(_mqtt_context.ca_mtx);
|
||||
return std::make_tuple(
|
||||
_mqtt_context.ca_props[p0], _mqtt_context.ca_props[props]...
|
||||
std::as_const(_mqtt_context.ca_props[p0]),
|
||||
std::as_const(_mqtt_context.ca_props[props])...
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -368,7 +368,7 @@ using encoder_types = std::tuple<
|
||||
prop_encoder_type<pp::content_type_t, basic::utf8_def>,
|
||||
prop_encoder_type<pp::response_topic_t, basic::utf8_def>,
|
||||
prop_encoder_type<pp::correlation_data_t, basic::utf8_def>,
|
||||
prop_encoder_type<pp::subscription_identifier_t, basic::int_def<uint32_t>>,
|
||||
prop_encoder_type<pp::subscription_identifier_t, basic::int_def<intptr_t>>, // varint
|
||||
prop_encoder_type<pp::session_expiry_interval_t, basic::int_def<int32_t>>,
|
||||
prop_encoder_type<pp::assigned_client_identifier_t, basic::utf8_def>,
|
||||
prop_encoder_type<pp::server_keep_alive_t, basic::int_def<int16_t>>,
|
||||
|
@ -10,7 +10,7 @@
|
||||
#include <async_mqtt5/detail/cancellable_handler.hpp>
|
||||
#include <async_mqtt5/detail/control_packet.hpp>
|
||||
#include <async_mqtt5/detail/internal_types.hpp>
|
||||
#include <async_mqtt5/detail/utf8_mqtt.hpp>
|
||||
#include <async_mqtt5/detail/topic_validation.hpp>
|
||||
|
||||
#include <async_mqtt5/impl/disconnect_op.hpp>
|
||||
#include <async_mqtt5/impl/codecs/message_decoders.hpp>
|
||||
@ -343,7 +343,7 @@ private:
|
||||
error_code validate_publish(
|
||||
const std::string& topic, retain_e retain, const publish_props& props
|
||||
) {
|
||||
if (!is_valid_topic_name(topic))
|
||||
if (validate_topic_name(topic) != validation_result::valid)
|
||||
return client::error::invalid_topic;
|
||||
|
||||
const auto& [max_qos, retain_avail, topic_alias_max] =
|
||||
@ -368,7 +368,7 @@ private:
|
||||
if (topic_alias_max && topic_alias && *topic_alias > *topic_alias_max)
|
||||
return client::error::topic_alias_maximum_reached;
|
||||
|
||||
return {};
|
||||
return error_code {};
|
||||
}
|
||||
|
||||
void on_malformed_packet(const std::string& reason) {
|
||||
|
@ -2,6 +2,7 @@
|
||||
#define ASYNC_MQTT5_SUBSCRIBE_OP_HPP
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdint>
|
||||
|
||||
#include <boost/asio/detached.hpp>
|
||||
|
||||
@ -11,7 +12,7 @@
|
||||
#include <async_mqtt5/detail/cancellable_handler.hpp>
|
||||
#include <async_mqtt5/detail/control_packet.hpp>
|
||||
#include <async_mqtt5/detail/internal_types.hpp>
|
||||
#include <async_mqtt5/detail/utf8_mqtt.hpp>
|
||||
#include <async_mqtt5/detail/topic_validation.hpp>
|
||||
|
||||
#include <async_mqtt5/impl/codecs/message_decoders.hpp>
|
||||
#include <async_mqtt5/impl/codecs/message_encoders.hpp>
|
||||
@ -62,7 +63,7 @@ public:
|
||||
const std::vector<subscribe_topic>& topics,
|
||||
const subscribe_props& props
|
||||
) {
|
||||
auto ec = validate_topics(topics);
|
||||
auto ec = validate_subscribe(topics, props);
|
||||
if (ec)
|
||||
return complete_post(ec, topics.size());
|
||||
|
||||
@ -148,15 +149,80 @@ public:
|
||||
|
||||
private:
|
||||
|
||||
static error_code validate_topics(
|
||||
const std::vector<subscribe_topic>& topics
|
||||
static bool is_option_available(std::optional<uint8_t> sub_opt) {
|
||||
return !sub_opt.has_value() || *sub_opt == 1;
|
||||
}
|
||||
|
||||
static error_code validate_props(
|
||||
const subscribe_props& props, bool sub_id_available
|
||||
) {
|
||||
for (const auto& topic: topics)
|
||||
if (!is_valid_topic_filter(topic.topic_filter))
|
||||
return client::error::invalid_topic;
|
||||
auto sub_id = props[prop::subscription_identifier];
|
||||
if (!sub_id.has_value())
|
||||
return error_code {};
|
||||
|
||||
if (!sub_id_available)
|
||||
return client::error::subscription_identifier_not_available;
|
||||
|
||||
constexpr uint32_t min_sub_id = 1;
|
||||
constexpr uint32_t max_sub_id = 268'435'455;
|
||||
return min_sub_id <= *sub_id && *sub_id <= max_sub_id ?
|
||||
error_code {} :
|
||||
client::error::subscription_identifier_not_available;
|
||||
}
|
||||
|
||||
static error_code validate_topic(
|
||||
const subscribe_topic& topic, bool wildcard_available, bool shared_available
|
||||
) {
|
||||
std::string_view topic_filter = topic.topic_filter;
|
||||
|
||||
constexpr std::string_view shared_sub_id = "$share/";
|
||||
validation_result result = validation_result::valid;
|
||||
if (
|
||||
topic_filter.compare(0, shared_sub_id.size(), shared_sub_id) == 0
|
||||
) {
|
||||
if (!shared_available)
|
||||
return client::error::shared_subscription_not_available;
|
||||
|
||||
result = validate_shared_topic_filter(topic_filter, wildcard_available);
|
||||
} else
|
||||
result = wildcard_available ?
|
||||
validate_topic_filter(topic_filter) :
|
||||
validate_topic_name(topic_filter);
|
||||
|
||||
if (result == validation_result::invalid)
|
||||
return client::error::invalid_topic;
|
||||
if (!wildcard_available && result != validation_result::valid)
|
||||
return client::error::wildcard_subscription_not_available;
|
||||
return error_code {};
|
||||
}
|
||||
|
||||
error_code validate_subscribe(
|
||||
const std::vector<subscribe_topic>& topics,
|
||||
const subscribe_props& props
|
||||
) {
|
||||
auto [wildcard_available, shared_available, sub_id_available] =
|
||||
std::apply(
|
||||
[](auto ...opt) {
|
||||
return std::make_tuple(is_option_available(opt)...);
|
||||
},
|
||||
_svc_ptr->connack_props(
|
||||
prop::wildcard_subscription_available,
|
||||
prop::shared_subscription_available,
|
||||
prop::subscription_identifier_available
|
||||
)
|
||||
);
|
||||
|
||||
error_code ec;
|
||||
for (const auto& topic: topics) {
|
||||
ec = validate_topic(topic, wildcard_available, shared_available);
|
||||
if (ec)
|
||||
return ec;
|
||||
}
|
||||
|
||||
ec = validate_props(props, sub_id_available);
|
||||
return ec;
|
||||
}
|
||||
|
||||
static std::vector<reason_code> to_reason_codes(
|
||||
std::vector<uint8_t> codes
|
||||
) {
|
||||
|
@ -9,7 +9,7 @@
|
||||
#include <async_mqtt5/detail/cancellable_handler.hpp>
|
||||
#include <async_mqtt5/detail/control_packet.hpp>
|
||||
#include <async_mqtt5/detail/internal_types.hpp>
|
||||
#include <async_mqtt5/detail/utf8_mqtt.hpp>
|
||||
#include <async_mqtt5/detail/topic_validation.hpp>
|
||||
|
||||
#include <async_mqtt5/impl/disconnect_op.hpp>
|
||||
#include <async_mqtt5/impl/codecs/message_decoders.hpp>
|
||||
@ -149,7 +149,7 @@ private:
|
||||
|
||||
static error_code validate_topics(const std::vector<std::string>& topics) {
|
||||
for (const auto& topic : topics)
|
||||
if (!is_valid_topic_filter(topic))
|
||||
if (validate_topic_filter(topic) != validation_result::valid)
|
||||
return client::error::invalid_topic;
|
||||
return error_code {};
|
||||
}
|
||||
|
@ -438,6 +438,9 @@ public:
|
||||
* - `boost::asio::error::operation_aborted` \n
|
||||
* - \link async_mqtt5::client::error::pid_overrun \endlink
|
||||
* - \link async_mqtt5::client::error::invalid_topic \endlink
|
||||
* - \link async_mqtt5::client::error::wildcard_subscription_not_available \endlink
|
||||
* - \link async_mqtt5::client::error::subscription_identifier_not_available \endlink
|
||||
* - \link async_mqtt5::client::error::shared_subscription_not_available \endlink
|
||||
*
|
||||
* Refer to the section on \__ERROR_HANDLING\__ to find the underlying causes for each error code.
|
||||
*/
|
||||
@ -505,6 +508,9 @@ public:
|
||||
* - `boost::asio::error::operation_aborted` \n
|
||||
* - \link async_mqtt5::client::error::pid_overrun \endlink
|
||||
* - \link async_mqtt5::client::error::invalid_topic \endlink
|
||||
* - \link async_mqtt5::client::error::wildcard_subscription_not_available \endlink
|
||||
* - \link async_mqtt5::client::error::subscription_identifier_not_available \endlink
|
||||
* - \link async_mqtt5::client::error::shared_subscription_not_available \endlink
|
||||
*
|
||||
* Refer to the section on \__ERROR_HANDLING\__ to find the underlying causes for each error code.
|
||||
*/
|
||||
|
@ -6,6 +6,8 @@
|
||||
#include <boost/asio/post.hpp>
|
||||
#include <boost/asio/prepend.hpp>
|
||||
|
||||
#include <async_mqtt5/types.hpp>
|
||||
|
||||
#include <async_mqtt5/impl/client_service.hpp>
|
||||
|
||||
namespace async_mqtt5::test {
|
||||
@ -21,11 +23,16 @@ class test_service : public detail::client_service<StreamType, TlsContext> {
|
||||
using base = detail::client_service<StreamType, TlsContext>;
|
||||
|
||||
asio::any_io_executor _ex;
|
||||
connack_props _test_props;
|
||||
public:
|
||||
test_service(const asio::any_io_executor ex)
|
||||
: base(ex, {}), _ex(ex)
|
||||
{}
|
||||
|
||||
test_service(const asio::any_io_executor ex, connack_props props)
|
||||
: base(ex, {}), _ex(ex), _test_props(std::move(props))
|
||||
{}
|
||||
|
||||
template <typename BufferType, typename CompletionToken>
|
||||
decltype(auto) async_send(
|
||||
const BufferType&, uint32_t, unsigned,
|
||||
@ -42,6 +49,21 @@ public:
|
||||
CompletionToken, void (error_code)
|
||||
> (std::move(initiation), token);
|
||||
}
|
||||
|
||||
template <typename Prop>
|
||||
decltype(auto) connack_prop(Prop p) {
|
||||
return std::as_const(_test_props[p]);
|
||||
}
|
||||
|
||||
template <typename Prop0, typename ...Props>
|
||||
decltype(auto) connack_props(Prop0 p0, Props ...props) {
|
||||
return std::make_tuple(
|
||||
std::as_const(_test_props[p0]),
|
||||
std::as_const(_test_props[props])...
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
};
|
||||
|
||||
|
||||
|
@ -180,7 +180,12 @@ BOOST_AUTO_TEST_CASE(test_puback) {
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_CASE(test_subscribe) {
|
||||
//testing variables
|
||||
uint32_t sub_id = 1'234'567;
|
||||
|
||||
subscribe_props sp;
|
||||
sp[prop::subscription_identifier] = sub_id;
|
||||
|
||||
std::vector<subscribe_topic> filters {
|
||||
{ "subscribe topic", { qos_e::at_least_once } }
|
||||
};
|
||||
@ -200,7 +205,8 @@ BOOST_AUTO_TEST_CASE(test_subscribe) {
|
||||
BOOST_CHECK_MESSAGE(rv, "Parsing SUBSCRIBE failed.");
|
||||
|
||||
const auto& [props_, filters_] = *rv;
|
||||
BOOST_CHECK_EQUAL(filters[0].topic_filter, std::get<0>(filters_[0]));
|
||||
BOOST_CHECK_EQUAL(std::get<0>(filters_[0]), filters[0].topic_filter);
|
||||
BOOST_CHECK_EQUAL(*props_[prop::subscription_identifier], sub_id);
|
||||
//TODO: sub options
|
||||
}
|
||||
|
||||
|
@ -1,5 +1,8 @@
|
||||
#include <boost/test/unit_test.hpp>
|
||||
|
||||
#include <string>
|
||||
|
||||
#include <async_mqtt5/detail/topic_validation.hpp>
|
||||
#include <async_mqtt5/detail/utf8_mqtt.hpp>
|
||||
|
||||
BOOST_AUTO_TEST_SUITE(utf8_mqtt/*, *boost::unit_test::disabled()*/)
|
||||
@ -29,76 +32,95 @@ std::string to_str(int utf8ch) {
|
||||
BOOST_AUTO_TEST_CASE(utf8_string_validation) {
|
||||
using namespace async_mqtt5::detail;
|
||||
|
||||
BOOST_CHECK_EQUAL(is_valid_mqtt_utf8("stringy"), true);
|
||||
BOOST_CHECK_EQUAL(is_valid_mqtt_utf8(""), true);
|
||||
BOOST_CHECK_EQUAL(is_valid_mqtt_utf8(std::string(75000, 'a')), false);
|
||||
BOOST_CHECK(is_valid_mqtt_utf8("stringy") == validation_result::valid);
|
||||
BOOST_CHECK(is_valid_mqtt_utf8("") == validation_result::valid);
|
||||
BOOST_CHECK(is_valid_mqtt_utf8(std::string(75000, 'a')) == validation_result::invalid);
|
||||
|
||||
BOOST_CHECK_EQUAL(is_valid_mqtt_utf8(to_str(0x1)), false);
|
||||
BOOST_CHECK_EQUAL(is_valid_mqtt_utf8(to_str(0x1F)), false);
|
||||
BOOST_CHECK_EQUAL(is_valid_mqtt_utf8(to_str(0x20)), true);
|
||||
BOOST_CHECK_EQUAL(is_valid_mqtt_utf8(to_str(0x7E)), true);
|
||||
BOOST_CHECK_EQUAL(is_valid_mqtt_utf8(to_str(0x7F)), false);
|
||||
BOOST_CHECK_EQUAL(is_valid_mqtt_utf8(to_str(0x9F)), false);
|
||||
BOOST_CHECK_EQUAL(is_valid_mqtt_utf8(to_str(0xA0)), true);
|
||||
BOOST_CHECK_EQUAL(is_valid_mqtt_utf8(to_str(0xD800)), false);
|
||||
BOOST_CHECK_EQUAL(is_valid_mqtt_utf8(to_str(0xDFFF)), false);
|
||||
BOOST_CHECK_EQUAL(is_valid_mqtt_utf8(to_str(0xFDD0)), false);
|
||||
BOOST_CHECK_EQUAL(is_valid_mqtt_utf8(to_str(0xFDEF)), false);
|
||||
BOOST_CHECK_EQUAL(is_valid_mqtt_utf8(to_str(0xFDF0)), true);
|
||||
BOOST_CHECK_EQUAL(is_valid_mqtt_utf8(to_str(0x1FFFE)), false);
|
||||
BOOST_CHECK_EQUAL(is_valid_mqtt_utf8(to_str(0x1FFFF)), false);
|
||||
BOOST_CHECK(is_valid_mqtt_utf8(to_str(0x1)) == validation_result::invalid);
|
||||
BOOST_CHECK(is_valid_mqtt_utf8(to_str(0x1F)) == validation_result::invalid);
|
||||
BOOST_CHECK(is_valid_mqtt_utf8(to_str(0x20)) == validation_result::valid);
|
||||
BOOST_CHECK(is_valid_mqtt_utf8(to_str(0x7E)) == validation_result::valid);
|
||||
BOOST_CHECK(is_valid_mqtt_utf8(to_str(0x7F)) == validation_result::invalid);
|
||||
BOOST_CHECK(is_valid_mqtt_utf8(to_str(0x9F)) == validation_result::invalid);
|
||||
BOOST_CHECK(is_valid_mqtt_utf8(to_str(0xA0)) == validation_result::valid);
|
||||
BOOST_CHECK(is_valid_mqtt_utf8(to_str(0xD800)) == validation_result::invalid);
|
||||
BOOST_CHECK(is_valid_mqtt_utf8(to_str(0xDFFF)) == validation_result::invalid);
|
||||
BOOST_CHECK(is_valid_mqtt_utf8(to_str(0xFDD0)) == validation_result::invalid);
|
||||
BOOST_CHECK(is_valid_mqtt_utf8(to_str(0xFDEF)) == validation_result::invalid);
|
||||
BOOST_CHECK(is_valid_mqtt_utf8(to_str(0xFDF0)) == validation_result::valid);
|
||||
BOOST_CHECK(is_valid_mqtt_utf8(to_str(0x1FFFE)) == validation_result::invalid);
|
||||
BOOST_CHECK(is_valid_mqtt_utf8(to_str(0x1FFFF)) == validation_result::invalid);
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_CASE(topic_filter_validation) {
|
||||
using namespace async_mqtt5::detail;
|
||||
|
||||
BOOST_CHECK_EQUAL(is_valid_topic_filter(""), false);
|
||||
BOOST_CHECK_EQUAL(is_valid_topic_filter("topic"), true);
|
||||
BOOST_CHECK_EQUAL(is_valid_topic_filter("topic/subtopic"), true);
|
||||
BOOST_CHECK(validate_topic_filter("") == validation_result::invalid);
|
||||
BOOST_CHECK(validate_topic_filter("topic") == validation_result::valid);
|
||||
BOOST_CHECK(validate_topic_filter("topic/subtopic") == validation_result::valid);
|
||||
|
||||
BOOST_CHECK_EQUAL(is_valid_topic_filter("#"), true);
|
||||
BOOST_CHECK_EQUAL(is_valid_topic_filter("#sport"), false);
|
||||
BOOST_CHECK_EQUAL(is_valid_topic_filter("sport#"), false);
|
||||
BOOST_CHECK_EQUAL(is_valid_topic_filter("sport/#/tennis"), false);
|
||||
BOOST_CHECK_EQUAL(is_valid_topic_filter("#/sport"), false);
|
||||
BOOST_CHECK_EQUAL(is_valid_topic_filter("spo#rt/#"), false);
|
||||
BOOST_CHECK_EQUAL(is_valid_topic_filter("sport/#"), true);
|
||||
BOOST_CHECK_EQUAL(is_valid_topic_filter("sport/tennis/#"), true);
|
||||
BOOST_CHECK_EQUAL(is_valid_topic_filter("sport/tennis#"), false);
|
||||
BOOST_CHECK(validate_topic_filter("#") == validation_result::valid);
|
||||
BOOST_CHECK(validate_topic_filter("#sport") == validation_result::invalid);
|
||||
BOOST_CHECK(validate_topic_filter("sport#") == validation_result::invalid);
|
||||
BOOST_CHECK(validate_topic_filter("sport/#/tennis") == validation_result::invalid);
|
||||
BOOST_CHECK(validate_topic_filter("#/sport") == validation_result::invalid);
|
||||
BOOST_CHECK(validate_topic_filter("spo#rt/#") == validation_result::invalid);
|
||||
BOOST_CHECK(validate_topic_filter("sport/#") == validation_result::valid);
|
||||
BOOST_CHECK(validate_topic_filter("sport/tennis/#") == validation_result::valid);
|
||||
BOOST_CHECK(validate_topic_filter("sport/tennis#") == validation_result::invalid);
|
||||
|
||||
BOOST_CHECK_EQUAL(is_valid_topic_filter("+"), true);
|
||||
BOOST_CHECK_EQUAL(is_valid_topic_filter("+/"), true);
|
||||
BOOST_CHECK_EQUAL(is_valid_topic_filter("/+"), true);
|
||||
BOOST_CHECK_EQUAL(is_valid_topic_filter("+/+"), true);
|
||||
BOOST_CHECK_EQUAL(is_valid_topic_filter("+/+/+"), true);
|
||||
BOOST_CHECK_EQUAL(is_valid_topic_filter("+sport"), false);
|
||||
BOOST_CHECK_EQUAL(is_valid_topic_filter("sport+"), false);
|
||||
BOOST_CHECK_EQUAL(is_valid_topic_filter("sport+/player1"), false);
|
||||
BOOST_CHECK_EQUAL(is_valid_topic_filter("sport/+player1"), false);
|
||||
BOOST_CHECK_EQUAL(is_valid_topic_filter("sport/+"), true);
|
||||
BOOST_CHECK_EQUAL(is_valid_topic_filter("sport/+/player1"), true);
|
||||
BOOST_CHECK_EQUAL(is_valid_topic_filter("+/sport/+/player1/+"), true);
|
||||
BOOST_CHECK(validate_topic_filter("+") == validation_result::valid);
|
||||
BOOST_CHECK(validate_topic_filter("+/") == validation_result::valid);
|
||||
BOOST_CHECK(validate_topic_filter("/+") == validation_result::valid);
|
||||
BOOST_CHECK(validate_topic_filter("+/+") == validation_result::valid);
|
||||
BOOST_CHECK(validate_topic_filter("+/+/+") == validation_result::valid);
|
||||
BOOST_CHECK(validate_topic_filter("+sport") == validation_result::invalid);
|
||||
BOOST_CHECK(validate_topic_filter("sport+") == validation_result::invalid);
|
||||
BOOST_CHECK(validate_topic_filter("sport+/player1") == validation_result::invalid);
|
||||
BOOST_CHECK(validate_topic_filter("sport/+player1") == validation_result::invalid);
|
||||
BOOST_CHECK(validate_topic_filter("sport/+") == validation_result::valid);
|
||||
BOOST_CHECK(validate_topic_filter("sport/+/player1") == validation_result::valid);
|
||||
BOOST_CHECK(validate_topic_filter("+/sport/+/player1/+") == validation_result::valid);
|
||||
|
||||
BOOST_CHECK_EQUAL(is_valid_topic_filter("+/tennis/#"), true);
|
||||
BOOST_CHECK(validate_topic_filter("+/tennis/#") == validation_result::valid);
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_CASE(topic_name_validation) {
|
||||
using namespace async_mqtt5::detail;
|
||||
|
||||
BOOST_CHECK_EQUAL(is_valid_topic_name(""), false);
|
||||
BOOST_CHECK_EQUAL(is_valid_topic_name("topic"), true);
|
||||
BOOST_CHECK_EQUAL(is_valid_topic_name("topic/subtopic"), true);
|
||||
BOOST_CHECK(validate_topic_name("") == validation_result::invalid);
|
||||
BOOST_CHECK(validate_topic_name("topic") == validation_result::valid);
|
||||
BOOST_CHECK(validate_topic_name("topic/subtopic") == validation_result::valid);
|
||||
|
||||
BOOST_CHECK_EQUAL(is_valid_topic_name("#"), false);
|
||||
BOOST_CHECK_EQUAL(is_valid_topic_name("sport#"), false);
|
||||
BOOST_CHECK_EQUAL(is_valid_topic_name("sport/#"), false);
|
||||
BOOST_CHECK(validate_topic_name("#") == validation_result::has_wildcard_character);
|
||||
BOOST_CHECK(validate_topic_name("sport#") == validation_result::has_wildcard_character);
|
||||
BOOST_CHECK(validate_topic_name("sport/#") == validation_result::has_wildcard_character);
|
||||
|
||||
BOOST_CHECK_EQUAL(is_valid_topic_name("+"), false);
|
||||
BOOST_CHECK_EQUAL(is_valid_topic_name("+sport"), false);
|
||||
BOOST_CHECK_EQUAL(is_valid_topic_name("sport+"), false);
|
||||
BOOST_CHECK_EQUAL(is_valid_topic_name("sport/+/player1"), false);
|
||||
BOOST_CHECK(validate_topic_name("+") == validation_result::has_wildcard_character);
|
||||
BOOST_CHECK(validate_topic_name("+sport") == validation_result::has_wildcard_character);
|
||||
BOOST_CHECK(validate_topic_name("sport+") == validation_result::has_wildcard_character);
|
||||
BOOST_CHECK(validate_topic_name("sport/+/player1") == validation_result::has_wildcard_character);
|
||||
|
||||
BOOST_CHECK_EQUAL(is_valid_topic_name("+/tennis/#"), false);
|
||||
BOOST_CHECK(validate_topic_name("+/tennis/#") == validation_result::has_wildcard_character);
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_CASE(shared_topic_filter_validation) {
|
||||
using namespace async_mqtt5::detail;
|
||||
|
||||
BOOST_CHECK(validate_shared_topic_filter("") == validation_result::invalid);
|
||||
BOOST_CHECK(validate_shared_topic_filter("$shared/grp/topic") == validation_result::invalid);
|
||||
BOOST_CHECK(validate_shared_topic_filter("$share//grp/topic") == validation_result::invalid);
|
||||
BOOST_CHECK(validate_shared_topic_filter("$share/grp+/topic") == validation_result::invalid);
|
||||
BOOST_CHECK(validate_shared_topic_filter("$share/#grp/topic") == validation_result::invalid);
|
||||
|
||||
BOOST_CHECK(validate_shared_topic_filter("$share/grp/topic") == validation_result::valid);
|
||||
BOOST_CHECK(validate_shared_topic_filter("$share/grp/topic/#") == validation_result::valid);
|
||||
BOOST_CHECK(validate_shared_topic_filter("$share/grp/+/topic/#") == validation_result::valid);
|
||||
BOOST_CHECK(validate_shared_topic_filter("$share/grp/topic/+") == validation_result::valid);
|
||||
|
||||
BOOST_CHECK(validate_shared_topic_filter("$share/grp/topic/#", false) == validation_result::has_wildcard_character);
|
||||
BOOST_CHECK(validate_shared_topic_filter("$share/grp/+/topic/#", false) == validation_result::has_wildcard_character);
|
||||
BOOST_CHECK(validate_shared_topic_filter("$share/grp/topic/+", false) == validation_result::has_wildcard_character);
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_SUITE_END();
|
||||
|
@ -14,7 +14,8 @@ BOOST_AUTO_TEST_SUITE(subscribe_op/*, *boost::unit_test::disabled()*/)
|
||||
|
||||
BOOST_AUTO_TEST_CASE(test_invalid_topic_filters) {
|
||||
std::vector<std::string> invalid_topics = {
|
||||
"", "+topic", "#topic", "some/#/topic", "topic+"
|
||||
"", "+topic", "#topic", "some/#/topic", "topic+",
|
||||
"$share//topic"
|
||||
};
|
||||
const int expected_handlers_called = invalid_topics.size();
|
||||
int handlers_called = 0;
|
||||
@ -39,4 +40,136 @@ BOOST_AUTO_TEST_CASE(test_invalid_topic_filters) {
|
||||
BOOST_CHECK_EQUAL(handlers_called, expected_handlers_called);
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_CASE(test_wildcard_subscriptions_not_supported) {
|
||||
std::vector<std::string> wildcard_topics = {
|
||||
"topic/#", "$share/grp/topic/#"
|
||||
};
|
||||
connack_props props;
|
||||
props[prop::wildcard_subscription_available] = uint8_t(0);
|
||||
|
||||
int expected_handlers_called = wildcard_topics.size();
|
||||
int handlers_called = 0;
|
||||
|
||||
asio::io_context ioc;
|
||||
using client_service_type = test::test_service<asio::ip::tcp::socket>;
|
||||
auto svc_ptr = std::make_shared<client_service_type>(
|
||||
ioc.get_executor(), std::move(props)
|
||||
);
|
||||
BOOST_ASSERT(svc_ptr->connack_prop(prop::wildcard_subscription_available) == 0);
|
||||
|
||||
for (const auto& topic: wildcard_topics) {
|
||||
auto handler = [&handlers_called](error_code ec, auto, auto) {
|
||||
++handlers_called;
|
||||
BOOST_CHECK(ec == client::error::wildcard_subscription_not_available);
|
||||
};
|
||||
|
||||
detail::subscribe_op<
|
||||
client_service_type, decltype(handler)
|
||||
> { svc_ptr, std::move(handler) }
|
||||
.perform(
|
||||
{{ topic, { qos_e::exactly_once } }}, subscribe_props {}
|
||||
);
|
||||
}
|
||||
|
||||
ioc.run();
|
||||
BOOST_CHECK_EQUAL(handlers_called, expected_handlers_called);
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_CASE(test_shared_subscriptions_not_supported) {
|
||||
connack_props props;
|
||||
props[prop::shared_subscription_available] = uint8_t(0);
|
||||
|
||||
constexpr int expected_handlers_called = 1;
|
||||
int handlers_called = 0;
|
||||
|
||||
asio::io_context ioc;
|
||||
using client_service_type = test::test_service<asio::ip::tcp::socket>;
|
||||
auto svc_ptr = std::make_shared<client_service_type>(
|
||||
ioc.get_executor(), std::move(props)
|
||||
);
|
||||
BOOST_ASSERT(svc_ptr->connack_prop(prop::shared_subscription_available) == 0);
|
||||
|
||||
auto handler = [&handlers_called](error_code ec, auto, auto) {
|
||||
++handlers_called;
|
||||
BOOST_CHECK(ec == client::error::shared_subscription_not_available);
|
||||
};
|
||||
|
||||
detail::subscribe_op<
|
||||
client_service_type, decltype(handler)
|
||||
> { svc_ptr, std::move(handler) }
|
||||
.perform(
|
||||
{{ "$share/group/topic", { qos_e::exactly_once } }}, subscribe_props {}
|
||||
);
|
||||
|
||||
ioc.run();
|
||||
BOOST_CHECK_EQUAL(handlers_called, expected_handlers_called);
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_CASE(test_large_subscription_id) {
|
||||
connack_props props;
|
||||
props[prop::subscription_identifier_available] = uint8_t(1);
|
||||
|
||||
constexpr int expected_handlers_called = 1;
|
||||
int handlers_called = 0;
|
||||
|
||||
asio::io_context ioc;
|
||||
using client_service_type = test::test_service<asio::ip::tcp::socket>;
|
||||
auto svc_ptr = std::make_shared<client_service_type>(
|
||||
ioc.get_executor(), std::move(props)
|
||||
);
|
||||
BOOST_ASSERT(svc_ptr->connack_prop(prop::subscription_identifier_available) == 1);
|
||||
|
||||
auto handler = [&handlers_called](error_code ec, auto, auto) {
|
||||
++handlers_called;
|
||||
BOOST_CHECK(ec == client::error::subscription_identifier_not_available);
|
||||
};
|
||||
|
||||
subscribe_props sub_props_big_id {};
|
||||
sub_props_big_id[prop::subscription_identifier] = std::numeric_limits<uint32_t>::max();
|
||||
|
||||
detail::subscribe_op<
|
||||
client_service_type, decltype(handler)
|
||||
> { svc_ptr, std::move(handler) }
|
||||
.perform(
|
||||
{{ "topic", { qos_e::exactly_once } }}, sub_props_big_id
|
||||
);
|
||||
|
||||
ioc.run();
|
||||
BOOST_CHECK_EQUAL(handlers_called, expected_handlers_called);
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_CASE(test_subscription_ids_not_supported) {
|
||||
connack_props props;
|
||||
props[prop::subscription_identifier_available] = uint8_t(0);
|
||||
BOOST_ASSERT(props[prop::subscription_identifier_available] == 0);
|
||||
|
||||
constexpr int expected_handlers_called = 1;
|
||||
int handlers_called = 0;
|
||||
|
||||
asio::io_context ioc;
|
||||
using client_service_type = test::test_service<asio::ip::tcp::socket>;
|
||||
auto svc_ptr = std::make_shared<client_service_type>(
|
||||
ioc.get_executor(), std::move(props)
|
||||
);
|
||||
BOOST_ASSERT(svc_ptr->connack_prop(prop::subscription_identifier_available) == 0);
|
||||
|
||||
auto handler = [&handlers_called](error_code ec, auto, auto) {
|
||||
++handlers_called;
|
||||
BOOST_CHECK(ec == client::error::subscription_identifier_not_available);
|
||||
};
|
||||
|
||||
subscribe_props sub_props {};
|
||||
sub_props[prop::subscription_identifier] = 23;
|
||||
|
||||
detail::subscribe_op<
|
||||
client_service_type, decltype(handler)
|
||||
> { svc_ptr, std::move(handler) }
|
||||
.perform(
|
||||
{{ "topic", { qos_e::exactly_once } }}, sub_props
|
||||
);
|
||||
|
||||
ioc.run();
|
||||
BOOST_CHECK_EQUAL(handlers_called, expected_handlers_called);
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_SUITE_END()
|
||||
|
Reference in New Issue
Block a user