mirror of
https://github.com/mikecarper/MeshCore.git
synced 2026-09-26 18:47:56 +00:00
Replace direct JSON construction in MQTTMessageBuilder with calls to MQTTPayloadBuilder for building status, packet, and raw messages. This change improves code maintainability and reduces duplication by centralizing message formatting logic. Additionally, update platformio.ini to include ArduinoJson dependency for JSON handling.
237 lines
8.9 KiB
C++
237 lines
8.9 KiB
C++
#include <gtest/gtest.h>
|
|
|
|
#include <ArduinoJson.h>
|
|
#include <cmath>
|
|
#include <cstring>
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
#include "helpers/MQTTPayloadBuilder.h"
|
|
|
|
namespace {
|
|
|
|
constexpr const char* kTimestamp = "2026-07-18T12:34:56.123456+00:00";
|
|
|
|
static int buildMinimalStatus(JsonDocument& scratch, char* buffer, size_t buffer_size) {
|
|
return MQTTPayloadBuilder::buildStatusMessage(
|
|
scratch, "DEN Repeater", "0123456789ABCDEF", "Heltec V3", "v1.16.0",
|
|
"915.000000,62.5,7,5", "MeshCore", "online", kTimestamp,
|
|
buffer, buffer_size);
|
|
}
|
|
|
|
static int buildRepresentativePacket(JsonDocument& scratch, const char* direction,
|
|
float score, const uint8_t* path, int path_hops,
|
|
int path_hash_size, const char* raw,
|
|
char* buffer, size_t buffer_size) {
|
|
return MQTTPayloadBuilder::buildPacketMessage(
|
|
scratch, "DEN Repeater", "0123456789ABCDEF", kTimestamp, direction,
|
|
"12:34:56", "18/07/2026", 42, 4, "D", 20, raw,
|
|
10.26f, -87, score, "89ABCDEF01234567", path, path_hops,
|
|
path_hash_size, 64, buffer, buffer_size);
|
|
}
|
|
|
|
TEST(MQTTPayloadBuilder, MinimalStatusHasExactRequiredContract) {
|
|
JsonDocument scratch;
|
|
char buffer[768];
|
|
int len = buildMinimalStatus(scratch, buffer, sizeof(buffer));
|
|
|
|
ASSERT_GT(len, 0);
|
|
EXPECT_EQ(static_cast<size_t>(len), strlen(buffer));
|
|
EXPECT_STREQ(
|
|
"{\"status\":\"online\",\"timestamp\":\"2026-07-18T12:34:56.123456+00:00\","
|
|
"\"origin\":\"DEN Repeater\",\"origin_id\":\"0123456789ABCDEF\","
|
|
"\"model\":\"Heltec V3\",\"firmware_version\":\"v1.16.0\","
|
|
"\"radio\":\"915.000000,62.5,7,5\",\"client_version\":\"MeshCore\"}",
|
|
buffer);
|
|
|
|
JsonDocument parsed;
|
|
ASSERT_FALSE(deserializeJson(parsed, buffer));
|
|
EXPECT_FALSE(parsed["repeat"].is<JsonVariant>());
|
|
EXPECT_FALSE(parsed["stats"].is<JsonVariant>());
|
|
}
|
|
|
|
TEST(MQTTPayloadBuilder, StatusIncludesRepeatAndEveryRequestedStatistic) {
|
|
JsonDocument scratch;
|
|
char buffer[1024];
|
|
int len = MQTTPayloadBuilder::buildStatusMessage(
|
|
scratch, "node", "id", "model", "firmware", "radio", "client", "online",
|
|
kTimestamp, buffer, sizeof(buffer), 4200, 86400, 3, 6, -112,
|
|
11, 22, 4, 180864, 31, 47, "on");
|
|
|
|
ASSERT_GT(len, 0);
|
|
JsonDocument parsed;
|
|
ASSERT_FALSE(deserializeJson(parsed, buffer));
|
|
EXPECT_STREQ("on", parsed["repeat"].as<const char*>());
|
|
JsonObject stats = parsed["stats"].as<JsonObject>();
|
|
ASSERT_FALSE(stats.isNull());
|
|
EXPECT_EQ(4200, stats["battery_mv"].as<int>());
|
|
EXPECT_EQ(86400, stats["uptime_secs"].as<int>());
|
|
EXPECT_EQ(3, stats["errors"].as<int>());
|
|
EXPECT_EQ(6, stats["queue_len"].as<int>());
|
|
EXPECT_EQ(-112, stats["noise_floor"].as<int>());
|
|
EXPECT_EQ(11, stats["tx_air_secs"].as<int>());
|
|
EXPECT_EQ(22, stats["rx_air_secs"].as<int>());
|
|
EXPECT_EQ(4, stats["recv_errors"].as<int>());
|
|
EXPECT_EQ(180864, stats["internal_heap"].as<int>());
|
|
EXPECT_EQ(31, stats["packets_sent"].as<int>());
|
|
EXPECT_EQ(47, stats["packets_received"].as<int>());
|
|
}
|
|
|
|
TEST(MQTTPayloadBuilder, StatusOmissionSentinelsRemainOmitted) {
|
|
JsonDocument scratch;
|
|
char buffer[768];
|
|
int len = MQTTPayloadBuilder::buildStatusMessage(
|
|
scratch, "node", "id", "model", "firmware", "radio", "client", "online",
|
|
kTimestamp, buffer, sizeof(buffer), -1, -1, -1, -1, -999,
|
|
-1, -1, -1, -1, -1, -1, nullptr);
|
|
|
|
ASSERT_GT(len, 0);
|
|
JsonDocument parsed;
|
|
ASSERT_FALSE(deserializeJson(parsed, buffer));
|
|
EXPECT_FALSE(parsed["stats"].is<JsonVariant>());
|
|
EXPECT_FALSE(parsed["repeat"].is<JsonVariant>());
|
|
}
|
|
|
|
TEST(MQTTPayloadBuilder, StringsAreEscapedAndRoundTrip) {
|
|
const char* origin = "node \"north\"\\rack\nline";
|
|
const char* model = "Heltec\tV3";
|
|
JsonDocument scratch;
|
|
char buffer[1024];
|
|
int len = MQTTPayloadBuilder::buildStatusMessage(
|
|
scratch, origin, "id", model, "v1", "radio", "client", "online",
|
|
kTimestamp, buffer, sizeof(buffer));
|
|
|
|
ASSERT_GT(len, 0);
|
|
EXPECT_NE(std::string::npos, std::string(buffer).find("\\\"north\\\""));
|
|
EXPECT_NE(std::string::npos, std::string(buffer).find("\\\\rack\\nline"));
|
|
JsonDocument parsed;
|
|
ASSERT_FALSE(deserializeJson(parsed, buffer));
|
|
EXPECT_STREQ(origin, parsed["origin"].as<const char*>());
|
|
EXPECT_STREQ(model, parsed["model"].as<const char*>());
|
|
}
|
|
|
|
TEST(MQTTPayloadBuilder, RxPacketIncludesMetricsScaledScoreAndPath) {
|
|
const uint8_t path[] = {0xAA, 0xBB, 0x01, 0x2F};
|
|
JsonDocument scratch;
|
|
char buffer[2048];
|
|
int len = buildRepresentativePacket(
|
|
scratch, "rx", 0.125f, path, 2, 2, "A0B1C2D3", buffer, sizeof(buffer));
|
|
|
|
ASSERT_GT(len, 0);
|
|
JsonDocument parsed;
|
|
ASSERT_FALSE(deserializeJson(parsed, buffer));
|
|
EXPECT_STREQ("PACKET", parsed["type"].as<const char*>());
|
|
EXPECT_STREQ("rx", parsed["direction"].as<const char*>());
|
|
EXPECT_STREQ("42", parsed["len"].as<const char*>());
|
|
EXPECT_STREQ("4", parsed["packet_type"].as<const char*>());
|
|
EXPECT_STREQ("20", parsed["payload_len"].as<const char*>());
|
|
EXPECT_STREQ("10.3", parsed["SNR"].as<const char*>());
|
|
EXPECT_STREQ("-87", parsed["RSSI"].as<const char*>());
|
|
EXPECT_STREQ("125", parsed["score"].as<const char*>());
|
|
JsonArray parsed_path = parsed["path"].as<JsonArray>();
|
|
ASSERT_EQ(2U, parsed_path.size());
|
|
EXPECT_STREQ("aabb", parsed_path[0].as<const char*>());
|
|
EXPECT_STREQ("012f", parsed_path[1].as<const char*>());
|
|
}
|
|
|
|
TEST(MQTTPayloadBuilder, TxPacketOmitsReceiveOnlyMetricsAndAbsentPath) {
|
|
JsonDocument scratch;
|
|
char buffer[2048];
|
|
int len = buildRepresentativePacket(
|
|
scratch, "tx", 0.5f, nullptr, 0, 0, "A0B1", buffer, sizeof(buffer));
|
|
|
|
ASSERT_GT(len, 0);
|
|
JsonDocument parsed;
|
|
ASSERT_FALSE(deserializeJson(parsed, buffer));
|
|
EXPECT_FALSE(parsed["SNR"].is<JsonVariant>());
|
|
EXPECT_FALSE(parsed["RSSI"].is<JsonVariant>());
|
|
EXPECT_FALSE(parsed["score"].is<JsonVariant>());
|
|
EXPECT_FALSE(parsed["path"].is<JsonVariant>());
|
|
}
|
|
|
|
TEST(MQTTPayloadBuilder, RxPacketOmitsUnknownNanScore) {
|
|
JsonDocument scratch;
|
|
char buffer[2048];
|
|
int len = buildRepresentativePacket(
|
|
scratch, "rx", std::nanf(""), nullptr, 0, 0, "A0B1", buffer, sizeof(buffer));
|
|
|
|
ASSERT_GT(len, 0);
|
|
JsonDocument parsed;
|
|
ASSERT_FALSE(deserializeJson(parsed, buffer));
|
|
EXPECT_TRUE(parsed["SNR"].is<const char*>());
|
|
EXPECT_TRUE(parsed["RSSI"].is<const char*>());
|
|
EXPECT_FALSE(parsed["score"].is<JsonVariant>());
|
|
}
|
|
|
|
TEST(MQTTPayloadBuilder, RawMessageHasExactContractAndEscapesData) {
|
|
char buffer[512];
|
|
int len = MQTTPayloadBuilder::buildRawMessage(
|
|
"node \"A\"", "id\\1", kTimestamp, "AA\nBB", buffer, sizeof(buffer));
|
|
|
|
ASSERT_GT(len, 0);
|
|
EXPECT_EQ(static_cast<size_t>(len), strlen(buffer));
|
|
EXPECT_STREQ(
|
|
"{\"origin\":\"node \\\"A\\\"\",\"origin_id\":\"id\\\\1\","
|
|
"\"timestamp\":\"2026-07-18T12:34:56.123456+00:00\","
|
|
"\"type\":\"RAW\",\"data\":\"AA\\nBB\"}",
|
|
buffer);
|
|
JsonDocument parsed;
|
|
ASSERT_FALSE(deserializeJson(parsed, buffer));
|
|
EXPECT_STREQ("AA\nBB", parsed["data"].as<const char*>());
|
|
}
|
|
|
|
TEST(MQTTPayloadBuilder, ExactOutputSizeSucceedsAndOneByteShortFailsCleanly) {
|
|
JsonDocument scratch;
|
|
char reference[768];
|
|
int reference_len = buildMinimalStatus(scratch, reference, sizeof(reference));
|
|
ASSERT_GT(reference_len, 0);
|
|
|
|
std::vector<char> exact(static_cast<size_t>(reference_len) + 1);
|
|
EXPECT_EQ(reference_len, buildMinimalStatus(scratch, exact.data(), exact.size()));
|
|
EXPECT_STREQ(reference, exact.data());
|
|
|
|
std::vector<char> short_buffer(static_cast<size_t>(reference_len), 'x');
|
|
EXPECT_EQ(0, buildMinimalStatus(scratch, short_buffer.data(), short_buffer.size()));
|
|
EXPECT_EQ('\0', short_buffer[0]);
|
|
EXPECT_EQ(0, buildMinimalStatus(scratch, short_buffer.data(), 1));
|
|
EXPECT_EQ('\0', short_buffer[0]);
|
|
EXPECT_EQ(0, buildMinimalStatus(scratch, nullptr, exact.size()));
|
|
}
|
|
|
|
TEST(MQTTPayloadBuilder, MaximumRepresentativePacketAndRawPayloadsRemainValid) {
|
|
uint8_t path[64];
|
|
for (size_t i = 0; i < sizeof(path); ++i) path[i] = static_cast<uint8_t>(i);
|
|
std::string raw(510, 'A');
|
|
|
|
JsonDocument scratch;
|
|
char packet_buffer[2048];
|
|
int packet_len = buildRepresentativePacket(
|
|
scratch, "rx", 1.0f, path, 16, 4, raw.c_str(),
|
|
packet_buffer, sizeof(packet_buffer));
|
|
ASSERT_GT(packet_len, 0);
|
|
JsonDocument packet;
|
|
ASSERT_FALSE(deserializeJson(packet, packet_buffer));
|
|
EXPECT_EQ(510U, strlen(packet["raw"].as<const char*>()));
|
|
JsonArray parsed_path = packet["path"].as<JsonArray>();
|
|
ASSERT_EQ(16U, parsed_path.size());
|
|
EXPECT_STREQ("00010203", parsed_path[0].as<const char*>());
|
|
EXPECT_STREQ("3c3d3e3f", parsed_path[15].as<const char*>());
|
|
|
|
char raw_buffer[1024];
|
|
int raw_len = MQTTPayloadBuilder::buildRawMessage(
|
|
"node", "0123456789ABCDEF", kTimestamp, raw.c_str(),
|
|
raw_buffer, sizeof(raw_buffer));
|
|
ASSERT_GT(raw_len, 0);
|
|
JsonDocument parsed_raw;
|
|
ASSERT_FALSE(deserializeJson(parsed_raw, raw_buffer));
|
|
EXPECT_EQ(510U, strlen(parsed_raw["data"].as<const char*>()));
|
|
}
|
|
|
|
} // namespace
|
|
|
|
int main(int argc, char** argv) {
|
|
::testing::InitGoogleTest(&argc, argv);
|
|
return RUN_ALL_TESTS();
|
|
}
|
|
|