test: add MQTT callback publish regressions and mock callback tracking

PubSubClientMock flags inbound callback context and counts publish attempts
from callbacks. Entity and core tests assert unsafe callback publishes are
rejected by the mock (baseline before HAMqtt deferred publish).
This commit is contained in:
2026-05-05 13:52:00 +10:00
parent c9e6eef5b2
commit 8ba5482892
13 changed files with 139 additions and 0 deletions
+9
View File
@@ -13,6 +13,8 @@ PubSubClientMock::PubSubClientMock() :
_flushedMessagesNb(0), _flushedMessagesNb(0),
_subscriptions(nullptr), _subscriptions(nullptr),
_subscriptionsNb(0), _subscriptionsNb(0),
_insideCallback(false),
_publishCallsFromCallbackNb(0),
callback(nullptr) callback(nullptr)
{ {
@@ -119,6 +121,11 @@ bool PubSubClientMock::beginPublish(
return false; return false;
} }
if (_insideCallback) {
_publishCallsFromCallbackNb++;
return false;
}
if (_pendingMessage) { if (_pendingMessage) {
delete _pendingMessage; delete _pendingMessage;
} }
@@ -239,7 +246,9 @@ void PubSubClientMock::fakeMessage(const char* topic, const char* message)
uint8_t data[len]; uint8_t data[len];
memcpy(data, message, len); memcpy(data, message, len);
_insideCallback = true;
callback(const_cast<char*>(topic), data, len); callback(const_cast<char*>(topic), data, len);
_insideCallback = false;
} }
void PubSubClientMock::fakeMessage( void PubSubClientMock::fakeMessage(
+11
View File
@@ -166,6 +166,15 @@ public:
inline const MqttWill& getLastWill() const inline const MqttWill& getLastWill() const
{ return _lastWill; } { return _lastWill; }
inline bool isInsideCallback() const
{ return _insideCallback; }
inline uint16_t getPublishCallsFromCallbackNb() const
{ return _publishCallsFromCallbackNb; }
inline void resetPublishCallsFromCallbackNb()
{ _publishCallsFromCallbackNb = 0; }
void clearFlushedMessages(); void clearFlushedMessages();
void clearSubscriptions(); void clearSubscriptions();
void fakeMessage(const char* topic, const char* message); void fakeMessage(const char* topic, const char* message);
@@ -184,6 +193,8 @@ private:
uint8_t _subscriptionsNb; uint8_t _subscriptionsNb;
MqttConnection _connection; MqttConnection _connection;
MqttWill _lastWill; MqttWill _lastWill;
bool _insideCallback;
uint16_t _publishCallsFromCallbackNb;
MQTT_CALLBACK_SIGNATURE; MQTT_CALLBACK_SIGNATURE;
}; };
+2
View File
@@ -85,6 +85,7 @@ static TestCase tests[] = {
TEST_ENTRY(test_SwitchTest_command_off), TEST_ENTRY(test_SwitchTest_command_off),
TEST_ENTRY(test_SwitchTest_command_on), TEST_ENTRY(test_SwitchTest_command_on),
TEST_ENTRY(test_SwitchTest_command_subscription), TEST_ENTRY(test_SwitchTest_command_subscription),
TEST_ENTRY(test_SwitchTest_callback_publish_attempt_is_rejected),
TEST_ENTRY(test_SwitchTest_current_state_setter), TEST_ENTRY(test_SwitchTest_current_state_setter),
TEST_ENTRY(test_SwitchTest_default_entity_id_setter), TEST_ENTRY(test_SwitchTest_default_entity_id_setter),
TEST_ENTRY(test_SwitchTest_default_params), TEST_ENTRY(test_SwitchTest_default_params),
@@ -120,6 +121,7 @@ static TestCase tests[] = {
TEST_ENTRY(test_TextTest_publish_nothing_if_retained), TEST_ENTRY(test_TextTest_publish_nothing_if_retained),
TEST_ENTRY(test_TextTest_publish_state), TEST_ENTRY(test_TextTest_publish_state),
TEST_ENTRY(test_TextTest_publish_state_debounce), TEST_ENTRY(test_TextTest_publish_state_debounce),
TEST_ENTRY(test_TextTest_callback_publish_attempt_is_rejected),
TEST_ENTRY(test_TextTest_retain_setter), TEST_ENTRY(test_TextTest_retain_setter),
}; };
+2
View File
@@ -113,6 +113,7 @@ extern void test_SwitchTest_publish_nothing_if_retained(void);
extern void test_SwitchTest_publish_state_off(void); extern void test_SwitchTest_publish_state_off(void);
extern void test_SwitchTest_publish_state_on(void); extern void test_SwitchTest_publish_state_on(void);
extern void test_SwitchTest_retain_setter(void); extern void test_SwitchTest_retain_setter(void);
extern void test_SwitchTest_callback_publish_attempt_is_rejected(void);
extern void test_TextTest_availability(void); extern void test_TextTest_availability(void);
extern void test_TextTest_command_callback(void); extern void test_TextTest_command_callback(void);
extern void test_TextTest_command_subscription(void); extern void test_TextTest_command_subscription(void);
@@ -131,5 +132,6 @@ extern void test_TextTest_publish_nothing_if_retained(void);
extern void test_TextTest_publish_state(void); extern void test_TextTest_publish_state(void);
extern void test_TextTest_publish_state_debounce(void); extern void test_TextTest_publish_state_debounce(void);
extern void test_TextTest_retain_setter(void); extern void test_TextTest_retain_setter(void);
extern void test_TextTest_callback_publish_attempt_is_rejected(void);
#endif #endif
@@ -42,6 +42,11 @@ void onCommandReceived(bool state, HASwitch* caller)
lastCommandCallbackCall.caller = caller; lastCommandCallbackCall.caller = caller;
} }
void onCommandPublishAttempt(bool state, HASwitch* caller)
{
TEST_ASSERT_FALSE(caller->setState(state));
}
void test_SwitchTest_invalid_unique_id(void) { void test_SwitchTest_invalid_unique_id(void) {
prepareTest prepareTest
@@ -388,6 +393,19 @@ void test_SwitchTest_command_off(void) {
assertCommandCallbackCalled(false, &testSwitch) assertCommandCallbackCalled(false, &testSwitch)
} }
void test_SwitchTest_callback_publish_attempt_is_rejected(void) {
prepareTest
mock->connectDummy();
HASwitch testSwitch(testUniqueId);
testSwitch.onCommand(onCommandPublishAttempt);
mock->fakeMessage(AHATOFSTR(CommandTopic), F("ON"));
TEST_ASSERT_EQUAL(1, mock->getPublishCallsFromCallbackNb());
TEST_ASSERT_EQUAL(0, mock->getFlushedMessagesNb());
}
void test_SwitchTest_different_switch_command(void) { void test_SwitchTest_different_switch_command(void) {
prepareTest prepareTest
@@ -46,6 +46,11 @@ void onCommandReceived(const char* value, HAText* caller)
lastCommandCallbackCall.caller = caller; lastCommandCallbackCall.caller = caller;
} }
void onCommandPublishAttempt(const char* value, HAText* caller)
{
TEST_ASSERT_FALSE(caller->setState(value));
}
void test_TextTest_invalid_unique_id(void) { void test_TextTest_invalid_unique_id(void) {
prepareTest prepareTest
@@ -323,6 +328,19 @@ void test_TextTest_command_callback(void) {
assertCommandCallbackCalled("hello", &text) assertCommandCallbackCalled("hello", &text)
} }
void test_TextTest_callback_publish_attempt_is_rejected(void) {
prepareTest
mock->connectDummy();
HAText text(testUniqueId);
text.onCommand(onCommandPublishAttempt);
mock->fakeMessage(AHATOFSTR(CommandTopic), F("hello"));
TEST_ASSERT_EQUAL(1, mock->getPublishCallsFromCallbackNb());
TEST_ASSERT_EQUAL(0, mock->getFlushedMessagesNb());
}
void test_TextTest_different_text_command(void) { void test_TextTest_different_text_command(void) {
prepareTest prepareTest
+2
View File
@@ -13,6 +13,7 @@ static TestCase tests[] = {
TEST_ENTRY(test_NumberTest_command_number_unsigned), TEST_ENTRY(test_NumberTest_command_number_unsigned),
TEST_ENTRY(test_NumberTest_command_number_zero), TEST_ENTRY(test_NumberTest_command_number_zero),
TEST_ENTRY(test_NumberTest_command_subscription), TEST_ENTRY(test_NumberTest_command_subscription),
TEST_ENTRY(test_NumberTest_callback_publish_attempt_is_rejected),
TEST_ENTRY(test_NumberTest_current_state_setter_getter_float), TEST_ENTRY(test_NumberTest_current_state_setter_getter_float),
TEST_ENTRY(test_NumberTest_current_state_setter_getter_int16), TEST_ENTRY(test_NumberTest_current_state_setter_getter_int16),
TEST_ENTRY(test_NumberTest_current_state_setter_getter_int32), TEST_ENTRY(test_NumberTest_current_state_setter_getter_int32),
@@ -226,6 +227,7 @@ static TestCase tests[] = {
TEST_ENTRY(test_SelectTest_publish_state_only), TEST_ENTRY(test_SelectTest_publish_state_only),
TEST_ENTRY(test_SelectTest_retain_setter), TEST_ENTRY(test_SelectTest_retain_setter),
TEST_ENTRY(test_SelectTest_single_option), TEST_ENTRY(test_SelectTest_single_option),
TEST_ENTRY(test_SelectTest_callback_publish_attempt_is_rejected),
}; };
static const size_t TEST_COUNT = sizeof(tests) / sizeof(tests[0]); static const size_t TEST_COUNT = sizeof(tests) / sizeof(tests[0]);
+2
View File
@@ -23,6 +23,7 @@ extern void test_NumberTest_command_number_signed(void);
extern void test_NumberTest_command_number_unsigned(void); extern void test_NumberTest_command_number_unsigned(void);
extern void test_NumberTest_command_number_zero(void); extern void test_NumberTest_command_number_zero(void);
extern void test_NumberTest_command_subscription(void); extern void test_NumberTest_command_subscription(void);
extern void test_NumberTest_callback_publish_attempt_is_rejected(void);
extern void test_NumberTest_current_state_setter_getter_float(void); extern void test_NumberTest_current_state_setter_getter_float(void);
extern void test_NumberTest_current_state_setter_getter_int16(void); extern void test_NumberTest_current_state_setter_getter_int16(void);
extern void test_NumberTest_current_state_setter_getter_int32(void); extern void test_NumberTest_current_state_setter_getter_int32(void);
@@ -236,5 +237,6 @@ extern void test_SelectTest_publish_state_none(void);
extern void test_SelectTest_publish_state_only(void); extern void test_SelectTest_publish_state_only(void);
extern void test_SelectTest_retain_setter(void); extern void test_SelectTest_retain_setter(void);
extern void test_SelectTest_single_option(void); extern void test_SelectTest_single_option(void);
extern void test_SelectTest_callback_publish_attempt_is_rejected(void);
#endif #endif
@@ -42,6 +42,11 @@ void onCommandReceived(HANumeric number, HANumber* caller)
lastCommandCallbackCall.caller = caller; lastCommandCallbackCall.caller = caller;
} }
void onCommandPublishAttempt(HANumeric value, HANumber* caller)
{
TEST_ASSERT_FALSE(caller->setState(value));
}
void test_NumberTest_invalid_unique_id(void) { void test_NumberTest_invalid_unique_id(void) {
prepareTest prepareTest
@@ -1010,6 +1015,19 @@ void test_NumberTest_command_number_float_p3(void) {
assertCommandCallbackCalled(HANumeric(-1.234f, 3), &number) assertCommandCallbackCalled(HANumeric(-1.234f, 3), &number)
} }
void test_NumberTest_callback_publish_attempt_is_rejected(void) {
prepareTest
mock->connectDummy();
HANumber number(testUniqueId);
number.onCommand(onCommandPublishAttempt);
mock->fakeMessage(AHATOFSTR(CommandTopic), F("1234"));
TEST_ASSERT_EQUAL(1, mock->getPublishCallsFromCallbackNb());
TEST_ASSERT_EQUAL(0, mock->getFlushedMessagesNb());
}
void test_NumberTest_command_number_invalid(void) { void test_NumberTest_command_number_invalid(void) {
prepareTest prepareTest
@@ -41,6 +41,11 @@ void onCommandReceived(int8_t index, HASelect* caller)
lastCommandCallbackCall.caller = caller; lastCommandCallbackCall.caller = caller;
} }
void onCommandPublishAttempt(int8_t index, HASelect* caller)
{
TEST_ASSERT_FALSE(caller->setState(index));
}
void test_SelectTest_invalid_unique_id(void) { void test_SelectTest_invalid_unique_id(void) {
prepareTest prepareTest
@@ -477,6 +482,20 @@ void test_SelectTest_command_option_non_existing(void) {
assertCommandCallbackNotCalled() assertCommandCallbackNotCalled()
} }
void test_SelectTest_callback_publish_attempt_is_rejected(void) {
prepareTest
mock->connectDummy();
HASelect select(testUniqueId);
select.setOptions("Option A;B;C");
select.onCommand(onCommandPublishAttempt);
mock->fakeMessage(AHATOFSTR(CommandTopic), F("B"));
TEST_ASSERT_EQUAL(1, mock->getPublishCallsFromCallbackNb());
TEST_ASSERT_EQUAL(0, mock->getFlushedMessagesNb());
}
void test_SelectTest_different_select_command(void) { void test_SelectTest_different_select_command(void) {
prepareTest prepareTest
+1
View File
@@ -7,6 +7,7 @@ static TestCase tests[] = {
TEST_ENTRY(test_MqttTest_reconnect_interval_default_value), TEST_ENTRY(test_MqttTest_reconnect_interval_default_value),
TEST_ENTRY(test_MqttTest_reconnect_interval_setter), TEST_ENTRY(test_MqttTest_reconnect_interval_setter),
TEST_ENTRY(test_MqttTest_reconnect_interval_throttles_attempts), TEST_ENTRY(test_MqttTest_reconnect_interval_throttles_attempts),
TEST_ENTRY(test_MqttTest_publish_attempt_from_message_callback_is_rejected),
TEST_ENTRY(test_DeviceTest_default_unique_id), TEST_ENTRY(test_DeviceTest_default_unique_id),
TEST_ENTRY(test_DeviceTest_unique_id_constructor_char), TEST_ENTRY(test_DeviceTest_unique_id_constructor_char),
+1
View File
@@ -17,6 +17,7 @@ extern void test_MqttTest_maximum_number_of_device_types(void);
extern void test_MqttTest_reconnect_interval_default_value(void); extern void test_MqttTest_reconnect_interval_default_value(void);
extern void test_MqttTest_reconnect_interval_setter(void); extern void test_MqttTest_reconnect_interval_setter(void);
extern void test_MqttTest_reconnect_interval_throttles_attempts(void); extern void test_MqttTest_reconnect_interval_throttles_attempts(void);
extern void test_MqttTest_publish_attempt_from_message_callback_is_rejected(void);
extern void test_DeviceTest_default_unique_id(void); extern void test_DeviceTest_default_unique_id(void);
extern void test_DeviceTest_unique_id_constructor_char(void); extern void test_DeviceTest_unique_id_constructor_char(void);
+36
View File
@@ -4,6 +4,8 @@
static const char* testDeviceId = "testDevice"; static const char* testDeviceId = "testDevice";
static const char* testUniqueId = "uniqueId"; static const char* testUniqueId = "uniqueId";
static HAMqtt* activeMqtt = nullptr;
static bool messageCallbackCalled = false;
const char ComponentNameStr[] PROGMEM = {"componentName"}; const char ComponentNameStr[] PROGMEM = {"componentName"};
@@ -20,6 +22,17 @@ protected:
} }
}; };
void onMessagePublishAttempt(const char* topic, const uint8_t* payload, uint16_t length)
{
(void)topic;
(void)payload;
(void)length;
messageCallbackCalled = true;
TEST_ASSERT_NOT_NULL(activeMqtt);
TEST_ASSERT_FALSE(activeMqtt->publish("testData/testDevice/echo", "1", false));
}
void test_MqttTest_maximum_number_of_device_types(void) void test_MqttTest_maximum_number_of_device_types(void)
{ {
HADevice device(testDeviceId); HADevice device(testDeviceId);
@@ -70,3 +83,26 @@ void test_MqttTest_reconnect_interval_throttles_attempts(void)
mqtt.loop(); mqtt.loop();
TEST_ASSERT_EQUAL_UINT16(2, mock->getConnectCallsNb()); TEST_ASSERT_EQUAL_UINT16(2, mock->getConnectCallsNb());
} }
void test_MqttTest_publish_attempt_from_message_callback_is_rejected(void)
{
PubSubClientMock* mock = new PubSubClientMock();
HADevice device(testDeviceId);
HAMqtt mqtt(mock, device);
mqtt.setDataPrefix("testData");
mqtt.begin("testHost", "testUser", "testPass");
mock->connectDummy();
activeMqtt = &mqtt;
messageCallbackCalled = false;
mock->resetPublishCallsFromCallbackNb();
mqtt.onMessage(onMessagePublishAttempt);
mock->fakeMessage("testData/testDevice/input", "1");
TEST_ASSERT_TRUE(messageCallbackCalled);
TEST_ASSERT_EQUAL_UINT16(1, mock->getPublishCallsFromCallbackNb());
TEST_ASSERT_EQUAL_UINT8(0, mock->getFlushedMessagesNb());
activeMqtt = nullptr;
}