mirror of
https://github.com/mikecarper/MeshCore.git
synced 2026-09-16 15:52:50 +00:00
# Conflicts: # MQTT_INTERNALS.md # build.sh # docs/cli_commands.md # examples/simple_repeater/MyMesh.cpp # examples/simple_repeater/MyMesh.h # examples/simple_repeater/UITask.cpp # examples/simple_room_server/MyMesh.cpp # examples/simple_room_server/MyMesh.h # examples/simple_room_server/UITask.cpp # examples/simple_sensor/SensorMesh.cpp # platformio.ini # scripts/generate_webconfig_html.py # src/MeshCore.h # src/helpers/CommonCLI.cpp # src/helpers/CommonCLI.h # src/helpers/CommonCLI_Observer.cpp # src/helpers/ESP32Board.h # src/helpers/MQTTMessageBuilder.cpp # src/helpers/MQTTPresets.h # src/helpers/NRF52Board.cpp # src/helpers/NRF52Board.h # src/helpers/bridges/MQTTBridge.cpp # src/helpers/bridges/MQTTBridge.h # src/helpers/esp32/WebConfigServer.cpp # src/helpers/esp32/WebConfigServer.h # src/helpers/radiolib/RadioLibWrappers.cpp # src/helpers/radiolib/RadioLibWrappers.h # variants/lilygo_tbeam_SX1262/platformio.ini # variants/lilygo_tbeam_SX1276/platformio.ini # variants/lilygo_tlora_v2_1/platformio.ini # webui/index.html
199 lines
8.9 KiB
C++
199 lines
8.9 KiB
C++
// Host contract tests for the complete MQTT publication-topic routing policy.
|
|
#define WITH_MQTT_BRIDGE 1
|
|
#define MQTT_PRESETS_IMPLEMENTATION
|
|
#define PROGMEM
|
|
|
|
#include <gtest/gtest.h>
|
|
#include <cstring>
|
|
#include <string>
|
|
|
|
#include "helpers/MQTTPresets.h"
|
|
#include "helpers/MQTTTopicRouter.h"
|
|
|
|
namespace {
|
|
|
|
constexpr const char* IATA = "DEN";
|
|
constexpr const char* DEVICE = "0123456789ABCDEF";
|
|
constexpr const char* TOKEN = "account-token";
|
|
|
|
struct TypeCase {
|
|
int type;
|
|
const char* name;
|
|
};
|
|
|
|
const TypeCase kTypes[] = {
|
|
{MQTT_PUBLICATION_STATUS, "status"},
|
|
{MQTT_PUBLICATION_PACKETS, "packets"},
|
|
{MQTT_PUBLICATION_RAW, "raw"},
|
|
{MQTT_PUBLICATION_NEIGHBORS, "neighbors"},
|
|
};
|
|
|
|
TEST(MQTTTopicRouter, EveryMeshCorePresetSupportsEveryPublicationType) {
|
|
for (int preset_index = 0; preset_index < MQTT_PRESET_COUNT; ++preset_index) {
|
|
const MQTTPresetDef& preset = MQTT_PRESETS[preset_index];
|
|
if (preset.topic_style != MQTT_TOPIC_MESHCORE) continue;
|
|
|
|
for (const TypeCase& type : kTypes) {
|
|
char topic[128];
|
|
ASSERT_TRUE(mqttBuildPublicationTopic(MQTT_ROUTE_MESHCORE, type.type, nullptr,
|
|
IATA, DEVICE, TOKEN, topic, sizeof(topic)))
|
|
<< preset.name << " / " << type.name;
|
|
EXPECT_EQ(std::string("meshcore/DEN/0123456789ABCDEF/") + type.name, topic)
|
|
<< preset.name;
|
|
}
|
|
}
|
|
}
|
|
|
|
TEST(MQTTTopicRouter, MeshRankContractIsPacketsOnly) {
|
|
const MQTTPresetDef* preset = findMQTTPreset("meshrank");
|
|
ASSERT_NE(nullptr, preset);
|
|
ASSERT_EQ(MQTT_TOPIC_MESHRANK, preset->topic_style);
|
|
|
|
char topic[128];
|
|
EXPECT_FALSE(mqttBuildPublicationTopic(MQTT_ROUTE_MESHRANK, MQTT_PUBLICATION_STATUS,
|
|
nullptr, IATA, DEVICE, TOKEN, topic, sizeof(topic)));
|
|
EXPECT_STREQ("", topic);
|
|
ASSERT_TRUE(mqttBuildPublicationTopic(MQTT_ROUTE_MESHRANK, MQTT_PUBLICATION_PACKETS,
|
|
nullptr, IATA, DEVICE, TOKEN, topic, sizeof(topic)));
|
|
EXPECT_STREQ("meshrank/uplink/account-token/0123456789ABCDEF/packets", topic);
|
|
EXPECT_FALSE(mqttBuildPublicationTopic(MQTT_ROUTE_MESHRANK, MQTT_PUBLICATION_RAW,
|
|
nullptr, IATA, DEVICE, TOKEN, topic, sizeof(topic)));
|
|
EXPECT_STREQ("", topic);
|
|
EXPECT_FALSE(mqttBuildPublicationTopic(MQTT_ROUTE_MESHRANK, MQTT_PUBLICATION_NEIGHBORS,
|
|
nullptr, IATA, DEVICE, TOKEN, topic, sizeof(topic)));
|
|
EXPECT_STREQ("", topic);
|
|
}
|
|
|
|
TEST(MQTTTopicRouter, MeshCoreRequiresUsableIataAndDevice) {
|
|
char topic[64];
|
|
const char* invalid_iatas[] = {nullptr, "", "XX", "XXXX", "X/X", "XXX"};
|
|
for (const char* iata : invalid_iatas) {
|
|
EXPECT_FALSE(mqttBuildPublicationTopic(MQTT_ROUTE_MESHCORE, MQTT_PUBLICATION_STATUS,
|
|
nullptr, iata, DEVICE, TOKEN, topic, sizeof(topic)));
|
|
EXPECT_STREQ("", topic);
|
|
}
|
|
EXPECT_FALSE(mqttBuildPublicationTopic(MQTT_ROUTE_MESHCORE, MQTT_PUBLICATION_STATUS,
|
|
nullptr, IATA, nullptr, TOKEN, topic, sizeof(topic)));
|
|
EXPECT_FALSE(mqttBuildPublicationTopic(MQTT_ROUTE_MESHCORE, MQTT_PUBLICATION_STATUS,
|
|
nullptr, IATA, "", TOKEN, topic, sizeof(topic)));
|
|
}
|
|
|
|
TEST(MQTTTopicRouter, MeshRankRequiresTokenAndDeviceButNotIata) {
|
|
char topic[128];
|
|
const char* missing_tokens[] = {nullptr, ""};
|
|
for (const char* token : missing_tokens) {
|
|
EXPECT_FALSE(mqttBuildPublicationTopic(MQTT_ROUTE_MESHRANK, MQTT_PUBLICATION_PACKETS,
|
|
nullptr, nullptr, DEVICE, token, topic, sizeof(topic)));
|
|
EXPECT_STREQ("", topic);
|
|
}
|
|
EXPECT_FALSE(mqttBuildPublicationTopic(MQTT_ROUTE_MESHRANK, MQTT_PUBLICATION_PACKETS,
|
|
nullptr, nullptr, nullptr, TOKEN, topic, sizeof(topic)));
|
|
EXPECT_TRUE(mqttBuildPublicationTopic(MQTT_ROUTE_MESHRANK, MQTT_PUBLICATION_PACKETS,
|
|
nullptr, nullptr, DEVICE, TOKEN, topic, sizeof(topic)));
|
|
}
|
|
|
|
TEST(MQTTTopicRouter, CustomTemplateExpandsEveryType) {
|
|
for (const TypeCase& type : kTypes) {
|
|
char topic[128];
|
|
ASSERT_TRUE(mqttBuildPublicationTopic(
|
|
MQTT_ROUTE_CUSTOM, type.type, "custom/{iata}/{token}/{device}/{type}",
|
|
IATA, DEVICE, TOKEN, topic, sizeof(topic)));
|
|
EXPECT_EQ(std::string("custom/DEN/account-token/0123456789ABCDEF/") + type.name, topic);
|
|
}
|
|
}
|
|
|
|
TEST(MQTTTopicRouter, CustomLiteralDoesNotRequireIataTokenOrDevice) {
|
|
char topic[32];
|
|
ASSERT_TRUE(mqttBuildPublicationTopic(MQTT_ROUTE_CUSTOM, MQTT_PUBLICATION_RAW,
|
|
"private/raw", nullptr, nullptr, nullptr,
|
|
topic, sizeof(topic)));
|
|
EXPECT_STREQ("private/raw", topic);
|
|
}
|
|
|
|
TEST(MQTTTopicRouter, EmptyCustomTemplateFailsRatherThanFallingBackImplicitly) {
|
|
char topic[64];
|
|
EXPECT_FALSE(mqttBuildPublicationTopic(MQTT_ROUTE_CUSTOM, MQTT_PUBLICATION_STATUS,
|
|
"", IATA, DEVICE, TOKEN, topic, sizeof(topic)));
|
|
EXPECT_STREQ("", topic);
|
|
|
|
// MQTTBridge selects the MeshCore style explicitly for a custom slot whose
|
|
// template is empty; make that default contract visible here.
|
|
EXPECT_TRUE(mqttBuildPublicationTopic(MQTT_ROUTE_MESHCORE, MQTT_PUBLICATION_STATUS,
|
|
nullptr, IATA, DEVICE, TOKEN, topic, sizeof(topic)));
|
|
EXPECT_STREQ("meshcore/DEN/0123456789ABCDEF/status", topic);
|
|
}
|
|
|
|
TEST(MQTTTopicRouter, FormattedTopicsRequireRoomForTerminator) {
|
|
const char* expected = "meshcore/DEN/0123456789ABCDEF/status";
|
|
const size_t exact_size = strlen(expected) + 1;
|
|
char exact[64];
|
|
ASSERT_LE(exact_size, sizeof(exact));
|
|
EXPECT_TRUE(mqttBuildPublicationTopic(MQTT_ROUTE_MESHCORE, MQTT_PUBLICATION_STATUS,
|
|
nullptr, IATA, DEVICE, TOKEN, exact, exact_size));
|
|
EXPECT_STREQ(expected, exact);
|
|
|
|
char short_buf[64];
|
|
memset(short_buf, 0x7f, sizeof(short_buf));
|
|
EXPECT_FALSE(mqttBuildPublicationTopic(MQTT_ROUTE_MESHCORE, MQTT_PUBLICATION_STATUS,
|
|
nullptr, IATA, DEVICE, TOKEN,
|
|
short_buf, exact_size - 1));
|
|
EXPECT_EQ('\0', short_buf[exact_size - 2]);
|
|
}
|
|
|
|
TEST(MQTTTopicRouter, CustomTopicHonorsExactBoundary) {
|
|
const char* expected = "custom/DEN/raw";
|
|
char exact[15];
|
|
static_assert(sizeof(exact) == 15, "fixture includes the terminator");
|
|
EXPECT_TRUE(mqttBuildPublicationTopic(MQTT_ROUTE_CUSTOM, MQTT_PUBLICATION_RAW,
|
|
"custom/{iata}/{type}", IATA, DEVICE, TOKEN,
|
|
exact, sizeof(exact)));
|
|
EXPECT_STREQ(expected, exact);
|
|
|
|
char short_buf[14];
|
|
EXPECT_FALSE(mqttBuildPublicationTopic(MQTT_ROUTE_CUSTOM, MQTT_PUBLICATION_RAW,
|
|
"custom/{iata}/{type}", IATA, DEVICE, TOKEN,
|
|
short_buf, sizeof(short_buf)));
|
|
EXPECT_LT(strlen(short_buf), sizeof(short_buf));
|
|
}
|
|
|
|
TEST(MQTTTopicRouter, RejectsInvalidStyleTypeSlotAndOutput) {
|
|
char topic[64] = "dirty";
|
|
EXPECT_FALSE(mqttBuildPublicationTopic(MQTT_ROUTE_MESHCORE, 99, nullptr,
|
|
IATA, DEVICE, TOKEN, topic, sizeof(topic)));
|
|
EXPECT_STREQ("", topic);
|
|
EXPECT_FALSE(mqttBuildPublicationTopic(static_cast<MQTTTopicRouteStyle>(99),
|
|
MQTT_PUBLICATION_STATUS, nullptr,
|
|
IATA, DEVICE, TOKEN, topic, sizeof(topic)));
|
|
EXPECT_FALSE(mqttBuildPublicationTopic(MQTT_ROUTE_MESHCORE, MQTT_PUBLICATION_STATUS,
|
|
nullptr, IATA, DEVICE, TOKEN, nullptr, 64));
|
|
EXPECT_FALSE(mqttBuildPublicationTopic(MQTT_ROUTE_MESHCORE, MQTT_PUBLICATION_STATUS,
|
|
nullptr, IATA, DEVICE, TOKEN, topic, 0));
|
|
|
|
EXPECT_FALSE(mqttTopicSlotIndexValid(-1, RUNTIME_MQTT_SLOTS));
|
|
EXPECT_TRUE(mqttTopicSlotIndexValid(0, RUNTIME_MQTT_SLOTS));
|
|
EXPECT_TRUE(mqttTopicSlotIndexValid(RUNTIME_MQTT_SLOTS - 1, RUNTIME_MQTT_SLOTS));
|
|
EXPECT_FALSE(mqttTopicSlotIndexValid(RUNTIME_MQTT_SLOTS, RUNTIME_MQTT_SLOTS));
|
|
EXPECT_FALSE(mqttTopicSlotIndexValid(0, 0));
|
|
}
|
|
|
|
TEST(MQTTTopicRouter, PublicationTypeEnumValuesAreFrozen) {
|
|
// The bridge passes MQTTBridge::MQTTMessageType to mqttBuildPublicationTopic
|
|
// as an int; a compile-time static_assert in the bridge ties the two enums
|
|
// together. Freeze the router side here so its values can't drift on their own.
|
|
EXPECT_EQ(0, MQTT_PUBLICATION_STATUS);
|
|
EXPECT_EQ(1, MQTT_PUBLICATION_PACKETS);
|
|
EXPECT_EQ(2, MQTT_PUBLICATION_RAW);
|
|
EXPECT_EQ(3, MQTT_PUBLICATION_NEIGHBORS);
|
|
EXPECT_STREQ("status", mqttPublicationTypeName(MQTT_PUBLICATION_STATUS));
|
|
EXPECT_STREQ("packets", mqttPublicationTypeName(MQTT_PUBLICATION_PACKETS));
|
|
EXPECT_STREQ("raw", mqttPublicationTypeName(MQTT_PUBLICATION_RAW));
|
|
EXPECT_STREQ("neighbors", mqttPublicationTypeName(MQTT_PUBLICATION_NEIGHBORS));
|
|
}
|
|
|
|
} // namespace
|
|
|
|
int main(int argc, char** argv) {
|
|
::testing::InitGoogleTest(&argc, argv);
|
|
return RUN_ALL_TESTS();
|
|
}
|