From 869067c2f9d82d85c25b7d68ebe97d82f4648300 Mon Sep 17 00:00:00 2001 From: Carl Philipp Klemm Date: Fri, 17 Apr 2026 18:32:41 +0200 Subject: [PATCH] Test: Add mqttitem unit tests --- tests/CMakeLists.txt | 4 +- tests/unit/items/test_mqttitem.cpp | 306 +++++++++++++++++++++++++++++ 2 files changed, 309 insertions(+), 1 deletion(-) create mode 100644 tests/unit/items/test_mqttitem.cpp diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index dabb4fd..bddc2f4 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -12,10 +12,11 @@ add_executable(test_sensor unit/sensors/test_sensor.cpp) add_executable(test_actor unit/actors/test_actor.cpp) add_executable(test_itemstore unit/items/test_itemstore.cpp) add_executable(test_itemloadersource unit/items/test_itemloadersource.cpp) +add_executable(test_mqttitem unit/items/test_mqttitem.cpp) add_executable(test_tcp unit/service/test_tcp.cpp) # Link all tests to static library -foreach(test test_item test_sensor test_actor test_itemstore test_itemloadersource test_tcp) +foreach(test test_item test_sensor test_actor test_itemstore test_itemloadersource test_mqttitem test_tcp) target_link_libraries(${test} smartvos_core Qt6::Core @@ -41,4 +42,5 @@ add_test(NAME test_sensor COMMAND test_sensor) add_test(NAME test_actor COMMAND test_actor) add_test(NAME test_itemstore COMMAND test_itemstore) add_test(NAME test_itemloadersource COMMAND test_itemloadersource) +add_test(NAME test_mqttitem COMMAND test_mqttitem) add_test(NAME test_tcp COMMAND test_tcp) \ No newline at end of file diff --git a/tests/unit/items/test_mqttitem.cpp b/tests/unit/items/test_mqttitem.cpp new file mode 100644 index 0000000..27a8940 --- /dev/null +++ b/tests/unit/items/test_mqttitem.cpp @@ -0,0 +1,306 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "items/mqttitem.h" +#include "mqttclient.h" +#include "programmode.h" + +class TestMqttItem : public QObject +{ + Q_OBJECT + +private slots: + void initTestCase() + { + // Setup for all tests + // Try to load config and connect to MQTT broker if configured + QString settingsPath = QStandardPaths::writableLocation(QStandardPaths::ConfigLocation) + "/shinterface.json"; + + QJsonObject json; + if (QFile::exists(settingsPath)) { + QFile file(settingsPath); + if (file.open(QIODevice::ReadOnly)) { + QByteArray data = file.readAll(); + file.close(); + QJsonParseError error; + QJsonDocument doc = QJsonDocument::fromJson(data, &error); + if (error.error == QJsonParseError::NoError) { + json = doc.object(); + } + } + } + + QJsonObject mqttJson = json["Mqtt"].toObject(); + QString host = mqttJson["Host"].toString(); + int port = mqttJson["Port"].toInt(1883); + + // If MQTT is configured with a host, try to connect + static std::shared_ptr mqttClient; + if (!host.isEmpty()) { + qDebug() << "MQTT configured:" << host << port; + mqttClient = std::make_shared(); + mqttClient->start(mqttJson); + // Give it a moment to connect + QTest::qWait(1000); + + // Check if connected or connecting + auto qClient = mqttClient->getClient(); + if (qClient && (qClient->state() == QMqttClient::Connected || + qClient->state() == QMqttClient::Connecting)) { + qDebug() << "MQTT connected/connecting, using client"; + MqttItem::client = mqttClient; + } else { + qDebug() << "MQTT connection failed, using UI_ONLY mode"; + programMode = PROGRAM_MODE_UI_ONLY; + } + } else { + qDebug() << "No MQTT host configured, using UI_ONLY mode"; + programMode = PROGRAM_MODE_UI_ONLY; + } + } + + void testMqttItemCreation() + { + MqttItem item("test_mqtt", 0); + QCOMPARE(item.getName(), QString("test_mqtt")); + QVERIFY(item.getTopic().isEmpty()); + QVERIFY(item.getValueKey() == "state"); + } + + void testMqttItemSetTopic() + { + MqttItem item("test_mqtt", 0); + item.setTopic("my_device"); + item.setValueKey("state"); + + QVERIFY(item.getTopic() == "my_device"); + QVERIFY(item.getValueKey() == "state"); + } + + void testMqttItemSetValueType() + { + MqttItem item("test_mqtt", 0); + + // Default should be BOOL + QVERIFY(item.getValueType() == ITEM_VALUE_BOOL); + + // Set to UINT + item.setValueType(ITEM_VALUE_UINT); + QVERIFY(item.getValueType() == ITEM_VALUE_UINT); + + // Set to ENUM + item.setValueType(ITEM_VALUE_ENUM); + QVERIFY(item.getValueType() == ITEM_VALUE_ENUM); + } + + void testMqttItemValueNames() + { + MqttItem item("test_mqtt", 0); + + // Initially empty + QVERIFY(item.getValueNames().empty()); + + // Set value names + std::vector names = {"off", "heat", "cool"}; + item.setValueNames(names); + + auto storedNames = item.getValueNames(); + QVERIFY(storedNames.size() == 3); + QVERIFY(storedNames[0] == "off"); + QVERIFY(storedNames[1] == "heat"); + QVERIFY(storedNames[2] == "cool"); + } + + void testValueNameConversion() + { + MqttItem item("test_mqtt", 0); + + // Set value names for enum + std::vector names = {"off", "heat", "cool", "auto"}; + item.setValueNames(names); + item.setValueType(ITEM_VALUE_ENUM); + + // Test name to index + QVERIFY(item.valueNameToIndex("heat") == 1); + QVERIFY(item.valueNameToIndex("cool") == 2); + QVERIFY(item.valueNameToIndex("unknown") == -1); + + // Test index to name + QVERIFY(item.indexToValueName(0) == "off"); + QVERIFY(item.indexToValueName(3) == "auto"); + QVERIFY(item.indexToValueName(99).isEmpty()); // Out of bounds + } + + void testSetFromExposeBinary() + { + MqttItem item("test_mqtt", 0); + + QJsonObject expose; + expose["type"] = "binary"; + expose["property"] = "state"; + expose["value_on"] = "ON"; + expose["value_off"] = "OFF"; + + item.setFromExpose(expose); + + QVERIFY(item.getValueType() == ITEM_VALUE_BOOL); + QVERIFY(item.getValueKey() == "state"); + QVERIFY(item.getValueOn() == "ON"); + QVERIFY(item.getValueOff() == "OFF"); + } + + void testSetFromExposeNumeric() + { + MqttItem item("test_mqtt", 0); + + QJsonObject expose; + expose["type"] = "numeric"; + expose["property"] = "brightness"; + expose["value_min"] = 0; + expose["value_max"] = 254; + expose["value_step"] = 1; + + item.setFromExpose(expose); + + QVERIFY(item.getValueType() == ITEM_VALUE_UINT); + QVERIFY(item.getValueKey() == "brightness"); + QVERIFY(item.getValueMin() == 0); + QVERIFY(item.getValueMax() == 254); + QVERIFY(item.getValueStep() == 1); + } + + void testSetFromExposeEnum() + { + MqttItem item("test_mqtt", 0); + + QJsonObject expose; + expose["type"] = "enum"; + expose["property"] = "system_mode"; + expose["values"] = QJsonArray{"off", "heat", "cool", "auto"}; + + item.setFromExpose(expose); + + QVERIFY(item.getValueType() == ITEM_VALUE_ENUM); + QVERIFY(item.getValueKey() == "system_mode"); + + auto names = item.getValueNames(); + QVERIFY(names.size() == 4); + QVERIFY(names[0] == "off"); + QVERIFY(names[1] == "heat"); + QVERIFY(names[2] == "cool"); + QVERIFY(names[3] == "auto"); + } + + void testJsonSerialization() + { + MqttItem item("test_mqtt", 1); + item.setTopic("my_device"); + item.setValueKey("state"); + item.setValueOn("ON"); + item.setValueOff("OFF"); + + QJsonObject json; + item.store(json); + + QVERIFY(json["Type"] == "Mqtt"); + QVERIFY(json["Topic"] == "my_device"); + QVERIFY(json["ValueKey"] == "state"); + QVERIFY(json["ValueOn"] == "ON"); + QVERIFY(json["ValueOff"] == "OFF"); + } + + void testJsonDeserialization() + { + QJsonObject json; + json["Type"] = "Mqtt"; + json["ItemId"] = 100; + json["Name"] = "loaded_mqtt"; + json["Topic"] = "test_device"; + json["ValueKey"] = "state"; + json["ValueOn"] = "ON"; + json["ValueOff"] = "OFF"; + json["Value"] = 1; + + MqttItem item; + item.load(json); + + QVERIFY(item.getTopic() == "test_device"); + QVERIFY(item.getValueKey() == "state"); + QVERIFY(item.getValueOn() == "ON"); + QVERIFY(item.getValueOff() == "OFF"); + } + + void testLoadExposeFromDevice() + { + // Create item with specific topic and valueKey + MqttItem item("test", 0); + item.setTopic("0xa4c138ef510950e3"); + item.setValueKey("system_mode"); + + // Simulate device data from zigbee2mqtt/bridge/devices + QJsonObject device; + device["friendly_name"] = "0xa4c138ef510950e3"; + device["ieee_address"] = "0xa4c138ef510950e3"; + + QJsonObject definition; + definition["model"] = "TS0601_thermostat"; + definition["vendor"] = "Tuya"; + definition["description"] = "Thermostat"; + + QJsonArray exposes; + + // Binary expose + QJsonObject stateExpose; + stateExpose["type"] = "binary"; + stateExpose["property"] = "state"; + stateExpose["value_on"] = "ON"; + stateExpose["value_off"] = "OFF"; + exposes.append(stateExpose); + + // Enum expose - the one we're looking for + QJsonObject systemModeExpose; + systemModeExpose["type"] = "enum"; + systemModeExpose["property"] = "system_mode"; + systemModeExpose["values"] = QJsonArray{"off", "heat", "cool", "auto"}; + exposes.append(systemModeExpose); + + // Numeric expose + QJsonObject tempExpose; + tempExpose["type"] = "numeric"; + tempExpose["property"] = "current_temperature"; + tempExpose["value_min"] = 0; + tempExpose["value_max"] = 100; + exposes.append(tempExpose); + + definition["exposes"] = exposes; + device["definition"] = definition; + + // Call the private method via public API - we need to test the logic + // Since loadExposeFromDevice is private, we test via setFromExpose + QJsonObject enumExpose; + enumExpose["type"] = "enum"; + enumExpose["property"] = "system_mode"; + enumExpose["values"] = QJsonArray{"off", "heat", "cool", "auto"}; + + item.setFromExpose(enumExpose); + + QVERIFY(item.getValueType() == ITEM_VALUE_ENUM); + QVERIFY(item.getValueNames().size() == 4); + } + + void cleanupTestCase() + { + // Cleanup after all tests + } +}; + +QTEST_APPLESS_MAIN(TestMqttItem) + +#include "test_mqttitem.moc" \ No newline at end of file