Signed-off-by: 吴文峰 <kevin@lmve.net>

This commit is contained in:
2026-03-03 22:16:00 +08:00
parent 7ae6e9e999
commit 88d56e1e9e
1660 changed files with 281430 additions and 0 deletions
+34
View File
@@ -0,0 +1,34 @@
#include "SerialConsole.h"
#include "concurrency/OSThread.h"
#include "gps/RTC.h"
#include "TestUtil.h"
#if defined(ARDUINO)
#include <Arduino.h>
#else
#include <chrono>
#include <thread>
#endif
void initializeTestEnvironment()
{
concurrency::hasBeenSetup = true;
consoleInit();
#if ARCH_PORTDUINO
struct timeval tv;
tv.tv_sec = time(NULL);
tv.tv_usec = 0;
perhapsSetRTC(RTCQualityNTP, &tv);
#endif
concurrency::OSThread::setup();
}
void testDelay(unsigned long ms)
{
#if defined(ARDUINO)
::delay(ms);
#else
std::this_thread::sleep_for(std::chrono::milliseconds(ms));
#endif
}
+7
View File
@@ -0,0 +1,7 @@
#pragma once
// Initialize testing environment.
void initializeTestEnvironment();
// Portable delay for tests (Arduino or host).
void testDelay(unsigned long ms);
+216
View File
@@ -0,0 +1,216 @@
#include <string.h>
#include <unity.h>
#include "TestUtil.h"
#include "meshUtils.h"
void setUp(void)
{
// set stuff up here
}
void tearDown(void)
{
// clean stuff up here
}
/**
* Test normal string without embedded nulls
* Should behave the same as strlen() for regular strings
*/
void test_normal_string(void)
{
char test_str[32] = "Hello World";
size_t expected = 11; // strlen("Hello World")
size_t result = pb_string_length(test_str, sizeof(test_str));
TEST_ASSERT_EQUAL_size_t(expected, result);
}
/**
* Test empty string
* Should return 0 for empty string
*/
void test_empty_string(void)
{
char test_str[32] = "";
size_t expected = 0;
size_t result = pb_string_length(test_str, sizeof(test_str));
TEST_ASSERT_EQUAL_size_t(expected, result);
}
/**
* Test string with only trailing nulls
* Common case - string followed by null padding
*/
void test_trailing_nulls(void)
{
char test_str[32] = {0};
strcpy(test_str, "Test");
// test_str is now: "Test\0\0\0\0..." (4 chars + 28 nulls)
size_t expected = 4;
size_t result = pb_string_length(test_str, sizeof(test_str));
TEST_ASSERT_EQUAL_size_t(expected, result);
}
/**
* Test string with embedded null byte
* This is the critical bug case - strlen() would truncate at first null
*/
void test_embedded_null(void)
{
char test_str[32] = {0};
// Create string "ABC\0XYZ" (embedded null after C)
test_str[0] = 'A';
test_str[1] = 'B';
test_str[2] = 'C';
test_str[3] = '\0'; // embedded null
test_str[4] = 'X';
test_str[5] = 'Y';
test_str[6] = 'Z';
// Rest is already null from initialization
// strlen would return 3, but pb_string_length should return 7
size_t strlen_result = strlen(test_str);
size_t pb_result = pb_string_length(test_str, sizeof(test_str));
TEST_ASSERT_EQUAL_size_t(3, strlen_result); // strlen stops at first null
TEST_ASSERT_EQUAL_size_t(7, pb_result); // pb_string_length finds last non-null
}
/**
* Test Android UID with embedded null bytes
* Real-world case from bug report: ANDROID-e7e455b40002429d
* The "00" in the UID represents 0x00 bytes that were truncating the string
*/
void test_android_uid_pattern(void)
{
char test_str[32] = {0};
// Simulate "ANDROID-e7e455b4" + 0x00 + 0x00 + "2429d"
const char part1[] = "ANDROID-e7e455b4";
strcpy(test_str, part1);
size_t pos = strlen(part1);
test_str[pos] = '\0'; // embedded null
test_str[pos + 1] = '\0'; // another embedded null
strcpy(test_str + pos + 2, "2429d");
// The full UID should be 24 characters
size_t strlen_result = strlen(test_str);
size_t pb_result = pb_string_length(test_str, sizeof(test_str));
TEST_ASSERT_EQUAL_size_t(16, strlen_result); // strlen truncates to "ANDROID-e7e455b4"
TEST_ASSERT_EQUAL_size_t(23, pb_result); // pb_string_length gets full length
}
/**
* Test string with multiple embedded nulls
* Edge case with several null bytes scattered through the string
*/
void test_multiple_embedded_nulls(void)
{
char test_str[32] = {0};
// Create "A\0B\0C\0D" (3 embedded nulls)
test_str[0] = 'A';
test_str[1] = '\0';
test_str[2] = 'B';
test_str[3] = '\0';
test_str[4] = 'C';
test_str[5] = '\0';
test_str[6] = 'D';
size_t strlen_result = strlen(test_str);
size_t pb_result = pb_string_length(test_str, sizeof(test_str));
TEST_ASSERT_EQUAL_size_t(1, strlen_result); // strlen stops at first null
TEST_ASSERT_EQUAL_size_t(7, pb_result); // pb_string_length finds all chars
}
/**
* Test buffer completely filled with non-null characters
* Edge case where string uses entire buffer
*/
void test_full_buffer(void)
{
char test_str[8];
// Fill entire buffer with 'X'
memset(test_str, 'X', sizeof(test_str));
size_t result = pb_string_length(test_str, sizeof(test_str));
TEST_ASSERT_EQUAL_size_t(8, result);
}
/**
* Test buffer with all nulls
* Should return 0
*/
void test_all_nulls(void)
{
char test_str[32] = {0};
size_t result = pb_string_length(test_str, sizeof(test_str));
TEST_ASSERT_EQUAL_size_t(0, result);
}
/**
* Test single character followed by nulls
* Minimal non-empty case
*/
void test_single_char(void)
{
char test_str[32] = {0};
test_str[0] = 'X';
size_t result = pb_string_length(test_str, sizeof(test_str));
TEST_ASSERT_EQUAL_size_t(1, result);
}
/**
* Test callsign field typical size
* Test with typical ATAK callsign field size (64 bytes)
*/
void test_callsign_field_size(void)
{
char test_str[64] = {0};
strcpy(test_str, "CALLSIGN-123");
size_t result = pb_string_length(test_str, sizeof(test_str));
TEST_ASSERT_EQUAL_size_t(12, result);
}
/**
* Test with data at end of buffer
* String with embedded null and data at very end
*/
void test_data_at_buffer_end(void)
{
char test_str[10] = {0};
test_str[0] = 'A';
test_str[1] = '\0';
test_str[8] = 'Z'; // Data near end
test_str[9] = 'X'; // Data at end
size_t result = pb_string_length(test_str, sizeof(test_str));
TEST_ASSERT_EQUAL_size_t(10, result); // Should find the 'X' at position 9
}
void setup()
{
// NOTE!!! Wait for >2 secs
// if board doesn't support software reset via Serial.DTR/RTS
testDelay(10);
testDelay(2000);
UNITY_BEGIN();
RUN_TEST(test_normal_string);
RUN_TEST(test_empty_string);
RUN_TEST(test_trailing_nulls);
RUN_TEST(test_embedded_null);
RUN_TEST(test_android_uid_pattern);
RUN_TEST(test_multiple_embedded_nulls);
RUN_TEST(test_full_buffer);
RUN_TEST(test_all_nulls);
RUN_TEST(test_single_char);
RUN_TEST(test_callsign_field_size);
RUN_TEST(test_data_at_buffer_end);
exit(UNITY_END());
}
void loop() {}
@@ -0,0 +1,198 @@
// trunk-ignore-all(gitleaks): These are dummy values. Not real secrets.
#include "CryptoEngine.h"
#include "TestUtil.h"
#include <unity.h>
void HexToBytes(uint8_t *result, const std::string hex, size_t len = 0)
{
if (len) {
memset(result, 0, len);
}
for (unsigned int i = 0; i < hex.length(); i += 2) {
std::string byteString = hex.substr(i, 2);
result[i / 2] = (uint8_t)strtol(byteString.c_str(), NULL, 16);
}
return;
}
void setUp(void)
{
// set stuff up here
}
void tearDown(void)
{
// clean stuff up here
}
void test_SHA256(void)
{
uint8_t expected[32];
uint8_t hash[32] = {0};
HexToBytes(expected, "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855");
crypto->hash(hash, 0);
TEST_ASSERT_EQUAL_MEMORY(hash, expected, 32);
HexToBytes(hash, "d3", 32);
HexToBytes(expected, "28969cdfa74a12c82f3bad960b0b000aca2ac329deea5c2328ebc6f2ba9802c1");
crypto->hash(hash, 1);
TEST_ASSERT_EQUAL_MEMORY(hash, expected, 32);
HexToBytes(hash, "11af", 32);
HexToBytes(expected, "5ca7133fa735326081558ac312c620eeca9970d1e70a4b95533d956f072d1f98");
crypto->hash(hash, 2);
TEST_ASSERT_EQUAL_MEMORY(hash, expected, 32);
}
void test_ECB_AES256(void)
{
// https://csrc.nist.gov/CSRC/media/Projects/Cryptographic-Standards-and-Guidelines/documents/examples/AES_ECB.pdf
uint8_t key[32] = {0};
uint8_t plain[16] = {0};
uint8_t result[16] = {0};
uint8_t expected[16] = {0};
HexToBytes(key, "603DEB1015CA71BE2B73AEF0857D77811F352C073B6108D72D9810A30914DFF4");
HexToBytes(plain, "6BC1BEE22E409F96E93D7E117393172A");
HexToBytes(expected, "F3EED1BDB5D2A03C064B5A7E3DB181F8");
crypto->aesSetKey(key, 32);
crypto->aesEncrypt(plain, result); // Does 16 bytes at a time
TEST_ASSERT_EQUAL_MEMORY(expected, result, 16);
HexToBytes(plain, "AE2D8A571E03AC9C9EB76FAC45AF8E51");
HexToBytes(expected, "591CCB10D410ED26DC5BA74A31362870");
crypto->aesSetKey(key, 32);
crypto->aesEncrypt(plain, result); // Does 16 bytes at a time
TEST_ASSERT_EQUAL_MEMORY(expected, result, 16);
HexToBytes(plain, "30C81C46A35CE411E5FBC1191A0A52EF");
HexToBytes(expected, "B6ED21B99CA6F4F9F153E7B1BEAFED1D");
crypto->aesSetKey(key, 32);
crypto->aesEncrypt(plain, result); // Does 16 bytes at a time
TEST_ASSERT_EQUAL_MEMORY(expected, result, 16);
}
void test_DH25519(void)
{
// test vectors from wycheproof x25519
// https://github.com/C2SP/wycheproof/blob/master/testvectors/x25519_test.json
uint8_t private_key[32];
uint8_t public_key[32];
uint8_t expected_shared[32];
HexToBytes(public_key, "504a36999f489cd2fdbc08baff3d88fa00569ba986cba22548ffde80f9806829");
HexToBytes(private_key, "c8a9d5a91091ad851c668b0736c1c9a02936c0d3ad62670858088047ba057475");
HexToBytes(expected_shared, "436a2c040cf45fea9b29a0cb81b1f41458f863d0d61b453d0a982720d6d61320");
crypto->setDHPrivateKey(private_key);
TEST_ASSERT(crypto->setDHPublicKey(public_key));
TEST_ASSERT_EQUAL_MEMORY(expected_shared, crypto->shared_key, 32);
HexToBytes(public_key, "63aa40c6e38346c5caf23a6df0a5e6c80889a08647e551b3563449befcfc9733");
HexToBytes(private_key, "d85d8c061a50804ac488ad774ac716c3f5ba714b2712e048491379a500211958");
HexToBytes(expected_shared, "279df67a7c4611db4708a0e8282b195e5ac0ed6f4b2f292c6fbd0acac30d1332");
crypto->setDHPrivateKey(private_key);
TEST_ASSERT(crypto->setDHPublicKey(public_key));
TEST_ASSERT_EQUAL_MEMORY(expected_shared, crypto->shared_key, 32);
HexToBytes(public_key, "ecffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f");
HexToBytes(private_key, "18630f93598637c35da623a74559cf944374a559114c7937811041fc8605564a");
crypto->setDHPrivateKey(private_key);
TEST_ASSERT(!crypto->setDHPublicKey(public_key)); // Weak public key results in 0 shared key
HexToBytes(public_key, "f7e13a1a067d2f4e1061bf9936fde5be6b0c2494a8f809cbac7f290ef719e91c");
HexToBytes(private_key, "10300724f3bea134eb1575245ef26ff9b8ccd59849cd98ce1a59002fe1d5986c");
HexToBytes(expected_shared, "24becd5dfed9e9289ba2e15b82b0d54f8e9aacb72f5e4248c58d8d74b451ce76");
crypto->setDHPrivateKey(private_key);
TEST_ASSERT(crypto->setDHPublicKey(public_key));
crypto->hash(crypto->shared_key, 32);
TEST_ASSERT_EQUAL_MEMORY(expected_shared, crypto->shared_key, 32);
}
void test_PKC(void)
{
uint8_t private_key[32];
meshtastic_UserLite_public_key_t public_key;
uint8_t expected_shared[32];
uint8_t expected_decrypted[32];
uint8_t radioBytes[128] __attribute__((__aligned__));
uint8_t decrypted[128] __attribute__((__aligned__));
uint8_t expected_nonce[16];
uint32_t fromNode = 0x0929;
uint64_t packetNum = 0x13b2d662;
HexToBytes(public_key.bytes, "db18fc50eea47f00251cb784819a3cf5fc361882597f589f0d7ff820e8064457");
public_key.size = 32;
HexToBytes(private_key, "a00330633e63522f8a4d81ec6d9d1e6617f6c8ffd3a4c698229537d44e522277");
HexToBytes(expected_shared, "777b1545c9d6f9a2");
HexToBytes(expected_decrypted, "08011204746573744800");
HexToBytes(radioBytes, "8c646d7a2909000062d6b2136b00000040df24abfcc30a17a3d9046726099e796a1c036a792b");
HexToBytes(expected_nonce, "62d6b213036a792b2909000000");
crypto->setDHPrivateKey(private_key);
TEST_ASSERT(crypto->decryptCurve25519(fromNode, public_key, packetNum, 22, radioBytes + 16, decrypted));
TEST_ASSERT_EQUAL_MEMORY(expected_shared, crypto->shared_key, 8);
TEST_ASSERT_EQUAL_MEMORY(expected_nonce, crypto->nonce, 13);
TEST_ASSERT_EQUAL_MEMORY(expected_decrypted, decrypted, 10);
uint32_t toNode = 0; // Only impacts logging
uint8_t encrypted[128] __attribute__((__aligned__));
TEST_ASSERT(crypto->encryptCurve25519(toNode, fromNode, public_key, packetNum, 10, decrypted, encrypted));
TEST_ASSERT_EQUAL_MEMORY(expected_shared, crypto->shared_key, 8);
// The extraNonce is random, so skip checking the nonce and encrypted output here
// Copy the nonce to check it after encryption
memcpy(expected_nonce, crypto->nonce, 16);
// Decrypt the re-encrypted bytes and check they are the same as what we expect
TEST_ASSERT(crypto->decryptCurve25519(fromNode, public_key, packetNum, 22, encrypted, decrypted));
TEST_ASSERT_EQUAL_MEMORY(expected_shared, crypto->shared_key, 8);
TEST_ASSERT_EQUAL_MEMORY(expected_nonce, crypto->nonce, 13);
TEST_ASSERT_EQUAL_MEMORY(expected_decrypted, decrypted, 10);
}
void test_AES_CTR(void)
{
uint8_t expected[32];
uint8_t plain[32];
uint8_t nonce[32];
CryptoKey k;
// vectors from https://www.rfc-editor.org/rfc/rfc3686#section-6
k.length = 32;
HexToBytes(k.bytes, "776BEFF2851DB06F4C8A0542C8696F6C6A81AF1EEC96B4D37FC1D689E6C1C104");
HexToBytes(nonce, "00000060DB5672C97AA8F0B200000001");
HexToBytes(expected, "145AD01DBF824EC7560863DC71E3E0C0");
memcpy(plain, "Single block msg", 16);
crypto->encryptAESCtr(k, nonce, 16, plain);
TEST_ASSERT_EQUAL_MEMORY(expected, plain, 16);
k.length = 16;
memcpy(plain, "Single block msg", 16);
HexToBytes(k.bytes, "AE6852F8121067CC4BF7A5765577F39E");
HexToBytes(nonce, "00000030000000000000000000000001");
HexToBytes(expected, "E4095D4FB7A7B3792D6175A3261311B8");
crypto->encryptAESCtr(k, nonce, 16, plain);
TEST_ASSERT_EQUAL_MEMORY(expected, plain, 16);
}
void setup()
{
// NOTE!!! Wait for >2 secs
// if board doesn't support software reset via Serial.DTR/RTS
delay(10);
delay(2000);
initializeTestEnvironment();
UNITY_BEGIN(); // IMPORTANT LINE!
RUN_TEST(test_SHA256);
RUN_TEST(test_ECB_AES256);
RUN_TEST(test_DH25519);
RUN_TEST(test_AES_CTR);
RUN_TEST(test_PKC);
exit(UNITY_END()); // stop unit testing
}
void loop() {}
@@ -0,0 +1,19 @@
#include "TestUtil.h"
#include <unity.h>
static void test_placeholder()
{
TEST_ASSERT_TRUE(true);
}
extern "C" {
void setup()
{
initializeTestEnvironment();
UNITY_BEGIN();
RUN_TEST(test_placeholder);
exit(UNITY_END());
}
void loop() {}
}
@@ -0,0 +1,116 @@
#include "MeshModule.h"
#include "MeshTypes.h"
#include "TestUtil.h"
#include <unity.h>
// Minimal concrete subclass for testing the base class helper
class TestModule : public MeshModule
{
public:
TestModule() : MeshModule("TestModule") {}
virtual bool wantPacket(const meshtastic_MeshPacket *p) override { return true; }
using MeshModule::currentRequest;
using MeshModule::isMultiHopBroadcastRequest;
};
static TestModule *testModule;
static meshtastic_MeshPacket testPacket;
void setUp(void)
{
testModule = new TestModule();
memset(&testPacket, 0, sizeof(testPacket));
TestModule::currentRequest = &testPacket;
}
void tearDown(void)
{
TestModule::currentRequest = NULL;
delete testModule;
}
// Zero-hop broadcast (hop_limit == hop_start): should be allowed
static void test_zeroHopBroadcast_isAllowed()
{
testPacket.to = NODENUM_BROADCAST;
testPacket.hop_start = 3;
testPacket.hop_limit = 3; // Not yet relayed
TEST_ASSERT_FALSE(testModule->isMultiHopBroadcastRequest());
}
// Multi-hop broadcast (hop_limit < hop_start): should be blocked
static void test_multiHopBroadcast_isBlocked()
{
testPacket.to = NODENUM_BROADCAST;
testPacket.hop_start = 7;
testPacket.hop_limit = 4; // Already relayed 3 hops
TEST_ASSERT_TRUE(testModule->isMultiHopBroadcastRequest());
}
// Direct message (not broadcast): should always be allowed regardless of hops
static void test_directMessage_isAllowed()
{
testPacket.to = 0x12345678; // Specific node
testPacket.hop_start = 7;
testPacket.hop_limit = 4;
TEST_ASSERT_FALSE(testModule->isMultiHopBroadcastRequest());
}
// Broadcast with hop_limit == 0 (fully relayed): should be blocked
static void test_fullyRelayedBroadcast_isBlocked()
{
testPacket.to = NODENUM_BROADCAST;
testPacket.hop_start = 3;
testPacket.hop_limit = 0;
TEST_ASSERT_TRUE(testModule->isMultiHopBroadcastRequest());
}
// No current request: should not crash, should return false
static void test_noCurrentRequest_isAllowed()
{
TestModule::currentRequest = NULL;
TEST_ASSERT_FALSE(testModule->isMultiHopBroadcastRequest());
}
// Broadcast with hop_start == 0 (legacy or local): should be allowed
static void test_legacyPacket_zeroHopStart_isAllowed()
{
testPacket.to = NODENUM_BROADCAST;
testPacket.hop_start = 0;
testPacket.hop_limit = 0;
// hop_limit == hop_start, so not multi-hop
TEST_ASSERT_FALSE(testModule->isMultiHopBroadcastRequest());
}
// Single hop relayed broadcast (hop_limit = hop_start - 1): should be blocked
static void test_singleHopRelayedBroadcast_isBlocked()
{
testPacket.to = NODENUM_BROADCAST;
testPacket.hop_start = 3;
testPacket.hop_limit = 2;
TEST_ASSERT_TRUE(testModule->isMultiHopBroadcastRequest());
}
void setup()
{
initializeTestEnvironment();
UNITY_BEGIN();
RUN_TEST(test_zeroHopBroadcast_isAllowed);
RUN_TEST(test_multiHopBroadcast_isBlocked);
RUN_TEST(test_directMessage_isAllowed);
RUN_TEST(test_fullyRelayedBroadcast_isBlocked);
RUN_TEST(test_noCurrentRequest_isAllowed);
RUN_TEST(test_legacyPacket_zeroHopStart_isAllowed);
RUN_TEST(test_singleHopRelayedBroadcast_isBlocked);
exit(UNITY_END());
}
void loop() {}
@@ -0,0 +1,59 @@
#include "../test_helpers.h"
// Helper function for all encrypted packet assertions
void assert_encrypted_packet(const std::string &json, meshtastic_MeshPacket packet)
{
// Parse and validate JSON
TEST_ASSERT_TRUE(json.length() > 0);
JSONValue *root = JSON::Parse(json.c_str());
TEST_ASSERT_NOT_NULL(root);
TEST_ASSERT_TRUE(root->IsObject());
JSONObject jsonObj = root->AsObject();
// Assert basic packet fields
TEST_ASSERT_TRUE(jsonObj.find("from") != jsonObj.end());
TEST_ASSERT_EQUAL(packet.from, (uint32_t)jsonObj.at("from")->AsNumber());
TEST_ASSERT_TRUE(jsonObj.find("to") != jsonObj.end());
TEST_ASSERT_EQUAL(packet.to, (uint32_t)jsonObj.at("to")->AsNumber());
TEST_ASSERT_TRUE(jsonObj.find("id") != jsonObj.end());
TEST_ASSERT_EQUAL(packet.id, (uint32_t)jsonObj.at("id")->AsNumber());
// Assert encrypted data fields
TEST_ASSERT_TRUE(jsonObj.find("bytes") != jsonObj.end());
TEST_ASSERT_TRUE(jsonObj.at("bytes")->IsString());
TEST_ASSERT_TRUE(jsonObj.find("size") != jsonObj.end());
TEST_ASSERT_EQUAL(packet.encrypted.size, (int)jsonObj.at("size")->AsNumber());
// Assert hex encoding
std::string encrypted_hex = jsonObj["bytes"]->AsString();
TEST_ASSERT_EQUAL(packet.encrypted.size * 2, encrypted_hex.length());
delete root;
}
// Test encrypted packet serialization
void test_encrypted_packet_serialization()
{
const char *data = "encrypted_payload_data";
meshtastic_MeshPacket packet =
create_test_packet(meshtastic_PortNum_TEXT_MESSAGE_APP, reinterpret_cast<const uint8_t *>(data), strlen(data),
meshtastic_MeshPacket_encrypted_tag);
std::string json = MeshPacketSerializer::JsonSerializeEncrypted(&packet);
assert_encrypted_packet(json, packet);
}
// Test empty encrypted packet
void test_empty_encrypted_packet()
{
meshtastic_MeshPacket packet =
create_test_packet(meshtastic_PortNum_TEXT_MESSAGE_APP, nullptr, 0, meshtastic_MeshPacket_encrypted_tag);
std::string json = MeshPacketSerializer::JsonSerializeEncrypted(&packet);
assert_encrypted_packet(json, packet);
}
@@ -0,0 +1,51 @@
#include "../test_helpers.h"
static size_t encode_user_info(uint8_t *buffer, size_t buffer_size)
{
meshtastic_User user = meshtastic_User_init_zero;
strcpy(user.short_name, "TEST");
strcpy(user.long_name, "Test User");
strcpy(user.id, "!12345678");
user.hw_model = meshtastic_HardwareModel_HELTEC_V3;
pb_ostream_t stream = pb_ostream_from_buffer(buffer, buffer_size);
pb_encode(&stream, &meshtastic_User_msg, &user);
return stream.bytes_written;
}
// Test NODEINFO_APP port
void test_nodeinfo_serialization()
{
uint8_t buffer[256];
size_t payload_size = encode_user_info(buffer, sizeof(buffer));
meshtastic_MeshPacket packet = create_test_packet(meshtastic_PortNum_NODEINFO_APP, buffer, payload_size);
std::string json = MeshPacketSerializer::JsonSerialize(&packet, false);
TEST_ASSERT_TRUE(json.length() > 0);
JSONValue *root = JSON::Parse(json.c_str());
TEST_ASSERT_NOT_NULL(root);
TEST_ASSERT_TRUE(root->IsObject());
JSONObject jsonObj = root->AsObject();
// Check message type
TEST_ASSERT_TRUE(jsonObj.find("type") != jsonObj.end());
TEST_ASSERT_EQUAL_STRING("nodeinfo", jsonObj["type"]->AsString().c_str());
// Check payload
TEST_ASSERT_TRUE(jsonObj.find("payload") != jsonObj.end());
TEST_ASSERT_TRUE(jsonObj["payload"]->IsObject());
JSONObject payload = jsonObj["payload"]->AsObject();
// Verify user data
TEST_ASSERT_TRUE(payload.find("shortname") != payload.end());
TEST_ASSERT_EQUAL_STRING("TEST", payload["shortname"]->AsString().c_str());
TEST_ASSERT_TRUE(payload.find("longname") != payload.end());
TEST_ASSERT_EQUAL_STRING("Test User", payload["longname"]->AsString().c_str());
delete root;
}
@@ -0,0 +1,57 @@
#include "../test_helpers.h"
static size_t encode_position(uint8_t *buffer, size_t buffer_size)
{
meshtastic_Position position = meshtastic_Position_init_zero;
position.latitude_i = 374208000; // 37.4208 degrees * 1e7
position.longitude_i = -1221981000; // -122.1981 degrees * 1e7
position.altitude = 123;
position.time = 1609459200;
position.has_altitude = true;
position.has_latitude_i = true;
position.has_longitude_i = true;
pb_ostream_t stream = pb_ostream_from_buffer(buffer, buffer_size);
pb_encode(&stream, &meshtastic_Position_msg, &position);
return stream.bytes_written;
}
// Test POSITION_APP port
void test_position_serialization()
{
uint8_t buffer[256];
size_t payload_size = encode_position(buffer, sizeof(buffer));
meshtastic_MeshPacket packet = create_test_packet(meshtastic_PortNum_POSITION_APP, buffer, payload_size);
std::string json = MeshPacketSerializer::JsonSerialize(&packet, false);
TEST_ASSERT_TRUE(json.length() > 0);
JSONValue *root = JSON::Parse(json.c_str());
TEST_ASSERT_NOT_NULL(root);
TEST_ASSERT_TRUE(root->IsObject());
JSONObject jsonObj = root->AsObject();
// Check message type
TEST_ASSERT_TRUE(jsonObj.find("type") != jsonObj.end());
TEST_ASSERT_EQUAL_STRING("position", jsonObj["type"]->AsString().c_str());
// Check payload
TEST_ASSERT_TRUE(jsonObj.find("payload") != jsonObj.end());
TEST_ASSERT_TRUE(jsonObj["payload"]->IsObject());
JSONObject payload = jsonObj["payload"]->AsObject();
// Verify position data
TEST_ASSERT_TRUE(payload.find("latitude_i") != payload.end());
TEST_ASSERT_EQUAL(374208000, (int)payload["latitude_i"]->AsNumber());
TEST_ASSERT_TRUE(payload.find("longitude_i") != payload.end());
TEST_ASSERT_EQUAL(-1221981000, (int)payload["longitude_i"]->AsNumber());
TEST_ASSERT_TRUE(payload.find("altitude") != payload.end());
TEST_ASSERT_EQUAL(123, (int)payload["altitude"]->AsNumber());
delete root;
}
@@ -0,0 +1,528 @@
#include "../test_helpers.h"
// Helper function to create and encode device metrics
static size_t encode_telemetry_device_metrics(uint8_t *buffer, size_t buffer_size)
{
meshtastic_Telemetry telemetry = meshtastic_Telemetry_init_zero;
telemetry.time = 1609459200;
telemetry.which_variant = meshtastic_Telemetry_device_metrics_tag;
telemetry.variant.device_metrics.battery_level = 85;
telemetry.variant.device_metrics.has_battery_level = true;
telemetry.variant.device_metrics.voltage = 3.72f;
telemetry.variant.device_metrics.has_voltage = true;
telemetry.variant.device_metrics.channel_utilization = 15.56f;
telemetry.variant.device_metrics.has_channel_utilization = true;
telemetry.variant.device_metrics.air_util_tx = 8.23f;
telemetry.variant.device_metrics.has_air_util_tx = true;
telemetry.variant.device_metrics.uptime_seconds = 12345;
telemetry.variant.device_metrics.has_uptime_seconds = true;
pb_ostream_t stream = pb_ostream_from_buffer(buffer, buffer_size);
pb_encode(&stream, &meshtastic_Telemetry_msg, &telemetry);
return stream.bytes_written;
}
// Helper function to create and encode empty environment metrics (no fields set)
static size_t encode_telemetry_environment_metrics_empty(uint8_t *buffer, size_t buffer_size)
{
meshtastic_Telemetry telemetry = meshtastic_Telemetry_init_zero;
telemetry.time = 1609459200;
telemetry.which_variant = meshtastic_Telemetry_environment_metrics_tag;
// NO fields are set - all has_* flags remain false
// This tests that empty environment metrics don't produce any JSON fields
pb_ostream_t stream = pb_ostream_from_buffer(buffer, buffer_size);
pb_encode(&stream, &meshtastic_Telemetry_msg, &telemetry);
return stream.bytes_written;
}
// Helper function to create environment metrics with ALL possible fields set
// This function should be updated whenever new fields are added to the protobuf
static size_t encode_telemetry_environment_metrics_all_fields(uint8_t *buffer, size_t buffer_size)
{
meshtastic_Telemetry telemetry = meshtastic_Telemetry_init_zero;
telemetry.time = 1609459200;
telemetry.which_variant = meshtastic_Telemetry_environment_metrics_tag;
// Basic environment metrics
telemetry.variant.environment_metrics.temperature = 23.56f;
telemetry.variant.environment_metrics.has_temperature = true;
telemetry.variant.environment_metrics.relative_humidity = 65.43f;
telemetry.variant.environment_metrics.has_relative_humidity = true;
telemetry.variant.environment_metrics.barometric_pressure = 1013.27f;
telemetry.variant.environment_metrics.has_barometric_pressure = true;
// Gas and air quality
telemetry.variant.environment_metrics.gas_resistance = 50.58f;
telemetry.variant.environment_metrics.has_gas_resistance = true;
telemetry.variant.environment_metrics.iaq = 120;
telemetry.variant.environment_metrics.has_iaq = true;
// Power measurements
telemetry.variant.environment_metrics.voltage = 3.34f;
telemetry.variant.environment_metrics.has_voltage = true;
telemetry.variant.environment_metrics.current = 0.53f;
telemetry.variant.environment_metrics.has_current = true;
// Light measurements (ALL 4 types)
telemetry.variant.environment_metrics.lux = 450.12f;
telemetry.variant.environment_metrics.has_lux = true;
telemetry.variant.environment_metrics.white_lux = 380.95f;
telemetry.variant.environment_metrics.has_white_lux = true;
telemetry.variant.environment_metrics.ir_lux = 25.37f;
telemetry.variant.environment_metrics.has_ir_lux = true;
telemetry.variant.environment_metrics.uv_lux = 15.68f;
telemetry.variant.environment_metrics.has_uv_lux = true;
// Distance measurement
telemetry.variant.environment_metrics.distance = 150.29f;
telemetry.variant.environment_metrics.has_distance = true;
// Wind measurements (ALL 4 types)
telemetry.variant.environment_metrics.wind_direction = 180;
telemetry.variant.environment_metrics.has_wind_direction = true;
telemetry.variant.environment_metrics.wind_speed = 5.52f;
telemetry.variant.environment_metrics.has_wind_speed = true;
telemetry.variant.environment_metrics.wind_gust = 8.24f;
telemetry.variant.environment_metrics.has_wind_gust = true;
telemetry.variant.environment_metrics.wind_lull = 2.13f;
telemetry.variant.environment_metrics.has_wind_lull = true;
// Weight measurement
telemetry.variant.environment_metrics.weight = 75.56f;
telemetry.variant.environment_metrics.has_weight = true;
// Radiation measurement
telemetry.variant.environment_metrics.radiation = 0.13f;
telemetry.variant.environment_metrics.has_radiation = true;
// Rainfall measurements (BOTH types)
telemetry.variant.environment_metrics.rainfall_1h = 2.57f;
telemetry.variant.environment_metrics.has_rainfall_1h = true;
telemetry.variant.environment_metrics.rainfall_24h = 15.89f;
telemetry.variant.environment_metrics.has_rainfall_24h = true;
// Soil measurements (BOTH types)
telemetry.variant.environment_metrics.soil_moisture = 85;
telemetry.variant.environment_metrics.has_soil_moisture = true;
telemetry.variant.environment_metrics.soil_temperature = 18.54f;
telemetry.variant.environment_metrics.has_soil_temperature = true;
// IMPORTANT: When new environment fields are added to the protobuf,
// they MUST be added here too, or the coverage test will fail!
pb_ostream_t stream = pb_ostream_from_buffer(buffer, buffer_size);
pb_encode(&stream, &meshtastic_Telemetry_msg, &telemetry);
return stream.bytes_written;
}
// Helper function to create and encode environment metrics with all current fields
static size_t encode_telemetry_environment_metrics(uint8_t *buffer, size_t buffer_size)
{
meshtastic_Telemetry telemetry = meshtastic_Telemetry_init_zero;
telemetry.time = 1609459200;
telemetry.which_variant = meshtastic_Telemetry_environment_metrics_tag;
// Basic environment metrics
telemetry.variant.environment_metrics.temperature = 23.56f;
telemetry.variant.environment_metrics.has_temperature = true;
telemetry.variant.environment_metrics.relative_humidity = 65.43f;
telemetry.variant.environment_metrics.has_relative_humidity = true;
telemetry.variant.environment_metrics.barometric_pressure = 1013.27f;
telemetry.variant.environment_metrics.has_barometric_pressure = true;
// Gas and air quality
telemetry.variant.environment_metrics.gas_resistance = 50.58f;
telemetry.variant.environment_metrics.has_gas_resistance = true;
telemetry.variant.environment_metrics.iaq = 120;
telemetry.variant.environment_metrics.has_iaq = true;
// Power measurements
telemetry.variant.environment_metrics.voltage = 3.34f;
telemetry.variant.environment_metrics.has_voltage = true;
telemetry.variant.environment_metrics.current = 0.53f;
telemetry.variant.environment_metrics.has_current = true;
// Light measurements
telemetry.variant.environment_metrics.lux = 450.12f;
telemetry.variant.environment_metrics.has_lux = true;
telemetry.variant.environment_metrics.white_lux = 380.95f;
telemetry.variant.environment_metrics.has_white_lux = true;
telemetry.variant.environment_metrics.ir_lux = 25.37f;
telemetry.variant.environment_metrics.has_ir_lux = true;
telemetry.variant.environment_metrics.uv_lux = 15.68f;
telemetry.variant.environment_metrics.has_uv_lux = true;
// Distance measurement
telemetry.variant.environment_metrics.distance = 150.29f;
telemetry.variant.environment_metrics.has_distance = true;
// Wind measurements
telemetry.variant.environment_metrics.wind_direction = 180;
telemetry.variant.environment_metrics.has_wind_direction = true;
telemetry.variant.environment_metrics.wind_speed = 5.52f;
telemetry.variant.environment_metrics.has_wind_speed = true;
telemetry.variant.environment_metrics.wind_gust = 8.24f;
telemetry.variant.environment_metrics.has_wind_gust = true;
telemetry.variant.environment_metrics.wind_lull = 2.13f;
telemetry.variant.environment_metrics.has_wind_lull = true;
// Weight measurement
telemetry.variant.environment_metrics.weight = 75.56f;
telemetry.variant.environment_metrics.has_weight = true;
// Radiation measurement
telemetry.variant.environment_metrics.radiation = 0.13f;
telemetry.variant.environment_metrics.has_radiation = true;
// Rainfall measurements
telemetry.variant.environment_metrics.rainfall_1h = 2.57f;
telemetry.variant.environment_metrics.has_rainfall_1h = true;
telemetry.variant.environment_metrics.rainfall_24h = 15.89f;
telemetry.variant.environment_metrics.has_rainfall_24h = true;
// Soil measurements
telemetry.variant.environment_metrics.soil_moisture = 85;
telemetry.variant.environment_metrics.has_soil_moisture = true;
telemetry.variant.environment_metrics.soil_temperature = 18.54f;
telemetry.variant.environment_metrics.has_soil_temperature = true;
pb_ostream_t stream = pb_ostream_from_buffer(buffer, buffer_size);
pb_encode(&stream, &meshtastic_Telemetry_msg, &telemetry);
return stream.bytes_written;
}
// Test TELEMETRY_APP port with device metrics
void test_telemetry_device_metrics_serialization()
{
uint8_t buffer[256];
size_t payload_size = encode_telemetry_device_metrics(buffer, sizeof(buffer));
meshtastic_MeshPacket packet = create_test_packet(meshtastic_PortNum_TELEMETRY_APP, buffer, payload_size);
std::string json = MeshPacketSerializer::JsonSerialize(&packet, false);
TEST_ASSERT_TRUE(json.length() > 0);
JSONValue *root = JSON::Parse(json.c_str());
TEST_ASSERT_NOT_NULL(root);
TEST_ASSERT_TRUE(root->IsObject());
JSONObject jsonObj = root->AsObject();
// Check message type
TEST_ASSERT_TRUE(jsonObj.find("type") != jsonObj.end());
TEST_ASSERT_EQUAL_STRING("telemetry", jsonObj["type"]->AsString().c_str());
// Check payload
TEST_ASSERT_TRUE(jsonObj.find("payload") != jsonObj.end());
TEST_ASSERT_TRUE(jsonObj["payload"]->IsObject());
JSONObject payload = jsonObj["payload"]->AsObject();
// Verify telemetry data
TEST_ASSERT_TRUE(payload.find("battery_level") != payload.end());
TEST_ASSERT_EQUAL(85, (int)payload["battery_level"]->AsNumber());
TEST_ASSERT_TRUE(payload.find("voltage") != payload.end());
TEST_ASSERT_FLOAT_WITHIN(0.01f, 3.72f, payload["voltage"]->AsNumber());
TEST_ASSERT_TRUE(payload.find("channel_utilization") != payload.end());
TEST_ASSERT_FLOAT_WITHIN(0.01f, 15.56f, payload["channel_utilization"]->AsNumber());
TEST_ASSERT_TRUE(payload.find("uptime_seconds") != payload.end());
TEST_ASSERT_EQUAL(12345, (int)payload["uptime_seconds"]->AsNumber());
// Note: JSON serialization may not preserve exact 2-decimal formatting due to float precision
// We verify the numeric values are correct within tolerance
delete root;
}
// Test that telemetry environment metrics are properly serialized
void test_telemetry_environment_metrics_serialization()
{
uint8_t buffer[256];
size_t payload_size = encode_telemetry_environment_metrics(buffer, sizeof(buffer));
meshtastic_MeshPacket packet = create_test_packet(meshtastic_PortNum_TELEMETRY_APP, buffer, payload_size);
std::string json = MeshPacketSerializer::JsonSerialize(&packet, false);
TEST_ASSERT_TRUE(json.length() > 0);
JSONValue *root = JSON::Parse(json.c_str());
TEST_ASSERT_NOT_NULL(root);
TEST_ASSERT_TRUE(root->IsObject());
JSONObject jsonObj = root->AsObject();
// Check payload exists
TEST_ASSERT_TRUE(jsonObj.find("payload") != jsonObj.end());
TEST_ASSERT_TRUE(jsonObj["payload"]->IsObject());
JSONObject payload = jsonObj["payload"]->AsObject();
// Test key fields that should be present in the serializer
TEST_ASSERT_TRUE(payload.find("temperature") != payload.end());
TEST_ASSERT_FLOAT_WITHIN(0.01f, 23.56f, payload["temperature"]->AsNumber());
TEST_ASSERT_TRUE(payload.find("relative_humidity") != payload.end());
TEST_ASSERT_FLOAT_WITHIN(0.01f, 65.43f, payload["relative_humidity"]->AsNumber());
TEST_ASSERT_TRUE(payload.find("distance") != payload.end());
TEST_ASSERT_FLOAT_WITHIN(0.01f, 150.29f, payload["distance"]->AsNumber());
// Note: JSON serialization may have float precision limitations
// We focus on verifying numeric accuracy rather than exact string formatting
delete root;
}
// Test comprehensive environment metrics coverage
void test_telemetry_environment_metrics_comprehensive()
{
uint8_t buffer[256];
size_t payload_size = encode_telemetry_environment_metrics(buffer, sizeof(buffer));
meshtastic_MeshPacket packet = create_test_packet(meshtastic_PortNum_TELEMETRY_APP, buffer, payload_size);
std::string json = MeshPacketSerializer::JsonSerialize(&packet, false);
TEST_ASSERT_TRUE(json.length() > 0);
JSONValue *root = JSON::Parse(json.c_str());
TEST_ASSERT_NOT_NULL(root);
TEST_ASSERT_TRUE(root->IsObject());
JSONObject jsonObj = root->AsObject();
// Check payload exists
TEST_ASSERT_TRUE(jsonObj.find("payload") != jsonObj.end());
TEST_ASSERT_TRUE(jsonObj["payload"]->IsObject());
JSONObject payload = jsonObj["payload"]->AsObject();
// Check all 15 originally supported fields
TEST_ASSERT_TRUE(payload.find("temperature") != payload.end());
TEST_ASSERT_TRUE(payload.find("relative_humidity") != payload.end());
TEST_ASSERT_TRUE(payload.find("barometric_pressure") != payload.end());
TEST_ASSERT_TRUE(payload.find("gas_resistance") != payload.end());
TEST_ASSERT_TRUE(payload.find("voltage") != payload.end());
TEST_ASSERT_TRUE(payload.find("current") != payload.end());
TEST_ASSERT_TRUE(payload.find("iaq") != payload.end());
TEST_ASSERT_TRUE(payload.find("distance") != payload.end());
TEST_ASSERT_TRUE(payload.find("lux") != payload.end());
TEST_ASSERT_TRUE(payload.find("white_lux") != payload.end());
TEST_ASSERT_TRUE(payload.find("wind_direction") != payload.end());
TEST_ASSERT_TRUE(payload.find("wind_speed") != payload.end());
TEST_ASSERT_TRUE(payload.find("wind_gust") != payload.end());
TEST_ASSERT_TRUE(payload.find("wind_lull") != payload.end());
TEST_ASSERT_TRUE(payload.find("radiation") != payload.end());
delete root;
}
// Test for the 7 environment fields that were added to complete coverage
void test_telemetry_environment_metrics_missing_fields()
{
uint8_t buffer[256];
size_t payload_size = encode_telemetry_environment_metrics(buffer, sizeof(buffer));
meshtastic_MeshPacket packet = create_test_packet(meshtastic_PortNum_TELEMETRY_APP, buffer, payload_size);
std::string json = MeshPacketSerializer::JsonSerialize(&packet, false);
TEST_ASSERT_TRUE(json.length() > 0);
JSONValue *root = JSON::Parse(json.c_str());
TEST_ASSERT_NOT_NULL(root);
TEST_ASSERT_TRUE(root->IsObject());
JSONObject jsonObj = root->AsObject();
// Check payload exists
TEST_ASSERT_TRUE(jsonObj.find("payload") != jsonObj.end());
TEST_ASSERT_TRUE(jsonObj["payload"]->IsObject());
JSONObject payload = jsonObj["payload"]->AsObject();
// Check the 7 fields that were previously missing
TEST_ASSERT_TRUE(payload.find("ir_lux") != payload.end());
TEST_ASSERT_FLOAT_WITHIN(0.01f, 25.37f, payload["ir_lux"]->AsNumber());
TEST_ASSERT_TRUE(payload.find("uv_lux") != payload.end());
TEST_ASSERT_FLOAT_WITHIN(0.01f, 15.68f, payload["uv_lux"]->AsNumber());
TEST_ASSERT_TRUE(payload.find("weight") != payload.end());
TEST_ASSERT_FLOAT_WITHIN(0.01f, 75.56f, payload["weight"]->AsNumber());
TEST_ASSERT_TRUE(payload.find("rainfall_1h") != payload.end());
TEST_ASSERT_FLOAT_WITHIN(0.01f, 2.57f, payload["rainfall_1h"]->AsNumber());
TEST_ASSERT_TRUE(payload.find("rainfall_24h") != payload.end());
TEST_ASSERT_FLOAT_WITHIN(0.01f, 15.89f, payload["rainfall_24h"]->AsNumber());
TEST_ASSERT_TRUE(payload.find("soil_moisture") != payload.end());
TEST_ASSERT_EQUAL(85, (int)payload["soil_moisture"]->AsNumber());
TEST_ASSERT_TRUE(payload.find("soil_temperature") != payload.end());
TEST_ASSERT_FLOAT_WITHIN(0.01f, 18.54f, payload["soil_temperature"]->AsNumber());
// Note: JSON float serialization may not preserve exact decimal formatting
// We verify the values are numerically correct within tolerance
delete root;
}
// Test that ALL environment fields are serialized (canary test for forgotten fields)
// This test will FAIL if a new environment field is added to the protobuf but not to the serializer
void test_telemetry_environment_metrics_complete_coverage()
{
uint8_t buffer[256];
size_t payload_size = encode_telemetry_environment_metrics_all_fields(buffer, sizeof(buffer));
meshtastic_MeshPacket packet = create_test_packet(meshtastic_PortNum_TELEMETRY_APP, buffer, payload_size);
std::string json = MeshPacketSerializer::JsonSerialize(&packet, false);
TEST_ASSERT_TRUE(json.length() > 0);
JSONValue *root = JSON::Parse(json.c_str());
TEST_ASSERT_NOT_NULL(root);
TEST_ASSERT_TRUE(root->IsObject());
JSONObject jsonObj = root->AsObject();
// Check payload exists
TEST_ASSERT_TRUE(jsonObj.find("payload") != jsonObj.end());
TEST_ASSERT_TRUE(jsonObj["payload"]->IsObject());
JSONObject payload = jsonObj["payload"]->AsObject();
// ✅ ALL 22 environment fields MUST be present and correct
// If this test fails, it means either:
// 1. A new field was added to the protobuf but not to the serializer
// 2. The encode_telemetry_environment_metrics_all_fields() function wasn't updated
// Basic environment (3 fields)
TEST_ASSERT_TRUE(payload.find("temperature") != payload.end());
TEST_ASSERT_FLOAT_WITHIN(0.01f, 23.56f, payload["temperature"]->AsNumber());
TEST_ASSERT_TRUE(payload.find("relative_humidity") != payload.end());
TEST_ASSERT_FLOAT_WITHIN(0.01f, 65.43f, payload["relative_humidity"]->AsNumber());
TEST_ASSERT_TRUE(payload.find("barometric_pressure") != payload.end());
TEST_ASSERT_FLOAT_WITHIN(0.01f, 1013.27f, payload["barometric_pressure"]->AsNumber());
// Gas and air quality (2 fields)
TEST_ASSERT_TRUE(payload.find("gas_resistance") != payload.end());
TEST_ASSERT_FLOAT_WITHIN(0.01f, 50.58f, payload["gas_resistance"]->AsNumber());
TEST_ASSERT_TRUE(payload.find("iaq") != payload.end());
TEST_ASSERT_EQUAL(120, (int)payload["iaq"]->AsNumber());
// Power measurements (2 fields)
TEST_ASSERT_TRUE(payload.find("voltage") != payload.end());
TEST_ASSERT_FLOAT_WITHIN(0.01f, 3.34f, payload["voltage"]->AsNumber());
TEST_ASSERT_TRUE(payload.find("current") != payload.end());
TEST_ASSERT_FLOAT_WITHIN(0.01f, 0.53f, payload["current"]->AsNumber());
// Light measurements (4 fields)
TEST_ASSERT_TRUE(payload.find("lux") != payload.end());
TEST_ASSERT_FLOAT_WITHIN(0.01f, 450.12f, payload["lux"]->AsNumber());
TEST_ASSERT_TRUE(payload.find("white_lux") != payload.end());
TEST_ASSERT_FLOAT_WITHIN(0.01f, 380.95f, payload["white_lux"]->AsNumber());
TEST_ASSERT_TRUE(payload.find("ir_lux") != payload.end());
TEST_ASSERT_FLOAT_WITHIN(0.01f, 25.37f, payload["ir_lux"]->AsNumber());
TEST_ASSERT_TRUE(payload.find("uv_lux") != payload.end());
TEST_ASSERT_FLOAT_WITHIN(0.01f, 15.68f, payload["uv_lux"]->AsNumber());
// Distance measurement (1 field)
TEST_ASSERT_TRUE(payload.find("distance") != payload.end());
TEST_ASSERT_FLOAT_WITHIN(0.01f, 150.29f, payload["distance"]->AsNumber());
// Wind measurements (4 fields)
TEST_ASSERT_TRUE(payload.find("wind_direction") != payload.end());
TEST_ASSERT_EQUAL(180, (int)payload["wind_direction"]->AsNumber());
TEST_ASSERT_TRUE(payload.find("wind_speed") != payload.end());
TEST_ASSERT_FLOAT_WITHIN(0.01f, 5.52f, payload["wind_speed"]->AsNumber());
TEST_ASSERT_TRUE(payload.find("wind_gust") != payload.end());
TEST_ASSERT_FLOAT_WITHIN(0.01f, 8.24f, payload["wind_gust"]->AsNumber());
TEST_ASSERT_TRUE(payload.find("wind_lull") != payload.end());
TEST_ASSERT_FLOAT_WITHIN(0.01f, 2.13f, payload["wind_lull"]->AsNumber());
// Weight measurement (1 field)
TEST_ASSERT_TRUE(payload.find("weight") != payload.end());
TEST_ASSERT_FLOAT_WITHIN(0.01f, 75.56f, payload["weight"]->AsNumber());
// Radiation measurement (1 field)
TEST_ASSERT_TRUE(payload.find("radiation") != payload.end());
TEST_ASSERT_FLOAT_WITHIN(0.01f, 0.13f, payload["radiation"]->AsNumber());
// Rainfall measurements (2 fields)
TEST_ASSERT_TRUE(payload.find("rainfall_1h") != payload.end());
TEST_ASSERT_FLOAT_WITHIN(0.01f, 2.57f, payload["rainfall_1h"]->AsNumber());
TEST_ASSERT_TRUE(payload.find("rainfall_24h") != payload.end());
TEST_ASSERT_FLOAT_WITHIN(0.01f, 15.89f, payload["rainfall_24h"]->AsNumber());
// Soil measurements (2 fields)
TEST_ASSERT_TRUE(payload.find("soil_moisture") != payload.end());
TEST_ASSERT_EQUAL(85, (int)payload["soil_moisture"]->AsNumber());
TEST_ASSERT_TRUE(payload.find("soil_temperature") != payload.end());
TEST_ASSERT_FLOAT_WITHIN(0.01f, 18.54f, payload["soil_temperature"]->AsNumber());
// Total: 22 environment fields
// This test ensures 100% coverage of environment metrics
// Note: JSON float serialization precision may vary due to the underlying library
// The important aspect is that all values are numerically accurate within tolerance
delete root;
}
// Test that unset environment fields are not present in JSON
void test_telemetry_environment_metrics_unset_fields()
{
uint8_t buffer[256];
size_t payload_size = encode_telemetry_environment_metrics_empty(buffer, sizeof(buffer));
meshtastic_MeshPacket packet = create_test_packet(meshtastic_PortNum_TELEMETRY_APP, buffer, payload_size);
std::string json = MeshPacketSerializer::JsonSerialize(&packet, false);
TEST_ASSERT_TRUE(json.length() > 0);
JSONValue *root = JSON::Parse(json.c_str());
TEST_ASSERT_NOT_NULL(root);
TEST_ASSERT_TRUE(root->IsObject());
JSONObject jsonObj = root->AsObject();
// Check payload exists
TEST_ASSERT_TRUE(jsonObj.find("payload") != jsonObj.end());
TEST_ASSERT_TRUE(jsonObj["payload"]->IsObject());
JSONObject payload = jsonObj["payload"]->AsObject();
// With completely empty environment metrics, NO fields should be present
// Only basic telemetry fields like "time" might be present
// All 22 environment fields should be absent (none were set)
TEST_ASSERT_TRUE(payload.find("temperature") == payload.end());
TEST_ASSERT_TRUE(payload.find("relative_humidity") == payload.end());
TEST_ASSERT_TRUE(payload.find("barometric_pressure") == payload.end());
TEST_ASSERT_TRUE(payload.find("gas_resistance") == payload.end());
TEST_ASSERT_TRUE(payload.find("iaq") == payload.end());
TEST_ASSERT_TRUE(payload.find("voltage") == payload.end());
TEST_ASSERT_TRUE(payload.find("current") == payload.end());
TEST_ASSERT_TRUE(payload.find("lux") == payload.end());
TEST_ASSERT_TRUE(payload.find("white_lux") == payload.end());
TEST_ASSERT_TRUE(payload.find("ir_lux") == payload.end());
TEST_ASSERT_TRUE(payload.find("uv_lux") == payload.end());
TEST_ASSERT_TRUE(payload.find("distance") == payload.end());
TEST_ASSERT_TRUE(payload.find("wind_direction") == payload.end());
TEST_ASSERT_TRUE(payload.find("wind_speed") == payload.end());
TEST_ASSERT_TRUE(payload.find("wind_gust") == payload.end());
TEST_ASSERT_TRUE(payload.find("wind_lull") == payload.end());
TEST_ASSERT_TRUE(payload.find("weight") == payload.end());
TEST_ASSERT_TRUE(payload.find("radiation") == payload.end());
TEST_ASSERT_TRUE(payload.find("rainfall_1h") == payload.end());
TEST_ASSERT_TRUE(payload.find("rainfall_24h") == payload.end());
TEST_ASSERT_TRUE(payload.find("soil_moisture") == payload.end());
TEST_ASSERT_TRUE(payload.find("soil_temperature") == payload.end());
delete root;
}
@@ -0,0 +1,105 @@
#include "../test_helpers.h"
#include <memory>
// Helper function to test common packet fields and structure
void verify_text_message_packet_structure(const std::string &json, const char *expected_text)
{
TEST_ASSERT_TRUE(json.length() > 0);
// Use smart pointer for automatic memory management
std::unique_ptr<JSONValue> root(JSON::Parse(json.c_str()));
TEST_ASSERT_NOT_NULL(root.get());
TEST_ASSERT_TRUE(root->IsObject());
JSONObject jsonObj = root->AsObject();
// Check basic packet fields - use helper function to reduce duplication
auto check_field = [&](const char *field, uint32_t expected_value) {
auto it = jsonObj.find(field);
TEST_ASSERT_TRUE(it != jsonObj.end());
TEST_ASSERT_EQUAL(expected_value, (uint32_t)it->second->AsNumber());
};
check_field("from", 0x11223344);
check_field("to", 0x55667788);
check_field("id", 0x9999);
// Check message type
auto type_it = jsonObj.find("type");
TEST_ASSERT_TRUE(type_it != jsonObj.end());
TEST_ASSERT_EQUAL_STRING("text", type_it->second->AsString().c_str());
// Check payload
auto payload_it = jsonObj.find("payload");
TEST_ASSERT_TRUE(payload_it != jsonObj.end());
TEST_ASSERT_TRUE(payload_it->second->IsObject());
JSONObject payload = payload_it->second->AsObject();
auto text_it = payload.find("text");
TEST_ASSERT_TRUE(text_it != payload.end());
TEST_ASSERT_EQUAL_STRING(expected_text, text_it->second->AsString().c_str());
// No need for manual delete with smart pointer
}
// Test TEXT_MESSAGE_APP port
void test_text_message_serialization()
{
const char *test_text = "Hello Meshtastic!";
meshtastic_MeshPacket packet =
create_test_packet(meshtastic_PortNum_TEXT_MESSAGE_APP, reinterpret_cast<const uint8_t *>(test_text), strlen(test_text));
std::string json = MeshPacketSerializer::JsonSerialize(&packet, false);
verify_text_message_packet_structure(json, test_text);
}
// Test with nullptr to check robustness
void test_text_message_serialization_null()
{
meshtastic_MeshPacket packet = create_test_packet(meshtastic_PortNum_TEXT_MESSAGE_APP, nullptr, 0);
std::string json = MeshPacketSerializer::JsonSerialize(&packet, false);
verify_text_message_packet_structure(json, "");
}
// Test TEXT_MESSAGE_APP port with very long message (boundary testing)
void test_text_message_serialization_long_text()
{
// Test with actual message size limits
constexpr size_t MAX_MESSAGE_SIZE = 200; // Typical LoRa payload limit
std::string long_text(MAX_MESSAGE_SIZE, 'A');
meshtastic_MeshPacket packet = create_test_packet(meshtastic_PortNum_TEXT_MESSAGE_APP,
reinterpret_cast<const uint8_t *>(long_text.c_str()), long_text.length());
std::string json = MeshPacketSerializer::JsonSerialize(&packet, false);
verify_text_message_packet_structure(json, long_text.c_str());
}
// Test with message over size limit (should fail)
void test_text_message_serialization_oversized()
{
constexpr size_t OVERSIZED_MESSAGE = 250; // Over the limit
std::string oversized_text(OVERSIZED_MESSAGE, 'B');
meshtastic_MeshPacket packet = create_test_packet(
meshtastic_PortNum_TEXT_MESSAGE_APP, reinterpret_cast<const uint8_t *>(oversized_text.c_str()), oversized_text.length());
// Should fail or return empty/error
std::string json = MeshPacketSerializer::JsonSerialize(&packet, false);
// Should only verify first 234 characters for oversized messages
std::string expected_text = oversized_text.substr(0, 234);
verify_text_message_packet_structure(json, expected_text.c_str());
}
// Add test for malformed UTF-8 sequences
void test_text_message_serialization_invalid_utf8()
{
const uint8_t invalid_utf8[] = {0xFF, 0xFE, 0xFD, 0x00}; // Invalid UTF-8
meshtastic_MeshPacket packet =
create_test_packet(meshtastic_PortNum_TEXT_MESSAGE_APP, invalid_utf8, sizeof(invalid_utf8) - 1);
// Should not crash, may produce replacement characters
std::string json = MeshPacketSerializer::JsonSerialize(&packet, false);
TEST_ASSERT_TRUE(json.length() > 0);
}
@@ -0,0 +1,53 @@
#include "../test_helpers.h"
static size_t encode_waypoint(uint8_t *buffer, size_t buffer_size)
{
meshtastic_Waypoint waypoint = meshtastic_Waypoint_init_zero;
waypoint.id = 12345;
waypoint.latitude_i = 374208000;
waypoint.longitude_i = -1221981000;
waypoint.expire = 1609459200 + 3600; // 1 hour from now
strcpy(waypoint.name, "Test Point");
strcpy(waypoint.description, "Test waypoint description");
pb_ostream_t stream = pb_ostream_from_buffer(buffer, buffer_size);
pb_encode(&stream, &meshtastic_Waypoint_msg, &waypoint);
return stream.bytes_written;
}
// Test WAYPOINT_APP port
void test_waypoint_serialization()
{
uint8_t buffer[256];
size_t payload_size = encode_waypoint(buffer, sizeof(buffer));
meshtastic_MeshPacket packet = create_test_packet(meshtastic_PortNum_WAYPOINT_APP, buffer, payload_size);
std::string json = MeshPacketSerializer::JsonSerialize(&packet, false);
TEST_ASSERT_TRUE(json.length() > 0);
JSONValue *root = JSON::Parse(json.c_str());
TEST_ASSERT_NOT_NULL(root);
TEST_ASSERT_TRUE(root->IsObject());
JSONObject jsonObj = root->AsObject();
// Check message type
TEST_ASSERT_TRUE(jsonObj.find("type") != jsonObj.end());
TEST_ASSERT_EQUAL_STRING("waypoint", jsonObj["type"]->AsString().c_str());
// Check payload
TEST_ASSERT_TRUE(jsonObj.find("payload") != jsonObj.end());
TEST_ASSERT_TRUE(jsonObj["payload"]->IsObject());
JSONObject payload = jsonObj["payload"]->AsObject();
// Verify waypoint data
TEST_ASSERT_TRUE(payload.find("id") != payload.end());
TEST_ASSERT_EQUAL(12345, (int)payload["id"]->AsNumber());
TEST_ASSERT_TRUE(payload.find("name") != payload.end());
TEST_ASSERT_EQUAL_STRING("Test Point", payload["name"]->AsString().c_str());
delete root;
}
@@ -0,0 +1,49 @@
#pragma once
#include "serialization/JSON.h"
#include "serialization/MeshPacketSerializer.h"
#include <Arduino.h>
#include <meshtastic/mesh.pb.h>
#include <meshtastic/mqtt.pb.h>
#include <meshtastic/telemetry.pb.h>
#include <pb_decode.h>
#include <pb_encode.h>
#include <unity.h>
// Helper function to create a test packet with the given port and payload
static meshtastic_MeshPacket create_test_packet(meshtastic_PortNum port, const uint8_t *payload, size_t payload_size,
int payload_variant = meshtastic_MeshPacket_decoded_tag)
{
meshtastic_MeshPacket packet = meshtastic_MeshPacket_init_zero;
packet.id = 0x9999;
packet.from = 0x11223344;
packet.to = 0x55667788;
packet.channel = 0;
packet.hop_limit = 3;
packet.want_ack = false;
packet.priority = meshtastic_MeshPacket_Priority_UNSET;
packet.rx_time = 1609459200;
packet.rx_snr = 10.5f;
packet.hop_start = 3;
packet.rx_rssi = -85;
packet.delayed = meshtastic_MeshPacket_Delayed_NO_DELAY;
// Set decoded variant
packet.which_payload_variant = payload_variant;
packet.decoded.portnum = port;
if (payload_variant == meshtastic_MeshPacket_encrypted_tag && payload) {
packet.encrypted.size = payload_size;
memcpy(packet.encrypted.bytes, payload, packet.encrypted.size);
}
memcpy(packet.decoded.payload.bytes, payload, payload_size);
packet.decoded.payload.size = payload_size;
packet.decoded.want_response = false;
packet.decoded.dest = 0x55667788;
packet.decoded.source = 0x11223344;
packet.decoded.request_id = 0;
packet.decoded.reply_id = 0;
packet.decoded.emoji = 0;
return packet;
}
@@ -0,0 +1,61 @@
#include "test_helpers.h"
#include <Arduino.h>
#include <unity.h>
// Forward declarations for test functions
void test_text_message_serialization();
void test_text_message_serialization_null();
void test_text_message_serialization_long_text();
void test_text_message_serialization_oversized();
void test_text_message_serialization_invalid_utf8();
void test_position_serialization();
void test_nodeinfo_serialization();
void test_waypoint_serialization();
void test_telemetry_device_metrics_serialization();
void test_telemetry_environment_metrics_serialization();
void test_telemetry_environment_metrics_comprehensive();
void test_telemetry_environment_metrics_missing_fields();
void test_telemetry_environment_metrics_complete_coverage();
void test_telemetry_environment_metrics_unset_fields();
void test_encrypted_packet_serialization();
void test_empty_encrypted_packet();
void setup()
{
UNITY_BEGIN();
// Text message tests
RUN_TEST(test_text_message_serialization);
RUN_TEST(test_text_message_serialization_null);
RUN_TEST(test_text_message_serialization_long_text);
RUN_TEST(test_text_message_serialization_oversized);
RUN_TEST(test_text_message_serialization_invalid_utf8);
// Position tests
RUN_TEST(test_position_serialization);
// Nodeinfo tests
RUN_TEST(test_nodeinfo_serialization);
// Waypoint tests
RUN_TEST(test_waypoint_serialization);
// Telemetry tests
RUN_TEST(test_telemetry_device_metrics_serialization);
RUN_TEST(test_telemetry_environment_metrics_serialization);
RUN_TEST(test_telemetry_environment_metrics_comprehensive);
RUN_TEST(test_telemetry_environment_metrics_missing_fields);
RUN_TEST(test_telemetry_environment_metrics_complete_coverage);
RUN_TEST(test_telemetry_environment_metrics_unset_fields);
// Encrypted packet test
RUN_TEST(test_encrypted_packet_serialization);
RUN_TEST(test_empty_encrypted_packet);
UNITY_END();
}
void loop()
{
delay(1000);
}
+933
View File
@@ -0,0 +1,933 @@
#include "DebugConfiguration.h"
#include "TestUtil.h"
#include <unity.h>
#ifdef ARCH_PORTDUINO
#include "mesh/CryptoEngine.h"
#include "mesh/Default.h"
#include "mesh/MeshService.h"
#include "mesh/NodeDB.h"
#include "mesh/Router.h"
#include "modules/RoutingModule.h"
#include "mqtt/MQTT.h"
#include "mqtt/ServiceEnvelope.h"
#include <PubSubClient.h>
#include <WiFiClient.h>
#include <arpa/inet.h>
#include <algorithm>
#include <list>
#include <optional>
#include <set>
#include <sstream>
#include <string>
#include <string_view>
#include <utility>
#include <variant>
#if defined(UNIT_TEST)
#define IS_RUNNING_TESTS 1
#else
#define IS_RUNNING_TESTS 0
#endif
namespace
{
// Minimal router needed to receive messages from MQTT.
class MockRouter : public Router
{
public:
~MockRouter()
{
// cryptLock is created in the constructor for Router.
delete cryptLock;
cryptLock = NULL;
}
void enqueueReceivedMessage(meshtastic_MeshPacket *p) override
{
packets_.emplace_back(*p);
packetPool.release(p);
}
std::list<meshtastic_MeshPacket> packets_; // Packets received by the Router.
};
// Minimal MeshService needed to receive messages from MQTT for testing PKI channel.
class MockMeshService : public MeshService
{
public:
void sendMqttMessageToClientProxy(meshtastic_MqttClientProxyMessage *m) override
{
messages_.emplace_back(*m);
releaseMqttClientProxyMessageToPool(m);
}
void sendClientNotification(meshtastic_ClientNotification *n) override
{
notifications_.emplace_back(*n);
releaseClientNotificationToPool(n);
}
std::list<meshtastic_MqttClientProxyMessage> messages_; // Messages received from the MeshService.
std::list<meshtastic_ClientNotification> notifications_; // Notifications received from the MeshService.
};
// Minimal NodeDB needed to return values from getMeshNode.
class MockNodeDB : public NodeDB
{
public:
meshtastic_NodeInfoLite *getMeshNode(NodeNum n) override { return &emptyNode; }
meshtastic_NodeInfoLite emptyNode = {};
};
// Minimal RoutingModule needed to return values from sendAckNak.
class MockRoutingModule : public RoutingModule
{
public:
void sendAckNak(meshtastic_Routing_Error err, NodeNum to, PacketId idFrom, ChannelIndex chIndex, uint8_t hopLimit = 0,
bool ackWantsAck = false) override
{
ackNacks_.emplace_back(err, to, idFrom, chIndex, hopLimit);
}
std::list<std::tuple<meshtastic_Routing_Error, NodeNum, PacketId, ChannelIndex, uint8_t>>
ackNacks_; // ackNacks received by the RoutingModule.
};
// A WiFi client used by the MQTT::PubSubClient. Implements a minimal pub/sub server.
// There isn't an easy way to mock PubSubClient due to it not having virtual methods, so we mock using
// the WiFiClinet that PubSubClient uses.
class MockPubSubServer : public WiFiClient
{
public:
static constexpr char kTextTopic[] = "TextTopic";
uint8_t connected() override { return connected_; }
void flush() override {}
IPAddress remoteIP() const override { return IPAddress(htonl(ipAddress_)); }
void stop() override { connected_ = false; }
int connect(IPAddress ip, uint16_t port) override
{
port_ = port;
if (refuseConnection_)
return 0;
connected_ = true;
return 1;
}
int connect(const char *host, uint16_t port) override
{
host_ = host;
port_ = port;
if (refuseConnection_)
return 0;
connected_ = true;
return 1;
}
int available() override
{
if (buffer_.empty())
return 0;
return buffer_.front().size();
}
int read() override
{
assert(available());
std::string &front = buffer_.front();
char ch = front[0];
front = front.substr(1, front.size());
if (front.empty())
buffer_.pop_front();
return ch;
}
size_t write(uint8_t data) override { return write(&data, 1); }
size_t write(const uint8_t *buf, size_t size) override
{
command_ += std::string(reinterpret_cast<const char *>(buf), size);
if (command_.size() < 2)
return size;
const int len = (uint8_t)command_[1] + 2;
if (command_.size() < len)
return size;
handleCommand(command_[0], command_.substr(2, len));
command_ = command_.substr(len, command_.size());
return size;
}
// The pub/sub "server".
// https://public.dhe.ibm.com/software/dw/webservices/ws-mqtt/MQTT_V3.1_Protocol_Specific.pdf
void handleCommand(uint8_t header, std::string_view message)
{
switch (header & 0xf0) {
case MQTTCONNECT:
LOG_DEBUG("MQTTCONNECT");
buffer_.push_back(std::string("\x20\x02\x00\x00", 4));
break;
case MQTTSUBSCRIBE: {
LOG_DEBUG("MQTTSUBSCRIBE");
assert(message.size() >= 5);
message.remove_prefix(2); // skip messageId
while (message.size() >= 3) {
const uint16_t topicSize = ((uint8_t)message[0]) << 8 | (uint8_t)message[1];
message.remove_prefix(2);
assert(message.size() >= topicSize + 1);
std::string topic(message.data(), topicSize);
message.remove_prefix(topicSize + 1);
LOG_DEBUG("Subscribed to topic: %s", topic.c_str());
subscriptions_.insert(std::move(topic));
}
break;
}
case MQTTPINGREQ:
LOG_DEBUG("MQTTPINGREQ");
buffer_.push_back(std::string("\xd0\x00", 2));
break;
case MQTTPUBLISH: {
LOG_DEBUG("MQTTPUBLISH");
assert(message.size() >= 3);
const uint16_t topicSize = ((uint8_t)message[0]) << 8 | (uint8_t)message[1];
message.remove_prefix(2);
assert(message.size() >= topicSize);
std::string topic(message.data(), topicSize);
message.remove_prefix(topicSize);
if (topic == kTextTopic) {
published_.emplace_back(std::move(topic), std::string(message.data(), message.size()));
} else {
published_.emplace_back(
std::move(topic), DecodedServiceEnvelope(reinterpret_cast<const uint8_t *>(message.data()), message.size()));
}
break;
}
}
}
bool connected_ = false;
bool refuseConnection_ = false; // Simulate a failed connection.
uint32_t ipAddress_ = 0x01010101; // IP address of the MQTT server.
std::string host_; // Requested host.
uint16_t port_; // Requested port.
std::list<std::string> buffer_; // Buffer of messages for the pubSub client to receive.
std::string command_; // Current command received from the pubSub client.
std::set<std::string> subscriptions_; // Topics that the pubSub client has subscribed to.
std::list<std::pair<std::string, std::variant<std::string,
DecodedServiceEnvelope>>>
published_; // Messages published from the pubSub client. Each list element is a pair containing the topic name and either
// a text message (if from the kTextTopic topic) or a DecodedServiceEnvelope.
};
// Instances of our mocks.
class MQTTUnitTest;
MQTTUnitTest *unitTest;
MockPubSubServer *pubsub;
MockRoutingModule *mockRoutingModule;
MockMeshService *mockMeshService;
MockRouter *mockRouter;
// Keep running the loop until either conditionMet returns true or 4 seconds elapse.
// Returns true if conditionMet returns true, returns false on timeout.
bool loopUntil(std::function<bool()> conditionMet)
{
long start = millis();
while (start + 4000 > millis()) {
long delayMsec = concurrency::mainController.runOrDelay();
if (conditionMet())
return true;
concurrency::mainDelay.delay(std::min(delayMsec, 5L));
}
return false;
}
// Used to access protected/private members of MQTT for unit testing.
class MQTTUnitTest : public MQTT
{
public:
MQTTUnitTest() : MQTT(std::make_unique<MockPubSubServer>())
{
pubsub = reinterpret_cast<MockPubSubServer *>(mqttClient.get());
}
~MQTTUnitTest()
{
// Needed because WiFiClient does not have a virtual destructor.
mqttClient.release();
delete pubsub;
}
using MQTT::isValidConfig;
using MQTT::reconnect;
int queueSize() { return mqttQueue.numUsed(); }
void reportToMap(std::optional<uint32_t> precision = std::nullopt)
{
if (precision.has_value())
map_position_precision = precision.value();
map_publish_interval_msecs = 0;
perhapsReportToMap();
}
void publish(const meshtastic_MeshPacket *p, std::string gateway = "!87654321", std::string channel = "test")
{
std::stringstream topic;
topic << "msh/2/e/" << channel << "/!" << gateway;
const meshtastic_ServiceEnvelope env = {.packet = const_cast<meshtastic_MeshPacket *>(p),
.channel_id = const_cast<char *>(channel.c_str()),
.gateway_id = const_cast<char *>(gateway.c_str())};
uint8_t bytes[256];
size_t numBytes = pb_encode_to_bytes(bytes, sizeof(bytes), &meshtastic_ServiceEnvelope_msg, &env);
mqttCallback(const_cast<char *>(topic.str().c_str()), bytes, numBytes);
}
static void restart()
{
if (mqtt != NULL) {
delete mqtt;
mqtt = unitTest = NULL;
}
mqtt = unitTest = new MQTTUnitTest();
mqtt->start();
if (!moduleConfig.mqtt.enabled || moduleConfig.mqtt.proxy_to_client_enabled || *moduleConfig.mqtt.root) {
loopUntil([] { return true; }); // Loop once
return;
}
// Wait for MQTT to subscribe to all topics.
TEST_ASSERT_TRUE(loopUntil(
[] { return pubsub->subscriptions_.count("msh/2/e/test/+") && pubsub->subscriptions_.count("msh/2/e/PKI/+"); }));
}
PubSubClient &getPubSub() { return pubSub; }
};
// Packets used in unit tests.
const meshtastic_MeshPacket decoded = {
.from = 1,
.to = 2,
.which_payload_variant = meshtastic_MeshPacket_decoded_tag,
.decoded = {.portnum = meshtastic_PortNum_TEXT_MESSAGE_APP, .has_bitfield = true, .bitfield = BITFIELD_OK_TO_MQTT_MASK},
.id = 4,
};
const meshtastic_MeshPacket encrypted = {
.from = 1,
.to = 2,
.which_payload_variant = meshtastic_MeshPacket_encrypted_tag,
.encrypted = {.size = 0},
.id = 3,
};
} // namespace
// Initialize mocks and configuration before running each test.
void setUp(void)
{
moduleConfig.mqtt =
meshtastic_ModuleConfig_MQTTConfig{.enabled = true, .map_reporting_enabled = true, .has_map_report_settings = true};
moduleConfig.mqtt.map_report_settings = meshtastic_ModuleConfig_MapReportSettings{
.publish_interval_secs = 0, .position_precision = 14, .should_report_location = true};
channelFile.channels[0] = meshtastic_Channel{
.index = 0,
.has_settings = true,
.settings = {.name = "test", .uplink_enabled = true, .downlink_enabled = true},
.role = meshtastic_Channel_Role_PRIMARY,
};
channelFile.channels_count = 1;
owner = meshtastic_User{.id = "!12345678"};
myNodeInfo = meshtastic_MyNodeInfo{.my_node_num = 0x12345678}; // Match the expected gateway ID in topic
localPosition =
meshtastic_Position{.has_latitude_i = true, .latitude_i = 700000000, .has_longitude_i = true, .longitude_i = 300000000};
router = mockRouter = new MockRouter();
service = mockMeshService = new MockMeshService();
routingModule = mockRoutingModule = new MockRoutingModule();
MQTTUnitTest::restart();
}
// Deinitialize all objects created in setUp.
void tearDown(void)
{
delete unitTest;
mqtt = unitTest = NULL;
delete mockRoutingModule;
routingModule = mockRoutingModule = NULL;
delete mockMeshService;
service = mockMeshService = NULL;
delete mockRouter;
router = mockRouter = NULL;
}
// Test that the decoded MeshPacket is published when encryption_enabled = false.
void test_sendDirectlyConnectedDecoded(void)
{
mqtt->onSend(encrypted, decoded, 0);
TEST_ASSERT_EQUAL(1, pubsub->published_.size());
const auto &[topic, payload] = pubsub->published_.front();
const DecodedServiceEnvelope &env = std::get<DecodedServiceEnvelope>(payload);
TEST_ASSERT_EQUAL_STRING("msh/2/e/test/!12345678", topic.c_str());
TEST_ASSERT_TRUE(env.validDecode);
TEST_ASSERT_EQUAL(decoded.id, env.packet->id);
}
// Test that the encrypted MeshPacket is published when encryption_enabled = true.
void test_sendDirectlyConnectedEncrypted(void)
{
moduleConfig.mqtt.encryption_enabled = true;
mqtt->onSend(encrypted, decoded, 0);
TEST_ASSERT_EQUAL(1, pubsub->published_.size());
const auto &[topic, payload] = pubsub->published_.front();
const DecodedServiceEnvelope &env = std::get<DecodedServiceEnvelope>(payload);
TEST_ASSERT_EQUAL_STRING("msh/2/e/test/!12345678", topic.c_str());
TEST_ASSERT_TRUE(env.validDecode);
TEST_ASSERT_EQUAL(encrypted.id, env.packet->id);
}
// Verify that the decoded MeshPacket is proxied through the MeshService when encryption_enabled = false.
void test_proxyToMeshServiceDecoded(void)
{
moduleConfig.mqtt.proxy_to_client_enabled = true;
MQTTUnitTest::restart();
mqtt->onSend(encrypted, decoded, 0);
TEST_ASSERT_EQUAL(1, mockMeshService->messages_.size());
const meshtastic_MqttClientProxyMessage &message = mockMeshService->messages_.front();
TEST_ASSERT_EQUAL_STRING("msh/2/e/test/!12345678", message.topic);
TEST_ASSERT_EQUAL(meshtastic_MqttClientProxyMessage_data_tag, message.which_payload_variant);
const DecodedServiceEnvelope env(message.payload_variant.data.bytes, message.payload_variant.data.size);
TEST_ASSERT_TRUE(env.validDecode);
TEST_ASSERT_EQUAL(decoded.id, env.packet->id);
}
// Verify that the encrypted MeshPacket is proxied through the MeshService when encryption_enabled = true.
void test_proxyToMeshServiceEncrypted(void)
{
moduleConfig.mqtt.proxy_to_client_enabled = true;
moduleConfig.mqtt.encryption_enabled = true;
MQTTUnitTest::restart();
mqtt->onSend(encrypted, decoded, 0);
TEST_ASSERT_EQUAL(1, mockMeshService->messages_.size());
const meshtastic_MqttClientProxyMessage &message = mockMeshService->messages_.front();
TEST_ASSERT_EQUAL_STRING("msh/2/e/test/!12345678", message.topic);
TEST_ASSERT_EQUAL(meshtastic_MqttClientProxyMessage_data_tag, message.which_payload_variant);
const DecodedServiceEnvelope env(message.payload_variant.data.bytes, message.payload_variant.data.size);
TEST_ASSERT_TRUE(env.validDecode);
TEST_ASSERT_EQUAL(encrypted.id, env.packet->id);
}
// A packet without the OK to MQTT bit set should not be published to a public server.
void test_dontMqttMeOnPublicServer(void)
{
meshtastic_MeshPacket p = decoded;
p.decoded.bitfield = 0;
p.decoded.has_bitfield = 0;
mqtt->onSend(encrypted, p, 0);
TEST_ASSERT_TRUE(pubsub->published_.empty());
}
// A packet without the OK to MQTT bit set should be published to a private server.
void test_okToMqttOnPrivateServer(void)
{
// Cause a disconnect.
pubsub->connected_ = false;
pubsub->refuseConnection_ = true;
TEST_ASSERT_TRUE(loopUntil([] { return !unitTest->getPubSub().connected(); }));
// Use 127.0.0.1 for the server's IP.
pubsub->ipAddress_ = 0x7f000001;
// Reconnect.
pubsub->refuseConnection_ = false;
TEST_ASSERT_TRUE(loopUntil([] { return unitTest->getPubSub().connected(); }));
// Send the same packet as test_dontMqttMeOnPublicServer.
meshtastic_MeshPacket p = decoded;
p.decoded.bitfield = 0;
p.decoded.has_bitfield = 0;
mqtt->onSend(encrypted, p, 0);
TEST_ASSERT_EQUAL(1, pubsub->published_.size());
}
// Range tests messages are not uplinked to the default server.
void test_noRangeTestAppOnDefaultServer(void)
{
meshtastic_MeshPacket p = decoded;
p.decoded.portnum = meshtastic_PortNum_RANGE_TEST_APP;
mqtt->onSend(encrypted, p, 0);
TEST_ASSERT_TRUE(pubsub->published_.empty());
}
// Detection sensor messages are not uplinked to the default server.
void test_noDetectionSensorAppOnDefaultServer(void)
{
meshtastic_MeshPacket p = decoded;
p.decoded.portnum = meshtastic_PortNum_DETECTION_SENSOR_APP;
mqtt->onSend(encrypted, p, 0);
TEST_ASSERT_TRUE(pubsub->published_.empty());
}
// Test that a MeshPacket is queued while the MQTT server is disconnected.
void test_sendQueued(void)
{
// Cause a disconnect.
pubsub->connected_ = false;
pubsub->refuseConnection_ = true;
TEST_ASSERT_TRUE(loopUntil([] { return !unitTest->getPubSub().connected(); }));
// Send while disconnected.
mqtt->onSend(encrypted, decoded, 0);
TEST_ASSERT_EQUAL(1, unitTest->queueSize());
TEST_ASSERT_TRUE(pubsub->published_.empty());
TEST_ASSERT_FALSE(unitTest->getPubSub().connected());
// Allow reconnect to happen. Expect to see the packet published now.
pubsub->refuseConnection_ = false;
TEST_ASSERT_TRUE(loopUntil([] { return !pubsub->published_.empty(); }));
TEST_ASSERT_EQUAL(0, unitTest->queueSize());
const auto &[topic, payload] = pubsub->published_.front();
const DecodedServiceEnvelope &env = std::get<DecodedServiceEnvelope>(payload);
TEST_ASSERT_EQUAL_STRING("msh/2/e/test/!12345678", topic.c_str());
TEST_ASSERT_TRUE(env.validDecode);
TEST_ASSERT_EQUAL(decoded.id, env.packet->id);
}
// Verify reconnecting with the proxy enabled does not reconnect to a MQTT server.
void test_reconnectProxyDoesNotReconnectMqtt(void)
{
moduleConfig.mqtt.proxy_to_client_enabled = true;
MQTTUnitTest::restart();
unitTest->reconnect();
TEST_ASSERT_FALSE(pubsub->connected_);
}
// Test receiving an empty MeshPacket on a subscribed topic.
void test_receiveEmptyMeshPacket(void)
{
unitTest->publish(NULL);
TEST_ASSERT_TRUE(mockRouter->packets_.empty());
TEST_ASSERT_TRUE(mockRoutingModule->ackNacks_.empty());
}
// Test receiving a decoded MeshPacket on a subscribed topic.
void test_receiveDecodedProto(void)
{
unitTest->publish(&decoded);
TEST_ASSERT_EQUAL(1, mockRouter->packets_.size());
const meshtastic_MeshPacket &p = mockRouter->packets_.front();
TEST_ASSERT_EQUAL(decoded.id, p.id);
TEST_ASSERT_TRUE(p.via_mqtt);
}
// Test receiving a decoded MeshPacket from the phone proxy.
void test_receiveDecodedProtoFromProxy(void)
{
const meshtastic_ServiceEnvelope env = {
.packet = const_cast<meshtastic_MeshPacket *>(&decoded), .channel_id = "test", .gateway_id = "!87654321"};
meshtastic_MqttClientProxyMessage message = meshtastic_MqttClientProxyMessage_init_default;
strcat(message.topic, "msh/2/e/test/!87654321");
message.which_payload_variant = meshtastic_MqttClientProxyMessage_data_tag;
message.payload_variant.data.size = pb_encode_to_bytes(
message.payload_variant.data.bytes, sizeof(message.payload_variant.data.bytes), &meshtastic_ServiceEnvelope_msg, &env);
mqtt->onClientProxyReceive(message);
TEST_ASSERT_EQUAL(1, mockRouter->packets_.size());
const meshtastic_MeshPacket &p = mockRouter->packets_.front();
TEST_ASSERT_EQUAL(decoded.id, p.id);
TEST_ASSERT_TRUE(p.via_mqtt);
}
// Properly handles the case where the received message is empty.
void test_receiveEmptyDataFromProxy(void)
{
meshtastic_MqttClientProxyMessage message = meshtastic_MqttClientProxyMessage_init_default;
message.which_payload_variant = meshtastic_MqttClientProxyMessage_data_tag;
mqtt->onClientProxyReceive(message);
TEST_ASSERT_TRUE(mockRouter->packets_.empty());
}
// Packets should be ignored if downlink is not enabled.
void test_receiveWithoutChannelDownlink(void)
{
channelFile.channels[0].settings.downlink_enabled = false;
unitTest->publish(&decoded);
TEST_ASSERT_TRUE(mockRouter->packets_.empty());
}
// Test receiving an encrypted MeshPacket on the PKI topic.
void test_receiveEncryptedPKITopicToUs(void)
{
meshtastic_MeshPacket e = encrypted;
e.to = myNodeInfo.my_node_num;
unitTest->publish(&e, "!87654321", "PKI");
TEST_ASSERT_EQUAL(1, mockRouter->packets_.size());
const meshtastic_MeshPacket &p = mockRouter->packets_.front();
TEST_ASSERT_EQUAL(encrypted.id, p.id);
TEST_ASSERT_TRUE(p.via_mqtt);
}
// Should ignore messages published to MQTT by this gateway.
void test_receiveIgnoresOwnPublishedMessages(void)
{
unitTest->publish(&decoded, nodeDB->getNodeId().c_str());
TEST_ASSERT_TRUE(mockRouter->packets_.empty());
TEST_ASSERT_TRUE(mockRoutingModule->ackNacks_.empty());
}
// Considers receiving one of our packets an acknowledgement of it being sent.
void test_receiveAcksOwnSentMessages(void)
{
meshtastic_MeshPacket p = decoded;
p.from = myNodeInfo.my_node_num;
unitTest->publish(&p, nodeDB->getNodeId().c_str());
// FIXME: Better assertion for this test
// TEST_ASSERT_TRUE(mockRouter->packets_.empty());
// TEST_ASSERT_EQUAL(1, mockRoutingModule->ackNacks_.size());
// const auto &[err, to, idFrom, chIndex, hopLimit] = mockRoutingModule->ackNacks_.front();
// TEST_ASSERT_EQUAL(meshtastic_Routing_Error_NONE, err);
// TEST_ASSERT_EQUAL(myNodeInfo.my_node_num, to);
// TEST_ASSERT_EQUAL(p.id, idFrom);
}
// Should ignore our own messages from MQTT that were heard by other nodes.
void test_receiveIgnoresSentMessagesFromOthers(void)
{
meshtastic_MeshPacket p = decoded;
p.from = myNodeInfo.my_node_num;
unitTest->publish(&p);
TEST_ASSERT_TRUE(mockRouter->packets_.empty());
TEST_ASSERT_TRUE(mockRoutingModule->ackNacks_.empty());
}
// Decoded MQTT messages should be ignored when encryption is enabled.
void test_receiveIgnoresDecodedWhenEncryptionEnabled(void)
{
moduleConfig.mqtt.encryption_enabled = true;
unitTest->publish(&decoded);
TEST_ASSERT_TRUE(mockRouter->packets_.empty());
}
// Non-encrypted messages for the Admin App should be ignored.
void test_receiveIgnoresDecodedAdminApp(void)
{
meshtastic_MeshPacket p = decoded;
p.decoded.portnum = meshtastic_PortNum_ADMIN_APP;
unitTest->publish(&p);
TEST_ASSERT_TRUE(mockRouter->packets_.empty());
}
// Only the same fields that are transmitted over LoRa should be set in MQTT messages.
void test_receiveIgnoresUnexpectedFields(void)
{
meshtastic_MeshPacket input = decoded;
input.rx_snr = 10;
input.rx_rssi = 20;
unitTest->publish(&input);
TEST_ASSERT_EQUAL(1, mockRouter->packets_.size());
const meshtastic_MeshPacket &p = mockRouter->packets_.front();
TEST_ASSERT_EQUAL(0, p.rx_snr);
TEST_ASSERT_EQUAL(0, p.rx_rssi);
}
// Messages with an invalid hop_limit are ignored.
void test_receiveIgnoresInvalidHopLimit(void)
{
meshtastic_MeshPacket p = decoded;
p.hop_limit = 10;
unitTest->publish(&p);
TEST_ASSERT_TRUE(mockRouter->packets_.empty());
}
// Publishing to a text channel.
void test_publishTextMessageDirect(void)
{
TEST_ASSERT_TRUE(mqtt->publish(MockPubSubServer::kTextTopic, "payload", 0));
TEST_ASSERT_EQUAL(1, pubsub->published_.size());
const auto &[topic, payload] = pubsub->published_.front();
TEST_ASSERT_EQUAL_STRING("payload", std::get<std::string>(payload).c_str());
}
// Publishing to a text channel via the MQTT client proxy.
void test_publishTextMessageWithProxy(void)
{
moduleConfig.mqtt.proxy_to_client_enabled = true;
TEST_ASSERT_TRUE(mqtt->publish(MockPubSubServer::kTextTopic, "payload", 0));
TEST_ASSERT_EQUAL(1, mockMeshService->messages_.size());
const meshtastic_MqttClientProxyMessage &message = mockMeshService->messages_.front();
TEST_ASSERT_EQUAL_STRING(MockPubSubServer::kTextTopic, message.topic);
TEST_ASSERT_EQUAL(meshtastic_MqttClientProxyMessage_text_tag, message.which_payload_variant);
TEST_ASSERT_EQUAL_STRING("payload", message.payload_variant.text);
}
// Helper method to verify the expected latitude/longitude was received.
void verifyLatLong(const DecodedServiceEnvelope &env, uint32_t latitude, uint32_t longitude)
{
TEST_ASSERT_TRUE(env.validDecode);
const meshtastic_MeshPacket &p = *env.packet;
TEST_ASSERT_EQUAL(NODENUM_BROADCAST, p.to);
TEST_ASSERT_EQUAL(meshtastic_MeshPacket_decoded_tag, p.which_payload_variant);
TEST_ASSERT_EQUAL(meshtastic_PortNum_MAP_REPORT_APP, p.decoded.portnum);
meshtastic_MapReport mapReport;
TEST_ASSERT_TRUE(
pb_decode_from_bytes(p.decoded.payload.bytes, p.decoded.payload.size, &meshtastic_MapReport_msg, &mapReport));
TEST_ASSERT_EQUAL(latitude, mapReport.latitude_i);
TEST_ASSERT_EQUAL(longitude, mapReport.longitude_i);
}
// Map reporting defaults to an imprecise location.
void test_reportToMapDefaultImprecise(void)
{
unitTest->reportToMap();
TEST_ASSERT_EQUAL(1, pubsub->published_.size());
const auto &[topic, payload] = pubsub->published_.front();
TEST_ASSERT_EQUAL_STRING("msh/2/map/", topic.c_str());
}
// Location is sent over the phone proxy.
void test_reportToMapImpreciseProxied(void)
{
moduleConfig.mqtt.proxy_to_client_enabled = true;
MQTTUnitTest::restart();
unitTest->reportToMap(/*precision=*/14);
TEST_ASSERT_EQUAL(1, mockMeshService->messages_.size());
const meshtastic_MqttClientProxyMessage &message = mockMeshService->messages_.front();
TEST_ASSERT_EQUAL_STRING("msh/2/map/", message.topic);
TEST_ASSERT_EQUAL(meshtastic_MqttClientProxyMessage_data_tag, message.which_payload_variant);
const DecodedServiceEnvelope env(message.payload_variant.data.bytes, message.payload_variant.data.size);
}
// isUsingDefaultServer returns true when using the default server.
void test_usingDefaultServer(void)
{
TEST_ASSERT_TRUE(mqtt->isUsingDefaultServer());
}
// isUsingDefaultServer returns true when using the default server and a port.
void test_usingDefaultServerWithPort(void)
{
std::string server = default_mqtt_address;
server += ":1883";
strcpy(moduleConfig.mqtt.address, server.c_str());
MQTTUnitTest::restart();
TEST_ASSERT_TRUE(mqtt->isUsingDefaultServer());
}
// isUsingDefaultServer returns true when using the default server and invalid port.
void test_usingDefaultServerWithInvalidPort(void)
{
std::string server = default_mqtt_address;
server += ":invalid";
strcpy(moduleConfig.mqtt.address, server.c_str());
MQTTUnitTest::restart();
TEST_ASSERT_TRUE(mqtt->isUsingDefaultServer());
}
// isUsingDefaultServer returns false when not using the default server.
void test_usingCustomServer(void)
{
strcpy(moduleConfig.mqtt.address, "custom");
MQTTUnitTest::restart();
TEST_ASSERT_FALSE(mqtt->isUsingDefaultServer());
}
// Test that isEnabled returns true the MQTT module is enabled.
void test_enabled(void)
{
TEST_ASSERT_TRUE(mqtt->isEnabled());
}
// Test that isEnabled returns false the MQTT module not enabled.
void test_disabled(void)
{
moduleConfig.mqtt.enabled = false;
MQTTUnitTest::restart();
TEST_ASSERT_FALSE(mqtt->isEnabled());
}
// Subscriptions contain the moduleConfig.mqtt.root prefix.
void test_customMqttRoot(void)
{
strcpy(moduleConfig.mqtt.root, "custom");
MQTTUnitTest::restart();
TEST_ASSERT_TRUE(loopUntil(
[] { return pubsub->subscriptions_.count("custom/2/e/test/+") && pubsub->subscriptions_.count("custom/2/e/PKI/+"); }));
}
// Empty configuration is valid.
void test_configEmptyIsValid(void)
{
meshtastic_ModuleConfig_MQTTConfig config = {};
TEST_ASSERT_TRUE(MQTT::isValidConfig(config));
}
// Empty 'enabled' configuration is valid.
void test_configEnabledEmptyIsValid(void)
{
meshtastic_ModuleConfig_MQTTConfig config = {.enabled = true};
MockPubSubServer client;
TEST_ASSERT_TRUE(MQTTUnitTest::isValidConfig(config, &client));
TEST_ASSERT_TRUE(client.connected_);
TEST_ASSERT_EQUAL_STRING(default_mqtt_address, client.host_.c_str());
TEST_ASSERT_EQUAL(1883, client.port_);
}
// Configuration with the default server is valid.
void test_configWithDefaultServer(void)
{
meshtastic_ModuleConfig_MQTTConfig config = {.address = default_mqtt_address};
TEST_ASSERT_TRUE(MQTT::isValidConfig(config));
}
// Configuration with the default server and port 8888 is invalid.
void test_configWithDefaultServerAndInvalidPort(void)
{
meshtastic_ModuleConfig_MQTTConfig config = {.address = default_mqtt_address ":8888"};
TEST_ASSERT_FALSE(MQTT::isValidConfig(config));
}
// isValidConfig connects to a custom host and port.
void test_configCustomHostAndPort(void)
{
meshtastic_ModuleConfig_MQTTConfig config = {.enabled = true, .address = "server:1234"};
MockPubSubServer client;
TEST_ASSERT_TRUE(MQTTUnitTest::isValidConfig(config, &client));
TEST_ASSERT_TRUE(client.connected_);
TEST_ASSERT_EQUAL_STRING("server", client.host_.c_str());
TEST_ASSERT_EQUAL(1234, client.port_);
}
// isValidConfig returns false if a connection cannot be established.
void test_configWithConnectionFailure(void)
{
meshtastic_ModuleConfig_MQTTConfig config = {.enabled = true, .address = "server"};
MockPubSubServer client;
client.refuseConnection_ = true;
TEST_ASSERT_FALSE(MQTTUnitTest::isValidConfig(config, &client));
}
// isValidConfig returns true when tls_enabled is supported, or false otherwise.
void test_configWithTLSEnabled(void)
{
meshtastic_ModuleConfig_MQTTConfig config = {.enabled = true, .address = "server", .tls_enabled = true};
MockPubSubServer client;
#if MQTT_SUPPORTS_TLS
TEST_ASSERT_TRUE(MQTTUnitTest::isValidConfig(config, &client));
#else
TEST_ASSERT_FALSE(MQTTUnitTest::isValidConfig(config, &client));
#endif
}
void setup()
{
initializeTestEnvironment();
const std::unique_ptr<MockNodeDB> mockNodeDB(new MockNodeDB());
nodeDB = mockNodeDB.get();
UNITY_BEGIN();
RUN_TEST(test_sendDirectlyConnectedDecoded);
RUN_TEST(test_sendDirectlyConnectedEncrypted);
RUN_TEST(test_proxyToMeshServiceDecoded);
RUN_TEST(test_proxyToMeshServiceEncrypted);
RUN_TEST(test_dontMqttMeOnPublicServer);
RUN_TEST(test_okToMqttOnPrivateServer);
RUN_TEST(test_noRangeTestAppOnDefaultServer);
RUN_TEST(test_noDetectionSensorAppOnDefaultServer);
RUN_TEST(test_sendQueued);
RUN_TEST(test_reconnectProxyDoesNotReconnectMqtt);
RUN_TEST(test_receiveEmptyMeshPacket);
RUN_TEST(test_receiveDecodedProto);
RUN_TEST(test_receiveDecodedProtoFromProxy);
RUN_TEST(test_receiveEmptyDataFromProxy);
RUN_TEST(test_receiveWithoutChannelDownlink);
RUN_TEST(test_receiveEncryptedPKITopicToUs);
RUN_TEST(test_receiveIgnoresOwnPublishedMessages);
RUN_TEST(test_receiveAcksOwnSentMessages);
RUN_TEST(test_receiveIgnoresSentMessagesFromOthers);
RUN_TEST(test_receiveIgnoresDecodedWhenEncryptionEnabled);
RUN_TEST(test_receiveIgnoresDecodedAdminApp);
RUN_TEST(test_receiveIgnoresUnexpectedFields);
RUN_TEST(test_receiveIgnoresInvalidHopLimit);
RUN_TEST(test_publishTextMessageDirect);
RUN_TEST(test_publishTextMessageWithProxy);
RUN_TEST(test_reportToMapDefaultImprecise);
RUN_TEST(test_reportToMapImpreciseProxied);
RUN_TEST(test_usingDefaultServer);
RUN_TEST(test_usingDefaultServerWithPort);
RUN_TEST(test_usingDefaultServerWithInvalidPort);
RUN_TEST(test_usingCustomServer);
RUN_TEST(test_enabled);
RUN_TEST(test_disabled);
RUN_TEST(test_customMqttRoot);
RUN_TEST(test_configEmptyIsValid);
RUN_TEST(test_configEnabledEmptyIsValid);
RUN_TEST(test_configWithDefaultServer);
RUN_TEST(test_configWithDefaultServerAndInvalidPort);
RUN_TEST(test_configCustomHostAndPort);
RUN_TEST(test_configWithConnectionFailure);
RUN_TEST(test_configWithTLSEnabled);
exit(UNITY_END());
}
#else
void setup()
{
initializeTestEnvironment();
LOG_WARN("This test requires the ARCH_PORTDUINO variant of WiFiClient");
UNITY_BEGIN();
UNITY_END();
}
#endif
void loop() {}
+100
View File
@@ -0,0 +1,100 @@
#include "MeshRadio.h"
#include "RadioInterface.h"
#include "TestUtil.h"
#include <unity.h>
#include "meshtastic/config.pb.h"
static void test_bwCodeToKHz_specialMappings()
{
TEST_ASSERT_FLOAT_WITHIN(0.0001f, 31.25f, bwCodeToKHz(31));
TEST_ASSERT_FLOAT_WITHIN(0.0001f, 62.5f, bwCodeToKHz(62));
TEST_ASSERT_FLOAT_WITHIN(0.0001f, 203.125f, bwCodeToKHz(200));
TEST_ASSERT_FLOAT_WITHIN(0.0001f, 406.25f, bwCodeToKHz(400));
TEST_ASSERT_FLOAT_WITHIN(0.0001f, 812.5f, bwCodeToKHz(800));
TEST_ASSERT_FLOAT_WITHIN(0.0001f, 1625.0f, bwCodeToKHz(1600));
}
static void test_bwCodeToKHz_passthrough()
{
TEST_ASSERT_FLOAT_WITHIN(0.0001f, 125.0f, bwCodeToKHz(125));
TEST_ASSERT_FLOAT_WITHIN(0.0001f, 250.0f, bwCodeToKHz(250));
}
static void test_bootstrapLoRaConfigFromPreset_noopWhenUsePresetFalse()
{
meshtastic_Config_LoRaConfig cfg = meshtastic_Config_LoRaConfig_init_zero;
cfg.use_preset = false;
cfg.region = meshtastic_Config_LoRaConfig_RegionCode_US;
cfg.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_FAST;
cfg.bandwidth = 123;
cfg.spread_factor = 8;
RadioInterface::bootstrapLoRaConfigFromPreset(cfg);
TEST_ASSERT_EQUAL_UINT16(123, cfg.bandwidth);
TEST_ASSERT_EQUAL_UINT32(8, cfg.spread_factor);
TEST_ASSERT_EQUAL(meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_FAST, cfg.modem_preset);
}
static void test_bootstrapLoRaConfigFromPreset_setsDerivedFields_nonWideRegion()
{
meshtastic_Config_LoRaConfig cfg = meshtastic_Config_LoRaConfig_init_zero;
cfg.use_preset = true;
cfg.region = meshtastic_Config_LoRaConfig_RegionCode_US;
cfg.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_FAST;
RadioInterface::bootstrapLoRaConfigFromPreset(cfg);
TEST_ASSERT_EQUAL_UINT16(250, cfg.bandwidth);
TEST_ASSERT_EQUAL_UINT32(9, cfg.spread_factor);
}
static void test_bootstrapLoRaConfigFromPreset_setsDerivedFields_wideRegion()
{
meshtastic_Config_LoRaConfig cfg = meshtastic_Config_LoRaConfig_init_zero;
cfg.use_preset = true;
cfg.region = meshtastic_Config_LoRaConfig_RegionCode_LORA_24;
cfg.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_FAST;
RadioInterface::bootstrapLoRaConfigFromPreset(cfg);
TEST_ASSERT_EQUAL_UINT16(800, cfg.bandwidth);
TEST_ASSERT_EQUAL_UINT32(9, cfg.spread_factor);
}
static void test_bootstrapLoRaConfigFromPreset_fallsBackIfBandwidthExceedsRegionSpan()
{
meshtastic_Config_LoRaConfig cfg = meshtastic_Config_LoRaConfig_init_zero;
cfg.use_preset = true;
cfg.region = meshtastic_Config_LoRaConfig_RegionCode_EU_868;
cfg.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_SHORT_TURBO;
RadioInterface::bootstrapLoRaConfigFromPreset(cfg);
TEST_ASSERT_EQUAL(meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST, cfg.modem_preset);
TEST_ASSERT_EQUAL_UINT16(250, cfg.bandwidth);
TEST_ASSERT_EQUAL_UINT32(11, cfg.spread_factor);
}
void setUp(void) {}
void tearDown(void) {}
void setup()
{
delay(10);
delay(2000);
initializeTestEnvironment();
UNITY_BEGIN();
RUN_TEST(test_bwCodeToKHz_specialMappings);
RUN_TEST(test_bwCodeToKHz_passthrough);
RUN_TEST(test_bootstrapLoRaConfigFromPreset_noopWhenUsePresetFalse);
RUN_TEST(test_bootstrapLoRaConfigFromPreset_setsDerivedFields_nonWideRegion);
RUN_TEST(test_bootstrapLoRaConfigFromPreset_setsDerivedFields_wideRegion);
RUN_TEST(test_bootstrapLoRaConfigFromPreset_fallsBackIfBandwidthExceedsRegionSpan);
exit(UNITY_END());
}
void loop() {}
@@ -0,0 +1,156 @@
#include "DebugConfiguration.h"
#include "TestUtil.h"
#include <unity.h>
#ifdef ARCH_PORTDUINO
#include "configuration.h"
#if defined(UNIT_TEST)
#define IS_RUNNING_TESTS 1
#else
#define IS_RUNNING_TESTS 0
#endif
#if (defined(ARCH_ESP32) || defined(ARCH_NRF52) || defined(ARCH_RP2040)) && !defined(CONFIG_IDF_TARGET_ESP32S2) && \
!defined(CONFIG_IDF_TARGET_ESP32C3)
#include "modules/SerialModule.h"
#endif
#if (defined(ARCH_ESP32) || defined(ARCH_NRF52) || defined(ARCH_RP2040)) && !defined(CONFIG_IDF_TARGET_ESP32S2) && \
!defined(CONFIG_IDF_TARGET_ESP32C3)
// Test that empty configuration is valid.
void test_serialConfigEmptyIsValid(void)
{
meshtastic_ModuleConfig_SerialConfig config = {};
TEST_ASSERT_TRUE(SerialModule::isValidConfig(config));
}
// Test that basic enabled configuration is valid.
void test_serialConfigEnabledIsValid(void)
{
meshtastic_ModuleConfig_SerialConfig config = {.enabled = true};
TEST_ASSERT_TRUE(SerialModule::isValidConfig(config));
}
// Test that configuration with override_console_serial_port and NMEA mode is valid.
void test_serialConfigWithOverrideConsoleNmeaModeIsValid(void)
{
meshtastic_ModuleConfig_SerialConfig config = {
.enabled = true, .override_console_serial_port = true, .mode = meshtastic_ModuleConfig_SerialConfig_Serial_Mode_NMEA};
TEST_ASSERT_TRUE(SerialModule::isValidConfig(config));
}
// Test that configuration with override_console_serial_port and CalTopo mode is valid.
void test_serialConfigWithOverrideConsoleCalTopoModeIsValid(void)
{
meshtastic_ModuleConfig_SerialConfig config = {
.enabled = true, .override_console_serial_port = true, .mode = meshtastic_ModuleConfig_SerialConfig_Serial_Mode_CALTOPO};
TEST_ASSERT_TRUE(SerialModule::isValidConfig(config));
}
// Test that configuration with override_console_serial_port and DEFAULT mode is invalid.
void test_serialConfigWithOverrideConsoleDefaultModeIsInvalid(void)
{
meshtastic_ModuleConfig_SerialConfig config = {
.enabled = true, .override_console_serial_port = true, .mode = meshtastic_ModuleConfig_SerialConfig_Serial_Mode_DEFAULT};
TEST_ASSERT_FALSE(SerialModule::isValidConfig(config));
}
// Test that configuration with override_console_serial_port and SIMPLE mode is invalid.
void test_serialConfigWithOverrideConsoleSimpleModeIsInvalid(void)
{
meshtastic_ModuleConfig_SerialConfig config = {
.enabled = true, .override_console_serial_port = true, .mode = meshtastic_ModuleConfig_SerialConfig_Serial_Mode_SIMPLE};
TEST_ASSERT_FALSE(SerialModule::isValidConfig(config));
}
// Test that configuration with override_console_serial_port and TEXTMSG mode is invalid.
void test_serialConfigWithOverrideConsoleTextMsgModeIsInvalid(void)
{
meshtastic_ModuleConfig_SerialConfig config = {
.enabled = true, .override_console_serial_port = true, .mode = meshtastic_ModuleConfig_SerialConfig_Serial_Mode_TEXTMSG};
TEST_ASSERT_FALSE(SerialModule::isValidConfig(config));
}
// Test that configuration with override_console_serial_port and PROTO mode is invalid.
void test_serialConfigWithOverrideConsoleProtoModeIsInvalid(void)
{
meshtastic_ModuleConfig_SerialConfig config = {
.enabled = true, .override_console_serial_port = true, .mode = meshtastic_ModuleConfig_SerialConfig_Serial_Mode_PROTO};
TEST_ASSERT_FALSE(SerialModule::isValidConfig(config));
}
// Test that various modes work without override_console_serial_port.
void test_serialConfigVariousModesWithoutOverrideAreValid(void)
{
meshtastic_ModuleConfig_SerialConfig config = {.enabled = true, .override_console_serial_port = false};
// Test DEFAULT mode
config.mode = meshtastic_ModuleConfig_SerialConfig_Serial_Mode_DEFAULT;
TEST_ASSERT_TRUE(SerialModule::isValidConfig(config));
// Test SIMPLE mode
config.mode = meshtastic_ModuleConfig_SerialConfig_Serial_Mode_SIMPLE;
TEST_ASSERT_TRUE(SerialModule::isValidConfig(config));
// Test TEXTMSG mode
config.mode = meshtastic_ModuleConfig_SerialConfig_Serial_Mode_TEXTMSG;
TEST_ASSERT_TRUE(SerialModule::isValidConfig(config));
// Test PROTO mode
config.mode = meshtastic_ModuleConfig_SerialConfig_Serial_Mode_PROTO;
TEST_ASSERT_TRUE(SerialModule::isValidConfig(config));
// Test NMEA mode
config.mode = meshtastic_ModuleConfig_SerialConfig_Serial_Mode_NMEA;
TEST_ASSERT_TRUE(SerialModule::isValidConfig(config));
// Test CALTOPO mode
config.mode = meshtastic_ModuleConfig_SerialConfig_Serial_Mode_CALTOPO;
TEST_ASSERT_TRUE(SerialModule::isValidConfig(config));
}
#endif // Architecture check
void setup()
{
initializeTestEnvironment();
#if (defined(ARCH_ESP32) || defined(ARCH_NRF52) || defined(ARCH_RP2040)) && !defined(CONFIG_IDF_TARGET_ESP32S2) && \
!defined(CONFIG_IDF_TARGET_ESP32C3)
UNITY_BEGIN();
RUN_TEST(test_serialConfigEmptyIsValid);
RUN_TEST(test_serialConfigEnabledIsValid);
RUN_TEST(test_serialConfigWithOverrideConsoleNmeaModeIsValid);
RUN_TEST(test_serialConfigWithOverrideConsoleCalTopoModeIsValid);
RUN_TEST(test_serialConfigWithOverrideConsoleDefaultModeIsInvalid);
RUN_TEST(test_serialConfigWithOverrideConsoleSimpleModeIsInvalid);
RUN_TEST(test_serialConfigWithOverrideConsoleTextMsgModeIsInvalid);
RUN_TEST(test_serialConfigWithOverrideConsoleProtoModeIsInvalid);
RUN_TEST(test_serialConfigVariousModesWithoutOverrideAreValid);
exit(UNITY_END());
#else
LOG_WARN("This test requires ESP32, NRF52, or RP2040 architecture");
UNITY_BEGIN();
UNITY_END();
#endif
}
#else
void setup()
{
initializeTestEnvironment();
LOG_WARN("This test requires the ARCH_PORTDUINO variant");
UNITY_BEGIN();
UNITY_END();
}
#endif
void loop() {}
@@ -0,0 +1,230 @@
#include "TestUtil.h"
#include "TransmitHistory.h"
#include <Throttle.h>
#include <unity.h>
// Reset the singleton between tests
static void resetTransmitHistory()
{
if (transmitHistory) {
delete transmitHistory;
transmitHistory = nullptr;
}
transmitHistory = TransmitHistory::getInstance();
}
void setUp(void)
{
resetTransmitHistory();
}
void tearDown(void) {}
static void test_setLastSentToMesh_stores_millis()
{
transmitHistory->setLastSentToMesh(meshtastic_PortNum_NODEINFO_APP);
uint32_t result = transmitHistory->getLastSentToMeshMillis(meshtastic_PortNum_NODEINFO_APP);
TEST_ASSERT_NOT_EQUAL(0, result);
// The stored millis value should be very close to current millis()
uint32_t diff = millis() - result;
TEST_ASSERT_LESS_OR_EQUAL(100, diff); // Within 100ms
}
static void test_set_overwrites_previous_value()
{
transmitHistory->setLastSentToMesh(meshtastic_PortNum_TELEMETRY_APP);
uint32_t first = transmitHistory->getLastSentToMeshMillis(meshtastic_PortNum_TELEMETRY_APP);
testDelay(50);
transmitHistory->setLastSentToMesh(meshtastic_PortNum_TELEMETRY_APP);
uint32_t second = transmitHistory->getLastSentToMeshMillis(meshtastic_PortNum_TELEMETRY_APP);
// The second value should be newer (larger millis)
TEST_ASSERT_GREATER_THAN(first, second);
}
// --- Throttle integration ---
static void test_throttle_blocks_within_interval()
{
transmitHistory->setLastSentToMesh(meshtastic_PortNum_NODEINFO_APP);
uint32_t lastMs = transmitHistory->getLastSentToMeshMillis(meshtastic_PortNum_NODEINFO_APP);
// Should be within a 10-minute interval (just set it)
bool withinInterval = Throttle::isWithinTimespanMs(lastMs, 10 * 60 * 1000);
TEST_ASSERT_TRUE(withinInterval);
}
static void test_throttle_allows_after_interval()
{
// Unknown key returns 0 — throttle should NOT block
uint32_t lastMs = transmitHistory->getLastSentToMeshMillis(meshtastic_PortNum_NODEINFO_APP);
TEST_ASSERT_EQUAL_UINT32(0, lastMs);
// When lastMs == 0, the module check `lastMs == 0 || !isWithinTimespan` allows sending
bool shouldSend = (lastMs == 0) || !Throttle::isWithinTimespanMs(lastMs, 10 * 60 * 1000);
TEST_ASSERT_TRUE(shouldSend);
}
static void test_throttle_blocks_after_set_then_zero_does_not()
{
// Set it — now throttle should block
transmitHistory->setLastSentToMesh(meshtastic_PortNum_TELEMETRY_APP);
uint32_t lastMs = transmitHistory->getLastSentToMeshMillis(meshtastic_PortNum_TELEMETRY_APP);
bool shouldSend = (lastMs == 0) || !Throttle::isWithinTimespanMs(lastMs, 60 * 60 * 1000);
TEST_ASSERT_FALSE(shouldSend); // Should be blocked (within 1hr interval)
// Different key — should allow
uint32_t otherMs = transmitHistory->getLastSentToMeshMillis(meshtastic_PortNum_POSITION_APP);
bool otherShouldSend = (otherMs == 0) || !Throttle::isWithinTimespanMs(otherMs, 60 * 60 * 1000);
TEST_ASSERT_TRUE(otherShouldSend);
}
// --- Multiple keys ---
static void test_multiple_keys_stored_independently()
{
transmitHistory->setLastSentToMesh(meshtastic_PortNum_NODEINFO_APP);
uint32_t nodeInfoInitial = transmitHistory->getLastSentToMeshMillis(meshtastic_PortNum_NODEINFO_APP);
testDelay(20);
transmitHistory->setLastSentToMesh(meshtastic_PortNum_POSITION_APP);
uint32_t positionInitial = transmitHistory->getLastSentToMeshMillis(meshtastic_PortNum_POSITION_APP);
testDelay(20);
transmitHistory->setLastSentToMesh(meshtastic_PortNum_TELEMETRY_APP);
uint32_t nodeInfo = transmitHistory->getLastSentToMeshMillis(meshtastic_PortNum_NODEINFO_APP);
uint32_t position = transmitHistory->getLastSentToMeshMillis(meshtastic_PortNum_POSITION_APP);
uint32_t telemetry = transmitHistory->getLastSentToMeshMillis(meshtastic_PortNum_TELEMETRY_APP);
// All should be non-zero
TEST_ASSERT_NOT_EQUAL(0, nodeInfo);
TEST_ASSERT_NOT_EQUAL(0, position);
TEST_ASSERT_NOT_EQUAL(0, telemetry);
// Updating other keys should not overwrite earlier key timestamps
TEST_ASSERT_EQUAL_UINT32(nodeInfoInitial, nodeInfo);
TEST_ASSERT_EQUAL_UINT32(positionInitial, position);
}
// --- Singleton ---
static void test_getInstance_returns_same_instance()
{
TransmitHistory *a = TransmitHistory::getInstance();
TransmitHistory *b = TransmitHistory::getInstance();
TEST_ASSERT_EQUAL_PTR(a, b);
}
static void test_getInstance_creates_global()
{
if (transmitHistory) {
delete transmitHistory;
transmitHistory = nullptr;
}
TEST_ASSERT_NULL(transmitHistory);
TransmitHistory::getInstance();
TEST_ASSERT_NOT_NULL(transmitHistory);
}
// --- Persistence round-trip (loadFromDisk / saveToDisk) ---
static void test_save_and_load_round_trip()
{
// Set some values
transmitHistory->setLastSentToMesh(meshtastic_PortNum_NODEINFO_APP);
testDelay(10);
transmitHistory->setLastSentToMesh(meshtastic_PortNum_POSITION_APP);
uint32_t nodeInfoEpoch = transmitHistory->getLastSentToMeshEpoch(meshtastic_PortNum_NODEINFO_APP);
uint32_t positionEpoch = transmitHistory->getLastSentToMeshEpoch(meshtastic_PortNum_POSITION_APP);
// Force save
transmitHistory->saveToDisk();
// Reset and reload
delete transmitHistory;
transmitHistory = nullptr;
transmitHistory = TransmitHistory::getInstance();
transmitHistory->loadFromDisk();
// Epoch values should be restored (if RTC was available when set)
uint32_t restoredNodeInfo = transmitHistory->getLastSentToMeshEpoch(meshtastic_PortNum_NODEINFO_APP);
uint32_t restoredPosition = transmitHistory->getLastSentToMeshEpoch(meshtastic_PortNum_POSITION_APP);
TEST_ASSERT_EQUAL_UINT32(nodeInfoEpoch, restoredNodeInfo);
TEST_ASSERT_EQUAL_UINT32(positionEpoch, restoredPosition);
// After loadFromDisk, millis should be seeded (non-zero) for stored entries
uint32_t restoredMillis = transmitHistory->getLastSentToMeshMillis(meshtastic_PortNum_NODEINFO_APP);
if (restoredNodeInfo > 0) {
// If epoch was stored, millis should be seeded from load
TEST_ASSERT_NOT_EQUAL(0, restoredMillis);
}
}
// --- Boot without RTC scenario ---
static void test_load_seeds_millis_even_without_rtc()
{
// This tests the critical crash-reboot scenario:
// After loadFromDisk(), even if getTime() returns 0 (no RTC),
// lastMillis should be seeded so throttle blocks immediate re-broadcast.
transmitHistory->setLastSentToMesh(meshtastic_PortNum_NODEINFO_APP);
transmitHistory->saveToDisk();
// Simulate reboot: destroy and recreate
delete transmitHistory;
transmitHistory = nullptr;
transmitHistory = TransmitHistory::getInstance();
transmitHistory->loadFromDisk();
// The key insight: after load, getLastSentToMeshMillis should return non-zero
// because loadFromDisk seeds lastMillis[key] = millis() for every loaded entry.
// This ensures throttle works even without RTC.
uint32_t result = transmitHistory->getLastSentToMeshMillis(meshtastic_PortNum_NODEINFO_APP);
uint32_t epoch = transmitHistory->getLastSentToMeshEpoch(meshtastic_PortNum_NODEINFO_APP);
if (epoch > 0) {
// Data was persisted — millis must be seeded
TEST_ASSERT_NOT_EQUAL(0, result);
// And it should cause throttle to block (treating as "just sent")
bool withinInterval = Throttle::isWithinTimespanMs(result, 10 * 60 * 1000);
TEST_ASSERT_TRUE(withinInterval);
}
// If epoch == 0, RTC wasn't available — no data was saved, so nothing to restore.
// This is expected on platforms without RTC during the very first boot.
}
void setup()
{
initializeTestEnvironment();
UNITY_BEGIN();
RUN_TEST(test_setLastSentToMesh_stores_millis);
RUN_TEST(test_set_overwrites_previous_value);
RUN_TEST(test_throttle_blocks_within_interval);
RUN_TEST(test_throttle_allows_after_interval);
RUN_TEST(test_throttle_blocks_after_set_then_zero_does_not);
RUN_TEST(test_multiple_keys_stored_independently);
// Singleton
RUN_TEST(test_getInstance_returns_same_instance);
RUN_TEST(test_getInstance_creates_global);
// Persistence
RUN_TEST(test_save_and_load_round_trip);
RUN_TEST(test_load_seeds_millis_even_without_rtc);
exit(UNITY_END());
}
void loop() {}