From a5b8c6598d60a3ff53780e73b191eac1306a9ea3 Mon Sep 17 00:00:00 2001 From: Michael Lynch Date: Fri, 24 Apr 2026 20:04:01 +0000 Subject: [PATCH 01/17] Add native advert data round-trip tests --- platformio.ini | 1 + test/mocks/Arduino.h | 6 ++++ test/test_utils/test_advert_data.cpp | 52 ++++++++++++++++++++++++++++ 3 files changed, 59 insertions(+) create mode 100644 test/mocks/Arduino.h create mode 100644 test/test_utils/test_advert_data.cpp diff --git a/platformio.ini b/platformio.ini index 5c6072b405..7ff42af8d3 100644 --- a/platformio.ini +++ b/platformio.ini @@ -164,5 +164,6 @@ test_build_src = yes build_src_filter = -<*> +<../src/Utils.cpp> + +<../src/helpers/AdvertDataHelpers.cpp> lib_deps = google/googletest @ 1.17.0 diff --git a/test/mocks/Arduino.h b/test/mocks/Arduino.h new file mode 100644 index 0000000000..d265a16e6a --- /dev/null +++ b/test/mocks/Arduino.h @@ -0,0 +1,6 @@ +#pragma once + +#include +#include +#include +#include diff --git a/test/test_utils/test_advert_data.cpp b/test/test_utils/test_advert_data.cpp new file mode 100644 index 0000000000..ef71983601 --- /dev/null +++ b/test/test_utils/test_advert_data.cpp @@ -0,0 +1,52 @@ +#include + +#include "helpers/AdvertDataHelpers.h" + +namespace { + +TEST(AdvertData, RoundTripsNameOnly) { + uint8_t app_data[MAX_ADVERT_DATA_SIZE] = {}; + AdvertDataBuilder builder(ADV_TYPE_CHAT, "alice"); + + uint8_t len = builder.encodeTo(app_data); + AdvertDataParser parser(app_data, len); + + ASSERT_TRUE(parser.isValid()); + EXPECT_EQ(ADV_TYPE_CHAT, parser.getType()); + EXPECT_TRUE(parser.hasName()); + EXPECT_STREQ("alice", parser.getName()); + EXPECT_FALSE(parser.hasLatLon()); +} + +TEST(AdvertData, RoundTripsNameAndCoordinates) { + uint8_t app_data[MAX_ADVERT_DATA_SIZE] = {}; + AdvertDataBuilder builder(ADV_TYPE_REPEATER, "node", 37.7749, -122.4194); + + uint8_t len = builder.encodeTo(app_data); + AdvertDataParser parser(app_data, len); + + ASSERT_TRUE(parser.isValid()); + EXPECT_EQ(ADV_TYPE_REPEATER, parser.getType()); + EXPECT_TRUE(parser.hasName()); + EXPECT_STREQ("node", parser.getName()); + EXPECT_TRUE(parser.hasLatLon()); + EXPECT_EQ(37774900, parser.getIntLat()); + EXPECT_EQ(-122419400, parser.getIntLon()); + EXPECT_DOUBLE_EQ(37.7749, parser.getLat()); + EXPECT_DOUBLE_EQ(-122.4194, parser.getLon()); +} + +TEST(AdvertData, RoundTripsCoordinateExtremes) { + uint8_t app_data[MAX_ADVERT_DATA_SIZE] = {}; + AdvertDataBuilder builder(ADV_TYPE_SENSOR, "edge", -90.0, 180.0); + + uint8_t len = builder.encodeTo(app_data); + AdvertDataParser parser(app_data, len); + + ASSERT_TRUE(parser.isValid()); + EXPECT_TRUE(parser.hasLatLon()); + EXPECT_EQ(-90000000, parser.getIntLat()); + EXPECT_EQ(180000000, parser.getIntLon()); +} + +} // namespace From 987f345ab63b4345443e42566c45c91a29771d51 Mon Sep 17 00:00:00 2001 From: Michael Lynch Date: Fri, 24 Apr 2026 23:29:57 +0000 Subject: [PATCH 02/17] Add invalid GPS advert data tests --- test/test_utils/test_advert_data.cpp | 40 ++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/test/test_utils/test_advert_data.cpp b/test/test_utils/test_advert_data.cpp index ef71983601..9645a516eb 100644 --- a/test/test_utils/test_advert_data.cpp +++ b/test/test_utils/test_advert_data.cpp @@ -49,4 +49,44 @@ TEST(AdvertData, RoundTripsCoordinateExtremes) { EXPECT_EQ(180000000, parser.getIntLon()); } +TEST(AdvertData, RejectsLongitudeOutsideValidRange) { + uint8_t app_data[MAX_ADVERT_DATA_SIZE] = {}; + AdvertDataBuilder builder(ADV_TYPE_CHAT, "node", 37.7749, 180.000001); + + uint8_t len = builder.encodeTo(app_data); + AdvertDataParser parser(app_data, len); + + EXPECT_FALSE(parser.isValid()); +} + +TEST(AdvertData, RejectsLongitudeBelowValidRange) { + uint8_t app_data[MAX_ADVERT_DATA_SIZE] = {}; + AdvertDataBuilder builder(ADV_TYPE_CHAT, "node", 37.7749, -180.000001); + + uint8_t len = builder.encodeTo(app_data); + AdvertDataParser parser(app_data, len); + + EXPECT_FALSE(parser.isValid()); +} + +TEST(AdvertData, RejectsLatitudeOutsideValidRange) { + uint8_t app_data[MAX_ADVERT_DATA_SIZE] = {}; + AdvertDataBuilder builder(ADV_TYPE_CHAT, "node", 90.000001, -122.4194); + + uint8_t len = builder.encodeTo(app_data); + AdvertDataParser parser(app_data, len); + + EXPECT_FALSE(parser.isValid()); +} + +TEST(AdvertData, RejectsLatitudeBelowValidRange) { + uint8_t app_data[MAX_ADVERT_DATA_SIZE] = {}; + AdvertDataBuilder builder(ADV_TYPE_CHAT, "node", -90.000001, -122.4194); + + uint8_t len = builder.encodeTo(app_data); + AdvertDataParser parser(app_data, len); + + EXPECT_FALSE(parser.isValid()); +} + } // namespace From a3600d94a4e6c24d0f7ac11b23e0bf1273597928 Mon Sep 17 00:00:00 2001 From: Michael Lynch Date: Fri, 24 Apr 2026 23:42:36 +0000 Subject: [PATCH 03/17] Rewrite advert data tests with raw payloads --- src/helpers/AdvertDataHelpers.cpp | 5 +- test/test_utils/test_advert_data.cpp | 124 ++++++++++++++++++++++----- 2 files changed, 106 insertions(+), 23 deletions(-) diff --git a/src/helpers/AdvertDataHelpers.cpp b/src/helpers/AdvertDataHelpers.cpp index 0e05620ec2..0802cac952 100644 --- a/src/helpers/AdvertDataHelpers.cpp +++ b/src/helpers/AdvertDataHelpers.cpp @@ -37,6 +37,9 @@ if (_flags & ADV_LATLON_MASK) { memcpy(&_lat, &app_data[i], 4); i += 4; memcpy(&_lon, &app_data[i], 4); i += 4; + if (_lat < -90000000 || _lat > 90000000 || _lon < -180000000 || _lon > 180000000) { + return; + } } if (_flags & ADV_FEAT1_MASK) { memcpy(&_extra1, &app_data[i], 2); i += 2; @@ -84,4 +87,4 @@ void AdvertTimeHelper::formatRelativeTimeDiff(char dest[], int32_t seconds_from_ } } } -} \ No newline at end of file +} diff --git a/test/test_utils/test_advert_data.cpp b/test/test_utils/test_advert_data.cpp index 9645a516eb..0de8e92aa4 100644 --- a/test/test_utils/test_advert_data.cpp +++ b/test/test_utils/test_advert_data.cpp @@ -1,15 +1,47 @@ +#include +#include + #include #include "helpers/AdvertDataHelpers.h" namespace { +void WriteU8(uint8_t* dest, size_t* offset, uint8_t value) { + dest[(*offset)++] = value; +} + +void WriteI32Le(uint8_t* dest, size_t* offset, int32_t value) { + const uint32_t raw = static_cast(value); + dest[(*offset)++] = static_cast(raw & 0xFF); + dest[(*offset)++] = static_cast((raw >> 8) & 0xFF); + dest[(*offset)++] = static_cast((raw >> 16) & 0xFF); + dest[(*offset)++] = static_cast((raw >> 24) & 0xFF); +} + +void WriteBytes(uint8_t* dest, size_t* offset, const uint8_t* bytes, size_t length) { + for (size_t i = 0; i < length; ++i) { + dest[*offset + i] = bytes[i]; + } + *offset += length; +} + +template +void WriteStringLiteral(uint8_t* dest, size_t* offset, const char (&value)[N]) { + static_assert(N > 0, "string literal must include a null terminator"); + WriteBytes(dest, offset, reinterpret_cast(value), N - 1); +} + TEST(AdvertData, RoundTripsNameOnly) { uint8_t app_data[MAX_ADVERT_DATA_SIZE] = {}; - AdvertDataBuilder builder(ADV_TYPE_CHAT, "alice"); + size_t offset = 0; + + // flags/type byte: chat advert with a trailing name field. + WriteU8(app_data, &offset, ADV_TYPE_CHAT | ADV_NAME_MASK); + // name field: raw bytes for "alice", consuming the rest of app_data. + WriteStringLiteral(app_data, &offset, "alice"); - uint8_t len = builder.encodeTo(app_data); - AdvertDataParser parser(app_data, len); + AdvertDataParser parser(app_data, offset); ASSERT_TRUE(parser.isValid()); EXPECT_EQ(ADV_TYPE_CHAT, parser.getType()); @@ -20,10 +52,18 @@ TEST(AdvertData, RoundTripsNameOnly) { TEST(AdvertData, RoundTripsNameAndCoordinates) { uint8_t app_data[MAX_ADVERT_DATA_SIZE] = {}; - AdvertDataBuilder builder(ADV_TYPE_REPEATER, "node", 37.7749, -122.4194); + size_t offset = 0; - uint8_t len = builder.encodeTo(app_data); - AdvertDataParser parser(app_data, len); + // flags/type byte: repeater advert with lat/lon followed by a name. + WriteU8(app_data, &offset, ADV_TYPE_REPEATER | ADV_LATLON_MASK | ADV_NAME_MASK); + // latitude field: signed little-endian microdegrees for 37.7749. + WriteI32Le(app_data, &offset, 37774900); + // longitude field: signed little-endian microdegrees for -122.4194. + WriteI32Le(app_data, &offset, -122419400); + // name field: raw bytes for "node" after the coordinate fields. + WriteStringLiteral(app_data, &offset, "node"); + + AdvertDataParser parser(app_data, offset); ASSERT_TRUE(parser.isValid()); EXPECT_EQ(ADV_TYPE_REPEATER, parser.getType()); @@ -38,10 +78,18 @@ TEST(AdvertData, RoundTripsNameAndCoordinates) { TEST(AdvertData, RoundTripsCoordinateExtremes) { uint8_t app_data[MAX_ADVERT_DATA_SIZE] = {}; - AdvertDataBuilder builder(ADV_TYPE_SENSOR, "edge", -90.0, 180.0); + size_t offset = 0; + + // flags/type byte: sensor advert with both location fields and a name. + WriteU8(app_data, &offset, ADV_TYPE_SENSOR | ADV_LATLON_MASK | ADV_NAME_MASK); + // latitude field: minimum supported latitude, -90.000000 degrees. + WriteI32Le(app_data, &offset, -90000000); + // longitude field: maximum supported longitude, 180.000000 degrees. + WriteI32Le(app_data, &offset, 180000000); + // name field: raw bytes for "edge". + WriteStringLiteral(app_data, &offset, "edge"); - uint8_t len = builder.encodeTo(app_data); - AdvertDataParser parser(app_data, len); + AdvertDataParser parser(app_data, offset); ASSERT_TRUE(parser.isValid()); EXPECT_TRUE(parser.hasLatLon()); @@ -51,40 +99,72 @@ TEST(AdvertData, RoundTripsCoordinateExtremes) { TEST(AdvertData, RejectsLongitudeOutsideValidRange) { uint8_t app_data[MAX_ADVERT_DATA_SIZE] = {}; - AdvertDataBuilder builder(ADV_TYPE_CHAT, "node", 37.7749, 180.000001); + size_t offset = 0; - uint8_t len = builder.encodeTo(app_data); - AdvertDataParser parser(app_data, len); + // flags/type byte: chat advert with location and name fields present. + WriteU8(app_data, &offset, ADV_TYPE_CHAT | ADV_LATLON_MASK | ADV_NAME_MASK); + // latitude field: valid latitude so the failure comes from longitude. + WriteI32Le(app_data, &offset, 37774900); + // longitude field: one microdegree above +180.0, which is invalid. + WriteI32Le(app_data, &offset, 180000001); + // name field: parser should reject before the trailing name matters. + WriteStringLiteral(app_data, &offset, "node"); + + AdvertDataParser parser(app_data, offset); EXPECT_FALSE(parser.isValid()); } TEST(AdvertData, RejectsLongitudeBelowValidRange) { uint8_t app_data[MAX_ADVERT_DATA_SIZE] = {}; - AdvertDataBuilder builder(ADV_TYPE_CHAT, "node", 37.7749, -180.000001); + size_t offset = 0; + + // flags/type byte: chat advert with location and name fields present. + WriteU8(app_data, &offset, ADV_TYPE_CHAT | ADV_LATLON_MASK | ADV_NAME_MASK); + // latitude field: valid latitude so the failure comes from longitude. + WriteI32Le(app_data, &offset, 37774900); + // longitude field: one microdegree below -180.0, which is invalid. + WriteI32Le(app_data, &offset, -180000001); + // name field: included to keep the payload shape consistent. + WriteStringLiteral(app_data, &offset, "node"); - uint8_t len = builder.encodeTo(app_data); - AdvertDataParser parser(app_data, len); + AdvertDataParser parser(app_data, offset); EXPECT_FALSE(parser.isValid()); } TEST(AdvertData, RejectsLatitudeOutsideValidRange) { uint8_t app_data[MAX_ADVERT_DATA_SIZE] = {}; - AdvertDataBuilder builder(ADV_TYPE_CHAT, "node", 90.000001, -122.4194); + size_t offset = 0; - uint8_t len = builder.encodeTo(app_data); - AdvertDataParser parser(app_data, len); + // flags/type byte: chat advert with location and name fields present. + WriteU8(app_data, &offset, ADV_TYPE_CHAT | ADV_LATLON_MASK | ADV_NAME_MASK); + // latitude field: one microdegree above +90.0, which is invalid. + WriteI32Le(app_data, &offset, 90000001); + // longitude field: valid longitude so the failure comes from latitude. + WriteI32Le(app_data, &offset, -122419400); + // name field: included to keep the payload shape consistent. + WriteStringLiteral(app_data, &offset, "node"); + + AdvertDataParser parser(app_data, offset); EXPECT_FALSE(parser.isValid()); } TEST(AdvertData, RejectsLatitudeBelowValidRange) { uint8_t app_data[MAX_ADVERT_DATA_SIZE] = {}; - AdvertDataBuilder builder(ADV_TYPE_CHAT, "node", -90.000001, -122.4194); - - uint8_t len = builder.encodeTo(app_data); - AdvertDataParser parser(app_data, len); + size_t offset = 0; + + // flags/type byte: chat advert with location and name fields present. + WriteU8(app_data, &offset, ADV_TYPE_CHAT | ADV_LATLON_MASK | ADV_NAME_MASK); + // latitude field: one microdegree below -90.0, which is invalid. + WriteI32Le(app_data, &offset, -90000001); + // longitude field: valid longitude so the failure comes from latitude. + WriteI32Le(app_data, &offset, -122419400); + // name field: included to keep the payload shape consistent. + WriteStringLiteral(app_data, &offset, "node"); + + AdvertDataParser parser(app_data, offset); EXPECT_FALSE(parser.isValid()); } From c736435d7081bc307e240a98574d1f24d267e860 Mon Sep 17 00:00:00 2001 From: Michael Lynch Date: Sat, 25 Apr 2026 00:04:29 +0000 Subject: [PATCH 04/17] Test advert parsing via mesh receive path --- platformio.ini | 6 + test/mocks/Arduino.h | 10 + test/mocks/Ed25519.h | 18 + test/mocks/SHA256.h | 40 +- test/mocks/Stream.h | 11 +- test/test_utils/test_advert_data.cpp | 551 +++++++++++++++++++++------ 6 files changed, 502 insertions(+), 134 deletions(-) create mode 100644 test/mocks/Ed25519.h diff --git a/platformio.ini b/platformio.ini index 7ff42af8d3..8e30c804ac 100644 --- a/platformio.ini +++ b/platformio.ini @@ -163,7 +163,13 @@ build_flags = -std=c++17 test_build_src = yes build_src_filter = -<*> + +<../src/Dispatcher.cpp> + +<../src/Identity.cpp> + +<../src/Mesh.cpp> + +<../src/Packet.cpp> +<../src/Utils.cpp> +<../src/helpers/AdvertDataHelpers.cpp> + +<../src/helpers/BaseChatMesh.cpp> + +<../src/helpers/TxtDataHelpers.cpp> lib_deps = google/googletest @ 1.17.0 diff --git a/test/mocks/Arduino.h b/test/mocks/Arduino.h index d265a16e6a..d35b9fb9ef 100644 --- a/test/mocks/Arduino.h +++ b/test/mocks/Arduino.h @@ -1,6 +1,16 @@ #pragma once +#include #include #include #include #include + +inline char* ltoa(long value, char* buffer, int base) { + if (base == 10) { + snprintf(buffer, 32, "%ld", value); + } else { + buffer[0] = 0; + } + return buffer; +} diff --git a/test/mocks/Ed25519.h b/test/mocks/Ed25519.h new file mode 100644 index 0000000000..1a57f5d1ea --- /dev/null +++ b/test/mocks/Ed25519.h @@ -0,0 +1,18 @@ +#pragma once + +#ifdef __cplusplus +extern "C" { +#endif +#include +#ifdef __cplusplus +} +#endif + +#include + +class Ed25519 { +public: + static bool verify(const uint8_t* sig, const uint8_t* pub_key, const uint8_t* message, int msg_len) { + return ed25519_verify(sig, message, msg_len, pub_key) != 0; + } +}; diff --git a/test/mocks/SHA256.h b/test/mocks/SHA256.h index b6e551a077..0c783dc02a 100644 --- a/test/mocks/SHA256.h +++ b/test/mocks/SHA256.h @@ -2,13 +2,41 @@ #include #include +#include -// Mock SHA256 class for testing -// Provides minimal interface to allow Utils.cpp to compile class SHA256 { + uint32_t state_ = 2166136261u; + public: - void update(const uint8_t* data, size_t len) {} - void finalize(uint8_t* hash, size_t hashLen) {} - void resetHMAC(const uint8_t* key, size_t keyLen) {} - void finalizeHMAC(const uint8_t* key, size_t keyLen, uint8_t* hash, size_t hashLen) {} + void update(const void* data, size_t len) { + update(static_cast(data), len); + } + + void update(const uint8_t* data, size_t len) { + for (size_t i = 0; i < len; ++i) { + state_ ^= data[i]; + state_ *= 16777619u; + state_ += 0x9E3779B9u; + } + } + + void finalize(uint8_t* hash, size_t hashLen) { + uint32_t value = state_; + for (size_t i = 0; i < hashLen; ++i) { + value ^= value >> 13; + value *= 1274126177u; + hash[i] = static_cast((value >> ((i & 3) * 8)) & 0xFF); + } + } + + void resetHMAC(const uint8_t* key, size_t keyLen) { + state_ = 2166136261u; + update(key, keyLen); + state_ ^= 0x36363636u; + } + + void finalizeHMAC(const uint8_t* key, size_t keyLen, uint8_t* hash, size_t hashLen) { + update(key, keyLen); + finalize(hash, hashLen); + } }; diff --git a/test/mocks/Stream.h b/test/mocks/Stream.h index 195a302973..5cc399eb3e 100644 --- a/test/mocks/Stream.h +++ b/test/mocks/Stream.h @@ -1,10 +1,13 @@ #pragma once -// Mock Stream class for native testing -// Provides minimal interface needed by Utils.h +#include +#include class Stream { public: - virtual void print(char c) {} - virtual void print(const char* str) {} + virtual size_t readBytes(uint8_t*, size_t) { return 0; } + virtual size_t write(const uint8_t*, size_t len) { return len; } + virtual void print(char) {} + virtual void print(const char*) {} + virtual void println() {} }; diff --git a/test/test_utils/test_advert_data.cpp b/test/test_utils/test_advert_data.cpp index 0de8e92aa4..5870fce015 100644 --- a/test/test_utils/test_advert_data.cpp +++ b/test/test_utils/test_advert_data.cpp @@ -1,172 +1,475 @@ #include #include +#include +#include #include -#include "helpers/AdvertDataHelpers.h" +#include "helpers/BaseChatMesh.h" +#include "helpers/SimpleMeshTables.h" namespace { +constexpr char kSenderPrivateKeyHex[] = + "70" + "65e18fd9fabb70c1ed90dca19907de698c88b709ea146eafd93d9b830c7b60" + "c4681193c79bbc39945ba8064104bb618f8fd7a84a0af6f57033d6e8ddcd6471"; +constexpr char kSenderPublicKeyHex[] = + "1ec77175b0918ed206f9ae04ec136d6d5d4315bb26305427f645b492e9350c10"; + void WriteU8(uint8_t* dest, size_t* offset, uint8_t value) { - dest[(*offset)++] = value; + dest[(*offset)++] = value; } void WriteI32Le(uint8_t* dest, size_t* offset, int32_t value) { - const uint32_t raw = static_cast(value); - dest[(*offset)++] = static_cast(raw & 0xFF); - dest[(*offset)++] = static_cast((raw >> 8) & 0xFF); - dest[(*offset)++] = static_cast((raw >> 16) & 0xFF); - dest[(*offset)++] = static_cast((raw >> 24) & 0xFF); + const uint32_t raw = static_cast(value); + dest[(*offset)++] = static_cast(raw & 0xFF); + dest[(*offset)++] = static_cast((raw >> 8) & 0xFF); + dest[(*offset)++] = static_cast((raw >> 16) & 0xFF); + dest[(*offset)++] = static_cast((raw >> 24) & 0xFF); } void WriteBytes(uint8_t* dest, size_t* offset, const uint8_t* bytes, size_t length) { - for (size_t i = 0; i < length; ++i) { - dest[*offset + i] = bytes[i]; - } - *offset += length; + memcpy(dest + *offset, bytes, length); + *offset += length; } template void WriteStringLiteral(uint8_t* dest, size_t* offset, const char (&value)[N]) { - static_assert(N > 0, "string literal must include a null terminator"); - WriteBytes(dest, offset, reinterpret_cast(value), N - 1); + static_assert(N > 0, "string literal must include a null terminator"); + WriteBytes(dest, offset, reinterpret_cast(value), N - 1); } -TEST(AdvertData, RoundTripsNameOnly) { - uint8_t app_data[MAX_ADVERT_DATA_SIZE] = {}; - size_t offset = 0; +class FakeMillis final : public mesh::MillisecondClock { +public: + unsigned long getMillis() override { + return 0; + } +}; + +class FakeRtc final : public mesh::RTCClock { +public: + explicit FakeRtc(uint32_t initial_time) : _time(initial_time) {} + + uint32_t getCurrentTime() override { + return _time; + } + + void setCurrentTime(uint32_t time) override { + _time = time; + } + +private: + uint32_t _time; +}; - // flags/type byte: chat advert with a trailing name field. - WriteU8(app_data, &offset, ADV_TYPE_CHAT | ADV_NAME_MASK); - // name field: raw bytes for "alice", consuming the rest of app_data. - WriteStringLiteral(app_data, &offset, "alice"); +class FakeRng final : public mesh::RNG { +public: + void random(uint8_t* dest, size_t sz) override { + memset(dest, 0x5A, sz); + } +}; - AdvertDataParser parser(app_data, offset); +class FakeRadio final : public mesh::Radio { +public: + int recvRaw(uint8_t*, int) override { + return 0; + } - ASSERT_TRUE(parser.isValid()); - EXPECT_EQ(ADV_TYPE_CHAT, parser.getType()); - EXPECT_TRUE(parser.hasName()); - EXPECT_STREQ("alice", parser.getName()); - EXPECT_FALSE(parser.hasLatLon()); -} + uint32_t getEstAirtimeFor(int) override { + return 1; + } -TEST(AdvertData, RoundTripsNameAndCoordinates) { - uint8_t app_data[MAX_ADVERT_DATA_SIZE] = {}; - size_t offset = 0; - - // flags/type byte: repeater advert with lat/lon followed by a name. - WriteU8(app_data, &offset, ADV_TYPE_REPEATER | ADV_LATLON_MASK | ADV_NAME_MASK); - // latitude field: signed little-endian microdegrees for 37.7749. - WriteI32Le(app_data, &offset, 37774900); - // longitude field: signed little-endian microdegrees for -122.4194. - WriteI32Le(app_data, &offset, -122419400); - // name field: raw bytes for "node" after the coordinate fields. - WriteStringLiteral(app_data, &offset, "node"); - - AdvertDataParser parser(app_data, offset); - - ASSERT_TRUE(parser.isValid()); - EXPECT_EQ(ADV_TYPE_REPEATER, parser.getType()); - EXPECT_TRUE(parser.hasName()); - EXPECT_STREQ("node", parser.getName()); - EXPECT_TRUE(parser.hasLatLon()); - EXPECT_EQ(37774900, parser.getIntLat()); - EXPECT_EQ(-122419400, parser.getIntLon()); - EXPECT_DOUBLE_EQ(37.7749, parser.getLat()); - EXPECT_DOUBLE_EQ(-122.4194, parser.getLon()); -} + float packetScore(float, int) override { + return 1.0f; + } -TEST(AdvertData, RoundTripsCoordinateExtremes) { - uint8_t app_data[MAX_ADVERT_DATA_SIZE] = {}; - size_t offset = 0; - - // flags/type byte: sensor advert with both location fields and a name. - WriteU8(app_data, &offset, ADV_TYPE_SENSOR | ADV_LATLON_MASK | ADV_NAME_MASK); - // latitude field: minimum supported latitude, -90.000000 degrees. - WriteI32Le(app_data, &offset, -90000000); - // longitude field: maximum supported longitude, 180.000000 degrees. - WriteI32Le(app_data, &offset, 180000000); - // name field: raw bytes for "edge". - WriteStringLiteral(app_data, &offset, "edge"); - - AdvertDataParser parser(app_data, offset); - - ASSERT_TRUE(parser.isValid()); - EXPECT_TRUE(parser.hasLatLon()); - EXPECT_EQ(-90000000, parser.getIntLat()); - EXPECT_EQ(180000000, parser.getIntLon()); -} + bool startSendRaw(const uint8_t*, int) override { + return true; + } + + bool isSendComplete() override { + return true; + } + + void onSendFinished() override {} + + bool isInRecvMode() const override { + return true; + } +}; + +class NoopPacketManager final : public mesh::PacketManager { +public: + mesh::Packet* allocNew() override { + return nullptr; + } + + void free(mesh::Packet*) override {} + + void queueOutbound(mesh::Packet*, uint8_t, uint32_t) override {} + + mesh::Packet* getNextOutbound(uint32_t) override { + return nullptr; + } + + int getOutboundCount(uint32_t) const override { + return 0; + } + + int getOutboundTotal() const override { + return 0; + } + + int getFreeCount() const override { + return 0; + } + + mesh::Packet* getOutboundByIdx(int) override { + return nullptr; + } + + mesh::Packet* removeOutboundByIdx(int) override { + return nullptr; + } + + void queueInbound(mesh::Packet*, uint32_t) override {} + + mesh::Packet* getNextInbound(uint32_t) override { + return nullptr; + } +}; + +class TestChatMesh final : public BaseChatMesh { +public: + TestChatMesh(mesh::Radio& radio, mesh::MillisecondClock& ms, mesh::RNG& rng, mesh::RTCClock& rtc, + mesh::PacketManager& mgr, mesh::MeshTables& tables) + : BaseChatMesh(radio, ms, rng, rtc, mgr, tables) {} + + mesh::DispatcherAction recv(mesh::Packet* pkt) { + return onRecvPacket(pkt); + } -TEST(AdvertData, RejectsLongitudeOutsideValidRange) { - uint8_t app_data[MAX_ADVERT_DATA_SIZE] = {}; - size_t offset = 0; + std::optional discovered_contact; - // flags/type byte: chat advert with location and name fields present. - WriteU8(app_data, &offset, ADV_TYPE_CHAT | ADV_LATLON_MASK | ADV_NAME_MASK); - // latitude field: valid latitude so the failure comes from longitude. - WriteI32Le(app_data, &offset, 37774900); - // longitude field: one microdegree above +180.0, which is invalid. - WriteI32Le(app_data, &offset, 180000001); - // name field: parser should reject before the trailing name matters. - WriteStringLiteral(app_data, &offset, "node"); +protected: + void onDiscoveredContact(ContactInfo& contact, bool, uint8_t, const uint8_t*) override { + discovered_contact = contact; + } - AdvertDataParser parser(app_data, offset); + ContactInfo* processAck(const uint8_t*) override { + return nullptr; + } - EXPECT_FALSE(parser.isValid()); + void onContactPathUpdated(const ContactInfo&) override {} + + void onMessageRecv(const ContactInfo&, mesh::Packet*, uint32_t, const char*) override {} + + void onCommandDataRecv(const ContactInfo&, mesh::Packet*, uint32_t, const char*) override {} + + void onSignedMessageRecv(const ContactInfo&, mesh::Packet*, uint32_t, const uint8_t*, const char*) override {} + + uint32_t calcFloodTimeoutMillisFor(uint32_t) const override { + return 0; + } + + uint32_t calcDirectTimeoutMillisFor(uint32_t, uint8_t) const override { + return 0; + } + + void onSendTimeout() override {} + + void onChannelMessageRecv(const mesh::GroupChannel&, mesh::Packet*, uint32_t, const char*) override {} + + uint8_t onContactRequest(const ContactInfo&, uint32_t, const uint8_t*, uint8_t, uint8_t*) override { + return 0; + } + + void onContactResponse(const ContactInfo&, const uint8_t*, uint8_t) override {} +}; + +mesh::LocalIdentity MakeSenderIdentity() { + return mesh::LocalIdentity(kSenderPrivateKeyHex, kSenderPublicKeyHex); } -TEST(AdvertData, RejectsLongitudeBelowValidRange) { - uint8_t app_data[MAX_ADVERT_DATA_SIZE] = {}; - size_t offset = 0; +mesh::Packet BuildSignedAdvertPacket(const mesh::LocalIdentity& sender, uint32_t timestamp, + const uint8_t* app_data, uint8_t app_data_len) { + mesh::Packet packet; + + // Wire header: flood-routed advert packet with no path hashes yet. + packet.header = ROUTE_TYPE_FLOOD | (PAYLOAD_TYPE_ADVERT << PH_TYPE_SHIFT); + packet.path_len = 0; + + int offset = 0; + // Sender public key: used by the receiver to identify who signed the advert. + memcpy(&packet.payload[offset], sender.pub_key, PUB_KEY_SIZE); + offset += PUB_KEY_SIZE; + + // Advert timestamp: the sender's monotonic advert time used for replay checks. + memcpy(&packet.payload[offset], ×tamp, sizeof(timestamp)); + offset += sizeof(timestamp); + + // Signature field: filled after the signed message bytes are assembled below. + uint8_t* signature = &packet.payload[offset]; + offset += SIGNATURE_SIZE; + + // Raw advert app_data: arbitrary bytes authored by the test, not by createAdvert(). + memcpy(&packet.payload[offset], app_data, app_data_len); + offset += app_data_len; + packet.payload_len = offset; + + uint8_t message[PUB_KEY_SIZE + 4 + MAX_ADVERT_DATA_SIZE]; + int message_len = 0; + memcpy(&message[message_len], sender.pub_key, PUB_KEY_SIZE); + message_len += PUB_KEY_SIZE; + memcpy(&message[message_len], ×tamp, sizeof(timestamp)); + message_len += sizeof(timestamp); + memcpy(&message[message_len], app_data, app_data_len); + message_len += app_data_len; + + sender.sign(signature, message, message_len); + return packet; +} - // flags/type byte: chat advert with location and name fields present. - WriteU8(app_data, &offset, ADV_TYPE_CHAT | ADV_LATLON_MASK | ADV_NAME_MASK); - // latitude field: valid latitude so the failure comes from longitude. - WriteI32Le(app_data, &offset, 37774900); - // longitude field: one microdegree below -180.0, which is invalid. - WriteI32Le(app_data, &offset, -180000001); - // name field: included to keep the payload shape consistent. - WriteStringLiteral(app_data, &offset, "node"); +TEST(AdvertData, ParsesNameOnlyFromNetworkPacket) { + FakeRadio radio; + FakeMillis ms; + FakeRng rng; + FakeRtc rtc(1704067200U); + NoopPacketManager packet_manager; + SimpleMeshTables tables; + TestChatMesh mesh(radio, ms, rng, rtc, packet_manager, tables); + + uint8_t app_data[MAX_ADVERT_DATA_SIZE] = {}; + size_t offset = 0; + + // flags/type byte: chat advert with a trailing name field. + WriteU8(app_data, &offset, ADV_TYPE_CHAT | ADV_NAME_MASK); + // name field: raw bytes for "alice", consuming the rest of app_data. + WriteStringLiteral(app_data, &offset, "alice"); + + mesh::LocalIdentity sender = MakeSenderIdentity(); + mesh::Packet packet = BuildSignedAdvertPacket(sender, 1704067201U, app_data, offset); + + mesh.recv(&packet); + + ASSERT_TRUE(mesh.discovered_contact.has_value()); + EXPECT_EQ(ADV_TYPE_CHAT, mesh.discovered_contact->type); + EXPECT_STREQ("alice", mesh.discovered_contact->name); + EXPECT_EQ(1704067201U, mesh.discovered_contact->last_advert_timestamp); + EXPECT_EQ(1704067200U, mesh.discovered_contact->lastmod); + EXPECT_EQ(0, mesh.discovered_contact->gps_lat); + EXPECT_EQ(0, mesh.discovered_contact->gps_lon); +} - AdvertDataParser parser(app_data, offset); +TEST(AdvertData, ParsesNameAndCoordinatesFromNetworkPacket) { + FakeRadio radio; + FakeMillis ms; + FakeRng rng; + FakeRtc rtc(1704067200U); + NoopPacketManager packet_manager; + SimpleMeshTables tables; + TestChatMesh mesh(radio, ms, rng, rtc, packet_manager, tables); + + uint8_t app_data[MAX_ADVERT_DATA_SIZE] = {}; + size_t offset = 0; + + // flags/type byte: repeater advert with lat/lon followed by a name. + WriteU8(app_data, &offset, ADV_TYPE_REPEATER | ADV_LATLON_MASK | ADV_NAME_MASK); + // latitude field: signed little-endian microdegrees for 37.7749. + WriteI32Le(app_data, &offset, 37774900); + // longitude field: signed little-endian microdegrees for -122.4194. + WriteI32Le(app_data, &offset, -122419400); + // name field: raw bytes for "node" after the coordinate fields. + WriteStringLiteral(app_data, &offset, "node"); + + mesh::LocalIdentity sender = MakeSenderIdentity(); + mesh::Packet packet = BuildSignedAdvertPacket(sender, 1704067201U, app_data, offset); + + mesh.recv(&packet); + + ASSERT_TRUE(mesh.discovered_contact.has_value()); + EXPECT_EQ(ADV_TYPE_REPEATER, mesh.discovered_contact->type); + EXPECT_STREQ("node", mesh.discovered_contact->name); + EXPECT_EQ(37774900, mesh.discovered_contact->gps_lat); + EXPECT_EQ(-122419400, mesh.discovered_contact->gps_lon); +} - EXPECT_FALSE(parser.isValid()); +TEST(AdvertData, ParsesCoordinateExtremesFromNetworkPacket) { + FakeRadio radio; + FakeMillis ms; + FakeRng rng; + FakeRtc rtc(1704067200U); + NoopPacketManager packet_manager; + SimpleMeshTables tables; + TestChatMesh mesh(radio, ms, rng, rtc, packet_manager, tables); + + uint8_t app_data[MAX_ADVERT_DATA_SIZE] = {}; + size_t offset = 0; + + // flags/type byte: sensor advert with both location fields and a name. + WriteU8(app_data, &offset, ADV_TYPE_SENSOR | ADV_LATLON_MASK | ADV_NAME_MASK); + // latitude field: minimum supported latitude, -90.000000 degrees. + WriteI32Le(app_data, &offset, -90000000); + // longitude field: maximum supported longitude, 180.000000 degrees. + WriteI32Le(app_data, &offset, 180000000); + // name field: raw bytes for "edge". + WriteStringLiteral(app_data, &offset, "edge"); + + mesh::LocalIdentity sender = MakeSenderIdentity(); + mesh::Packet packet = BuildSignedAdvertPacket(sender, 1704067201U, app_data, offset); + + mesh.recv(&packet); + + ASSERT_TRUE(mesh.discovered_contact.has_value()); + EXPECT_EQ(ADV_TYPE_SENSOR, mesh.discovered_contact->type); + EXPECT_STREQ("edge", mesh.discovered_contact->name); + EXPECT_EQ(-90000000, mesh.discovered_contact->gps_lat); + EXPECT_EQ(180000000, mesh.discovered_contact->gps_lon); } -TEST(AdvertData, RejectsLatitudeOutsideValidRange) { - uint8_t app_data[MAX_ADVERT_DATA_SIZE] = {}; - size_t offset = 0; +TEST(AdvertData, RejectsLongitudeOutsideValidRangeFromNetworkPacket) { + FakeRadio radio; + FakeMillis ms; + FakeRng rng; + FakeRtc rtc(1704067200U); + NoopPacketManager packet_manager; + SimpleMeshTables tables; + TestChatMesh mesh(radio, ms, rng, rtc, packet_manager, tables); + + uint8_t app_data[MAX_ADVERT_DATA_SIZE] = {}; + size_t offset = 0; + + // flags/type byte: chat advert with location and name fields present. + WriteU8(app_data, &offset, ADV_TYPE_CHAT | ADV_LATLON_MASK | ADV_NAME_MASK); + // latitude field: valid latitude so the failure comes from longitude. + WriteI32Le(app_data, &offset, 37774900); + // longitude field: one microdegree above +180.0, which is invalid. + WriteI32Le(app_data, &offset, 180000001); + // name field: parser should reject before the trailing name matters. + WriteStringLiteral(app_data, &offset, "node"); + + mesh::LocalIdentity sender = MakeSenderIdentity(); + mesh::Packet packet = BuildSignedAdvertPacket(sender, 1704067201U, app_data, offset); + + mesh.recv(&packet); + + EXPECT_FALSE(mesh.discovered_contact.has_value()); +} - // flags/type byte: chat advert with location and name fields present. - WriteU8(app_data, &offset, ADV_TYPE_CHAT | ADV_LATLON_MASK | ADV_NAME_MASK); - // latitude field: one microdegree above +90.0, which is invalid. - WriteI32Le(app_data, &offset, 90000001); - // longitude field: valid longitude so the failure comes from latitude. - WriteI32Le(app_data, &offset, -122419400); - // name field: included to keep the payload shape consistent. - WriteStringLiteral(app_data, &offset, "node"); +TEST(AdvertData, RejectsLongitudeBelowValidRangeFromNetworkPacket) { + FakeRadio radio; + FakeMillis ms; + FakeRng rng; + FakeRtc rtc(1704067200U); + NoopPacketManager packet_manager; + SimpleMeshTables tables; + TestChatMesh mesh(radio, ms, rng, rtc, packet_manager, tables); + + uint8_t app_data[MAX_ADVERT_DATA_SIZE] = {}; + size_t offset = 0; + + // flags/type byte: chat advert with location and name fields present. + WriteU8(app_data, &offset, ADV_TYPE_CHAT | ADV_LATLON_MASK | ADV_NAME_MASK); + // latitude field: valid latitude so the failure comes from longitude. + WriteI32Le(app_data, &offset, 37774900); + // longitude field: one microdegree below -180.0, which is invalid. + WriteI32Le(app_data, &offset, -180000001); + // name field: included to keep the payload shape consistent. + WriteStringLiteral(app_data, &offset, "node"); + + mesh::LocalIdentity sender = MakeSenderIdentity(); + mesh::Packet packet = BuildSignedAdvertPacket(sender, 1704067201U, app_data, offset); + + mesh.recv(&packet); + + EXPECT_FALSE(mesh.discovered_contact.has_value()); +} - AdvertDataParser parser(app_data, offset); +TEST(AdvertData, RejectsLatitudeOutsideValidRangeFromNetworkPacket) { + FakeRadio radio; + FakeMillis ms; + FakeRng rng; + FakeRtc rtc(1704067200U); + NoopPacketManager packet_manager; + SimpleMeshTables tables; + TestChatMesh mesh(radio, ms, rng, rtc, packet_manager, tables); + + uint8_t app_data[MAX_ADVERT_DATA_SIZE] = {}; + size_t offset = 0; + + // flags/type byte: chat advert with location and name fields present. + WriteU8(app_data, &offset, ADV_TYPE_CHAT | ADV_LATLON_MASK | ADV_NAME_MASK); + // latitude field: one microdegree above +90.0, which is invalid. + WriteI32Le(app_data, &offset, 90000001); + // longitude field: valid longitude so the failure comes from latitude. + WriteI32Le(app_data, &offset, -122419400); + // name field: included to keep the payload shape consistent. + WriteStringLiteral(app_data, &offset, "node"); + + mesh::LocalIdentity sender = MakeSenderIdentity(); + mesh::Packet packet = BuildSignedAdvertPacket(sender, 1704067201U, app_data, offset); + + mesh.recv(&packet); + + EXPECT_FALSE(mesh.discovered_contact.has_value()); +} - EXPECT_FALSE(parser.isValid()); +TEST(AdvertData, RejectsLatitudeBelowValidRangeFromNetworkPacket) { + FakeRadio radio; + FakeMillis ms; + FakeRng rng; + FakeRtc rtc(1704067200U); + NoopPacketManager packet_manager; + SimpleMeshTables tables; + TestChatMesh mesh(radio, ms, rng, rtc, packet_manager, tables); + + uint8_t app_data[MAX_ADVERT_DATA_SIZE] = {}; + size_t offset = 0; + + // flags/type byte: chat advert with location and name fields present. + WriteU8(app_data, &offset, ADV_TYPE_CHAT | ADV_LATLON_MASK | ADV_NAME_MASK); + // latitude field: one microdegree below -90.0, which is invalid. + WriteI32Le(app_data, &offset, -90000001); + // longitude field: valid longitude so the failure comes from latitude. + WriteI32Le(app_data, &offset, -122419400); + // name field: included to keep the payload shape consistent. + WriteStringLiteral(app_data, &offset, "node"); + + mesh::LocalIdentity sender = MakeSenderIdentity(); + mesh::Packet packet = BuildSignedAdvertPacket(sender, 1704067201U, app_data, offset); + + mesh.recv(&packet); + + EXPECT_FALSE(mesh.discovered_contact.has_value()); } -TEST(AdvertData, RejectsLatitudeBelowValidRange) { - uint8_t app_data[MAX_ADVERT_DATA_SIZE] = {}; - size_t offset = 0; +TEST(AdvertData, RejectsForgedSignatureFromNetworkPacket) { + FakeRadio radio; + FakeMillis ms; + FakeRng rng; + FakeRtc rtc(1704067200U); + NoopPacketManager packet_manager; + SimpleMeshTables tables; + TestChatMesh mesh(radio, ms, rng, rtc, packet_manager, tables); + + uint8_t app_data[MAX_ADVERT_DATA_SIZE] = {}; + size_t offset = 0; + + // flags/type byte: chat advert with a trailing name field. + WriteU8(app_data, &offset, ADV_TYPE_CHAT | ADV_NAME_MASK); + // name field: raw bytes for "mallory". + WriteStringLiteral(app_data, &offset, "mallory"); + + mesh::LocalIdentity sender = MakeSenderIdentity(); + mesh::Packet packet = BuildSignedAdvertPacket(sender, 1704067201U, app_data, offset); - // flags/type byte: chat advert with location and name fields present. - WriteU8(app_data, &offset, ADV_TYPE_CHAT | ADV_LATLON_MASK | ADV_NAME_MASK); - // latitude field: one microdegree below -90.0, which is invalid. - WriteI32Le(app_data, &offset, -90000001); - // longitude field: valid longitude so the failure comes from latitude. - WriteI32Le(app_data, &offset, -122419400); - // name field: included to keep the payload shape consistent. - WriteStringLiteral(app_data, &offset, "node"); + // Corrupt the signature bytes after signing so verification must fail in Mesh::onRecvPacket(). + packet.payload[PUB_KEY_SIZE + 4] ^= 0xFF; - AdvertDataParser parser(app_data, offset); + mesh.recv(&packet); - EXPECT_FALSE(parser.isValid()); + EXPECT_FALSE(mesh.discovered_contact.has_value()); } } // namespace From 44bcaf77715cb36bed4d207480e74e77cb32b4c1 Mon Sep 17 00:00:00 2001 From: Michael Lynch Date: Sat, 25 Apr 2026 00:10:52 +0000 Subject: [PATCH 05/17] Move advert data test to helpers --- test/{test_utils => helpers}/test_advert_data.cpp | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename test/{test_utils => helpers}/test_advert_data.cpp (100%) diff --git a/test/test_utils/test_advert_data.cpp b/test/helpers/test_advert_data.cpp similarity index 100% rename from test/test_utils/test_advert_data.cpp rename to test/helpers/test_advert_data.cpp From 84983ed0b88353dd20c0f1dfecdcbc786a6d5a97 Mon Sep 17 00:00:00 2001 From: Michael Lynch Date: Sat, 25 Apr 2026 00:30:12 +0000 Subject: [PATCH 06/17] Fix native helper test discovery --- test/{helpers => test_helpers}/test_advert_data.cpp | 0 test/test_helpers/test_main.cpp | 6 ++++++ 2 files changed, 6 insertions(+) rename test/{helpers => test_helpers}/test_advert_data.cpp (100%) create mode 100644 test/test_helpers/test_main.cpp diff --git a/test/helpers/test_advert_data.cpp b/test/test_helpers/test_advert_data.cpp similarity index 100% rename from test/helpers/test_advert_data.cpp rename to test/test_helpers/test_advert_data.cpp diff --git a/test/test_helpers/test_main.cpp b/test/test_helpers/test_main.cpp new file mode 100644 index 0000000000..697a9d70c0 --- /dev/null +++ b/test/test_helpers/test_main.cpp @@ -0,0 +1,6 @@ +#include + +int main(int argc, char** argv) { + ::testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} From 2dac5740968ab7f255d27f8b9a35a058c322da16 Mon Sep 17 00:00:00 2001 From: Michael Lynch Date: Sat, 25 Apr 2026 00:35:18 +0000 Subject: [PATCH 07/17] Inline advert test sender identity --- test/test_helpers/test_advert_data.cpp | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/test/test_helpers/test_advert_data.cpp b/test/test_helpers/test_advert_data.cpp index 5870fce015..bc287a4359 100644 --- a/test/test_helpers/test_advert_data.cpp +++ b/test/test_helpers/test_advert_data.cpp @@ -188,10 +188,6 @@ class TestChatMesh final : public BaseChatMesh { void onContactResponse(const ContactInfo&, const uint8_t*, uint8_t) override {} }; -mesh::LocalIdentity MakeSenderIdentity() { - return mesh::LocalIdentity(kSenderPrivateKeyHex, kSenderPublicKeyHex); -} - mesh::Packet BuildSignedAdvertPacket(const mesh::LocalIdentity& sender, uint32_t timestamp, const uint8_t* app_data, uint8_t app_data_len) { mesh::Packet packet; @@ -248,7 +244,7 @@ TEST(AdvertData, ParsesNameOnlyFromNetworkPacket) { // name field: raw bytes for "alice", consuming the rest of app_data. WriteStringLiteral(app_data, &offset, "alice"); - mesh::LocalIdentity sender = MakeSenderIdentity(); + mesh::LocalIdentity sender(kSenderPrivateKeyHex, kSenderPublicKeyHex); mesh::Packet packet = BuildSignedAdvertPacket(sender, 1704067201U, app_data, offset); mesh.recv(&packet); @@ -283,7 +279,7 @@ TEST(AdvertData, ParsesNameAndCoordinatesFromNetworkPacket) { // name field: raw bytes for "node" after the coordinate fields. WriteStringLiteral(app_data, &offset, "node"); - mesh::LocalIdentity sender = MakeSenderIdentity(); + mesh::LocalIdentity sender(kSenderPrivateKeyHex, kSenderPublicKeyHex); mesh::Packet packet = BuildSignedAdvertPacket(sender, 1704067201U, app_data, offset); mesh.recv(&packet); @@ -316,7 +312,7 @@ TEST(AdvertData, ParsesCoordinateExtremesFromNetworkPacket) { // name field: raw bytes for "edge". WriteStringLiteral(app_data, &offset, "edge"); - mesh::LocalIdentity sender = MakeSenderIdentity(); + mesh::LocalIdentity sender(kSenderPrivateKeyHex, kSenderPublicKeyHex); mesh::Packet packet = BuildSignedAdvertPacket(sender, 1704067201U, app_data, offset); mesh.recv(&packet); @@ -349,7 +345,7 @@ TEST(AdvertData, RejectsLongitudeOutsideValidRangeFromNetworkPacket) { // name field: parser should reject before the trailing name matters. WriteStringLiteral(app_data, &offset, "node"); - mesh::LocalIdentity sender = MakeSenderIdentity(); + mesh::LocalIdentity sender(kSenderPrivateKeyHex, kSenderPublicKeyHex); mesh::Packet packet = BuildSignedAdvertPacket(sender, 1704067201U, app_data, offset); mesh.recv(&packet); @@ -378,7 +374,7 @@ TEST(AdvertData, RejectsLongitudeBelowValidRangeFromNetworkPacket) { // name field: included to keep the payload shape consistent. WriteStringLiteral(app_data, &offset, "node"); - mesh::LocalIdentity sender = MakeSenderIdentity(); + mesh::LocalIdentity sender(kSenderPrivateKeyHex, kSenderPublicKeyHex); mesh::Packet packet = BuildSignedAdvertPacket(sender, 1704067201U, app_data, offset); mesh.recv(&packet); @@ -407,7 +403,7 @@ TEST(AdvertData, RejectsLatitudeOutsideValidRangeFromNetworkPacket) { // name field: included to keep the payload shape consistent. WriteStringLiteral(app_data, &offset, "node"); - mesh::LocalIdentity sender = MakeSenderIdentity(); + mesh::LocalIdentity sender(kSenderPrivateKeyHex, kSenderPublicKeyHex); mesh::Packet packet = BuildSignedAdvertPacket(sender, 1704067201U, app_data, offset); mesh.recv(&packet); @@ -436,7 +432,7 @@ TEST(AdvertData, RejectsLatitudeBelowValidRangeFromNetworkPacket) { // name field: included to keep the payload shape consistent. WriteStringLiteral(app_data, &offset, "node"); - mesh::LocalIdentity sender = MakeSenderIdentity(); + mesh::LocalIdentity sender(kSenderPrivateKeyHex, kSenderPublicKeyHex); mesh::Packet packet = BuildSignedAdvertPacket(sender, 1704067201U, app_data, offset); mesh.recv(&packet); @@ -461,7 +457,7 @@ TEST(AdvertData, RejectsForgedSignatureFromNetworkPacket) { // name field: raw bytes for "mallory". WriteStringLiteral(app_data, &offset, "mallory"); - mesh::LocalIdentity sender = MakeSenderIdentity(); + mesh::LocalIdentity sender(kSenderPrivateKeyHex, kSenderPublicKeyHex); mesh::Packet packet = BuildSignedAdvertPacket(sender, 1704067201U, app_data, offset); // Corrupt the signature bytes after signing so verification must fail in Mesh::onRecvPacket(). From 390a74d6610dbd79cf6dc1f79f2415b8105c8acb Mon Sep 17 00:00:00 2001 From: Michael Lynch Date: Sat, 25 Apr 2026 00:39:54 +0000 Subject: [PATCH 08/17] Simplify advert test packet helper --- test/test_helpers/test_advert_data.cpp | 38 +++++++++++++------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/test/test_helpers/test_advert_data.cpp b/test/test_helpers/test_advert_data.cpp index bc287a4359..510b1c4975 100644 --- a/test/test_helpers/test_advert_data.cpp +++ b/test/test_helpers/test_advert_data.cpp @@ -188,8 +188,8 @@ class TestChatMesh final : public BaseChatMesh { void onContactResponse(const ContactInfo&, const uint8_t*, uint8_t) override {} }; -mesh::Packet BuildSignedAdvertPacket(const mesh::LocalIdentity& sender, uint32_t timestamp, - const uint8_t* app_data, uint8_t app_data_len) { +mesh::Packet BuildSignedAdvertPacket(uint32_t timestamp, const uint8_t* app_data, uint8_t app_data_len) { + mesh::LocalIdentity sender(kSenderPrivateKeyHex, kSenderPublicKeyHex); mesh::Packet packet; // Wire header: flood-routed advert packet with no path hashes yet. @@ -238,21 +238,21 @@ TEST(AdvertData, ParsesNameOnlyFromNetworkPacket) { uint8_t app_data[MAX_ADVERT_DATA_SIZE] = {}; size_t offset = 0; + constexpr uint32_t advert_timestamp = 1704067201U; // flags/type byte: chat advert with a trailing name field. WriteU8(app_data, &offset, ADV_TYPE_CHAT | ADV_NAME_MASK); // name field: raw bytes for "alice", consuming the rest of app_data. WriteStringLiteral(app_data, &offset, "alice"); - mesh::LocalIdentity sender(kSenderPrivateKeyHex, kSenderPublicKeyHex); - mesh::Packet packet = BuildSignedAdvertPacket(sender, 1704067201U, app_data, offset); + mesh::Packet packet = BuildSignedAdvertPacket(advert_timestamp, app_data, offset); mesh.recv(&packet); ASSERT_TRUE(mesh.discovered_contact.has_value()); EXPECT_EQ(ADV_TYPE_CHAT, mesh.discovered_contact->type); EXPECT_STREQ("alice", mesh.discovered_contact->name); - EXPECT_EQ(1704067201U, mesh.discovered_contact->last_advert_timestamp); + EXPECT_EQ(advert_timestamp, mesh.discovered_contact->last_advert_timestamp); EXPECT_EQ(1704067200U, mesh.discovered_contact->lastmod); EXPECT_EQ(0, mesh.discovered_contact->gps_lat); EXPECT_EQ(0, mesh.discovered_contact->gps_lon); @@ -269,6 +269,7 @@ TEST(AdvertData, ParsesNameAndCoordinatesFromNetworkPacket) { uint8_t app_data[MAX_ADVERT_DATA_SIZE] = {}; size_t offset = 0; + constexpr uint32_t advert_timestamp = 1704067201U; // flags/type byte: repeater advert with lat/lon followed by a name. WriteU8(app_data, &offset, ADV_TYPE_REPEATER | ADV_LATLON_MASK | ADV_NAME_MASK); @@ -279,8 +280,7 @@ TEST(AdvertData, ParsesNameAndCoordinatesFromNetworkPacket) { // name field: raw bytes for "node" after the coordinate fields. WriteStringLiteral(app_data, &offset, "node"); - mesh::LocalIdentity sender(kSenderPrivateKeyHex, kSenderPublicKeyHex); - mesh::Packet packet = BuildSignedAdvertPacket(sender, 1704067201U, app_data, offset); + mesh::Packet packet = BuildSignedAdvertPacket(advert_timestamp, app_data, offset); mesh.recv(&packet); @@ -302,6 +302,7 @@ TEST(AdvertData, ParsesCoordinateExtremesFromNetworkPacket) { uint8_t app_data[MAX_ADVERT_DATA_SIZE] = {}; size_t offset = 0; + constexpr uint32_t advert_timestamp = 1704067201U; // flags/type byte: sensor advert with both location fields and a name. WriteU8(app_data, &offset, ADV_TYPE_SENSOR | ADV_LATLON_MASK | ADV_NAME_MASK); @@ -312,8 +313,7 @@ TEST(AdvertData, ParsesCoordinateExtremesFromNetworkPacket) { // name field: raw bytes for "edge". WriteStringLiteral(app_data, &offset, "edge"); - mesh::LocalIdentity sender(kSenderPrivateKeyHex, kSenderPublicKeyHex); - mesh::Packet packet = BuildSignedAdvertPacket(sender, 1704067201U, app_data, offset); + mesh::Packet packet = BuildSignedAdvertPacket(advert_timestamp, app_data, offset); mesh.recv(&packet); @@ -335,6 +335,7 @@ TEST(AdvertData, RejectsLongitudeOutsideValidRangeFromNetworkPacket) { uint8_t app_data[MAX_ADVERT_DATA_SIZE] = {}; size_t offset = 0; + constexpr uint32_t advert_timestamp = 1704067201U; // flags/type byte: chat advert with location and name fields present. WriteU8(app_data, &offset, ADV_TYPE_CHAT | ADV_LATLON_MASK | ADV_NAME_MASK); @@ -345,8 +346,7 @@ TEST(AdvertData, RejectsLongitudeOutsideValidRangeFromNetworkPacket) { // name field: parser should reject before the trailing name matters. WriteStringLiteral(app_data, &offset, "node"); - mesh::LocalIdentity sender(kSenderPrivateKeyHex, kSenderPublicKeyHex); - mesh::Packet packet = BuildSignedAdvertPacket(sender, 1704067201U, app_data, offset); + mesh::Packet packet = BuildSignedAdvertPacket(advert_timestamp, app_data, offset); mesh.recv(&packet); @@ -364,6 +364,7 @@ TEST(AdvertData, RejectsLongitudeBelowValidRangeFromNetworkPacket) { uint8_t app_data[MAX_ADVERT_DATA_SIZE] = {}; size_t offset = 0; + constexpr uint32_t advert_timestamp = 1704067201U; // flags/type byte: chat advert with location and name fields present. WriteU8(app_data, &offset, ADV_TYPE_CHAT | ADV_LATLON_MASK | ADV_NAME_MASK); @@ -374,8 +375,7 @@ TEST(AdvertData, RejectsLongitudeBelowValidRangeFromNetworkPacket) { // name field: included to keep the payload shape consistent. WriteStringLiteral(app_data, &offset, "node"); - mesh::LocalIdentity sender(kSenderPrivateKeyHex, kSenderPublicKeyHex); - mesh::Packet packet = BuildSignedAdvertPacket(sender, 1704067201U, app_data, offset); + mesh::Packet packet = BuildSignedAdvertPacket(advert_timestamp, app_data, offset); mesh.recv(&packet); @@ -393,6 +393,7 @@ TEST(AdvertData, RejectsLatitudeOutsideValidRangeFromNetworkPacket) { uint8_t app_data[MAX_ADVERT_DATA_SIZE] = {}; size_t offset = 0; + constexpr uint32_t advert_timestamp = 1704067201U; // flags/type byte: chat advert with location and name fields present. WriteU8(app_data, &offset, ADV_TYPE_CHAT | ADV_LATLON_MASK | ADV_NAME_MASK); @@ -403,8 +404,7 @@ TEST(AdvertData, RejectsLatitudeOutsideValidRangeFromNetworkPacket) { // name field: included to keep the payload shape consistent. WriteStringLiteral(app_data, &offset, "node"); - mesh::LocalIdentity sender(kSenderPrivateKeyHex, kSenderPublicKeyHex); - mesh::Packet packet = BuildSignedAdvertPacket(sender, 1704067201U, app_data, offset); + mesh::Packet packet = BuildSignedAdvertPacket(advert_timestamp, app_data, offset); mesh.recv(&packet); @@ -422,6 +422,7 @@ TEST(AdvertData, RejectsLatitudeBelowValidRangeFromNetworkPacket) { uint8_t app_data[MAX_ADVERT_DATA_SIZE] = {}; size_t offset = 0; + constexpr uint32_t advert_timestamp = 1704067201U; // flags/type byte: chat advert with location and name fields present. WriteU8(app_data, &offset, ADV_TYPE_CHAT | ADV_LATLON_MASK | ADV_NAME_MASK); @@ -432,8 +433,7 @@ TEST(AdvertData, RejectsLatitudeBelowValidRangeFromNetworkPacket) { // name field: included to keep the payload shape consistent. WriteStringLiteral(app_data, &offset, "node"); - mesh::LocalIdentity sender(kSenderPrivateKeyHex, kSenderPublicKeyHex); - mesh::Packet packet = BuildSignedAdvertPacket(sender, 1704067201U, app_data, offset); + mesh::Packet packet = BuildSignedAdvertPacket(advert_timestamp, app_data, offset); mesh.recv(&packet); @@ -451,14 +451,14 @@ TEST(AdvertData, RejectsForgedSignatureFromNetworkPacket) { uint8_t app_data[MAX_ADVERT_DATA_SIZE] = {}; size_t offset = 0; + constexpr uint32_t advert_timestamp = 1704067201U; // flags/type byte: chat advert with a trailing name field. WriteU8(app_data, &offset, ADV_TYPE_CHAT | ADV_NAME_MASK); // name field: raw bytes for "mallory". WriteStringLiteral(app_data, &offset, "mallory"); - mesh::LocalIdentity sender(kSenderPrivateKeyHex, kSenderPublicKeyHex); - mesh::Packet packet = BuildSignedAdvertPacket(sender, 1704067201U, app_data, offset); + mesh::Packet packet = BuildSignedAdvertPacket(advert_timestamp, app_data, offset); // Corrupt the signature bytes after signing so verification must fail in Mesh::onRecvPacket(). packet.payload[PUB_KEY_SIZE + 4] ^= 0xFF; From 1ac857af78ea1ff0a2965ea5bac2fad4544dcfe8 Mon Sep 17 00:00:00 2001 From: Michael Lynch Date: Sat, 25 Apr 2026 00:42:34 +0000 Subject: [PATCH 09/17] Move advert test timestamp declarations --- test/test_helpers/test_advert_data.cpp | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/test/test_helpers/test_advert_data.cpp b/test/test_helpers/test_advert_data.cpp index 510b1c4975..396770d007 100644 --- a/test/test_helpers/test_advert_data.cpp +++ b/test/test_helpers/test_advert_data.cpp @@ -238,13 +238,13 @@ TEST(AdvertData, ParsesNameOnlyFromNetworkPacket) { uint8_t app_data[MAX_ADVERT_DATA_SIZE] = {}; size_t offset = 0; - constexpr uint32_t advert_timestamp = 1704067201U; // flags/type byte: chat advert with a trailing name field. WriteU8(app_data, &offset, ADV_TYPE_CHAT | ADV_NAME_MASK); // name field: raw bytes for "alice", consuming the rest of app_data. WriteStringLiteral(app_data, &offset, "alice"); + constexpr uint32_t advert_timestamp = 1704067201U; mesh::Packet packet = BuildSignedAdvertPacket(advert_timestamp, app_data, offset); mesh.recv(&packet); @@ -269,7 +269,6 @@ TEST(AdvertData, ParsesNameAndCoordinatesFromNetworkPacket) { uint8_t app_data[MAX_ADVERT_DATA_SIZE] = {}; size_t offset = 0; - constexpr uint32_t advert_timestamp = 1704067201U; // flags/type byte: repeater advert with lat/lon followed by a name. WriteU8(app_data, &offset, ADV_TYPE_REPEATER | ADV_LATLON_MASK | ADV_NAME_MASK); @@ -280,6 +279,7 @@ TEST(AdvertData, ParsesNameAndCoordinatesFromNetworkPacket) { // name field: raw bytes for "node" after the coordinate fields. WriteStringLiteral(app_data, &offset, "node"); + constexpr uint32_t advert_timestamp = 1704067201U; mesh::Packet packet = BuildSignedAdvertPacket(advert_timestamp, app_data, offset); mesh.recv(&packet); @@ -302,7 +302,6 @@ TEST(AdvertData, ParsesCoordinateExtremesFromNetworkPacket) { uint8_t app_data[MAX_ADVERT_DATA_SIZE] = {}; size_t offset = 0; - constexpr uint32_t advert_timestamp = 1704067201U; // flags/type byte: sensor advert with both location fields and a name. WriteU8(app_data, &offset, ADV_TYPE_SENSOR | ADV_LATLON_MASK | ADV_NAME_MASK); @@ -313,6 +312,7 @@ TEST(AdvertData, ParsesCoordinateExtremesFromNetworkPacket) { // name field: raw bytes for "edge". WriteStringLiteral(app_data, &offset, "edge"); + constexpr uint32_t advert_timestamp = 1704067201U; mesh::Packet packet = BuildSignedAdvertPacket(advert_timestamp, app_data, offset); mesh.recv(&packet); @@ -335,7 +335,6 @@ TEST(AdvertData, RejectsLongitudeOutsideValidRangeFromNetworkPacket) { uint8_t app_data[MAX_ADVERT_DATA_SIZE] = {}; size_t offset = 0; - constexpr uint32_t advert_timestamp = 1704067201U; // flags/type byte: chat advert with location and name fields present. WriteU8(app_data, &offset, ADV_TYPE_CHAT | ADV_LATLON_MASK | ADV_NAME_MASK); @@ -346,6 +345,7 @@ TEST(AdvertData, RejectsLongitudeOutsideValidRangeFromNetworkPacket) { // name field: parser should reject before the trailing name matters. WriteStringLiteral(app_data, &offset, "node"); + constexpr uint32_t advert_timestamp = 1704067201U; mesh::Packet packet = BuildSignedAdvertPacket(advert_timestamp, app_data, offset); mesh.recv(&packet); @@ -364,7 +364,6 @@ TEST(AdvertData, RejectsLongitudeBelowValidRangeFromNetworkPacket) { uint8_t app_data[MAX_ADVERT_DATA_SIZE] = {}; size_t offset = 0; - constexpr uint32_t advert_timestamp = 1704067201U; // flags/type byte: chat advert with location and name fields present. WriteU8(app_data, &offset, ADV_TYPE_CHAT | ADV_LATLON_MASK | ADV_NAME_MASK); @@ -375,6 +374,7 @@ TEST(AdvertData, RejectsLongitudeBelowValidRangeFromNetworkPacket) { // name field: included to keep the payload shape consistent. WriteStringLiteral(app_data, &offset, "node"); + constexpr uint32_t advert_timestamp = 1704067201U; mesh::Packet packet = BuildSignedAdvertPacket(advert_timestamp, app_data, offset); mesh.recv(&packet); @@ -393,7 +393,6 @@ TEST(AdvertData, RejectsLatitudeOutsideValidRangeFromNetworkPacket) { uint8_t app_data[MAX_ADVERT_DATA_SIZE] = {}; size_t offset = 0; - constexpr uint32_t advert_timestamp = 1704067201U; // flags/type byte: chat advert with location and name fields present. WriteU8(app_data, &offset, ADV_TYPE_CHAT | ADV_LATLON_MASK | ADV_NAME_MASK); @@ -404,6 +403,7 @@ TEST(AdvertData, RejectsLatitudeOutsideValidRangeFromNetworkPacket) { // name field: included to keep the payload shape consistent. WriteStringLiteral(app_data, &offset, "node"); + constexpr uint32_t advert_timestamp = 1704067201U; mesh::Packet packet = BuildSignedAdvertPacket(advert_timestamp, app_data, offset); mesh.recv(&packet); @@ -422,7 +422,6 @@ TEST(AdvertData, RejectsLatitudeBelowValidRangeFromNetworkPacket) { uint8_t app_data[MAX_ADVERT_DATA_SIZE] = {}; size_t offset = 0; - constexpr uint32_t advert_timestamp = 1704067201U; // flags/type byte: chat advert with location and name fields present. WriteU8(app_data, &offset, ADV_TYPE_CHAT | ADV_LATLON_MASK | ADV_NAME_MASK); @@ -433,6 +432,7 @@ TEST(AdvertData, RejectsLatitudeBelowValidRangeFromNetworkPacket) { // name field: included to keep the payload shape consistent. WriteStringLiteral(app_data, &offset, "node"); + constexpr uint32_t advert_timestamp = 1704067201U; mesh::Packet packet = BuildSignedAdvertPacket(advert_timestamp, app_data, offset); mesh.recv(&packet); @@ -451,13 +451,13 @@ TEST(AdvertData, RejectsForgedSignatureFromNetworkPacket) { uint8_t app_data[MAX_ADVERT_DATA_SIZE] = {}; size_t offset = 0; - constexpr uint32_t advert_timestamp = 1704067201U; // flags/type byte: chat advert with a trailing name field. WriteU8(app_data, &offset, ADV_TYPE_CHAT | ADV_NAME_MASK); // name field: raw bytes for "mallory". WriteStringLiteral(app_data, &offset, "mallory"); + constexpr uint32_t advert_timestamp = 1704067201U; mesh::Packet packet = BuildSignedAdvertPacket(advert_timestamp, app_data, offset); // Corrupt the signature bytes after signing so verification must fail in Mesh::onRecvPacket(). From ae0312e233e0fb640d5ab3490988fcdd36332e1e Mon Sep 17 00:00:00 2001 From: Michael Lynch Date: Sat, 25 Apr 2026 00:54:39 +0000 Subject: [PATCH 10/17] Refactor advert data test mesh setup --- test/test_helpers/test_advert_data.cpp | 181 +++++++++++-------------- 1 file changed, 79 insertions(+), 102 deletions(-) diff --git a/test/test_helpers/test_advert_data.cpp b/test/test_helpers/test_advert_data.cpp index 396770d007..b821bed42b 100644 --- a/test/test_helpers/test_advert_data.cpp +++ b/test/test_helpers/test_advert_data.cpp @@ -188,6 +188,31 @@ class TestChatMesh final : public BaseChatMesh { void onContactResponse(const ContactInfo&, const uint8_t*, uint8_t) override {} }; +struct TestMeshContext { + explicit TestMeshContext(uint32_t current_timestamp) + : rtc(current_timestamp), mesh(radio, ms, rng, rtc, packet_manager, tables) {} + + TestChatMesh* operator->() { + return &mesh; + } + + const TestChatMesh* operator->() const { + return &mesh; + } + + FakeRadio radio; + FakeMillis ms; + FakeRng rng; + FakeRtc rtc; + NoopPacketManager packet_manager; + SimpleMeshTables tables; + TestChatMesh mesh; +}; + +TestMeshContext MakeTestMesh(uint32_t current_timestamp) { + return TestMeshContext(current_timestamp); +} + mesh::Packet BuildSignedAdvertPacket(uint32_t timestamp, const uint8_t* app_data, uint8_t app_data_len) { mesh::LocalIdentity sender(kSenderPrivateKeyHex, kSenderPublicKeyHex); mesh::Packet packet; @@ -228,14 +253,6 @@ mesh::Packet BuildSignedAdvertPacket(uint32_t timestamp, const uint8_t* app_data } TEST(AdvertData, ParsesNameOnlyFromNetworkPacket) { - FakeRadio radio; - FakeMillis ms; - FakeRng rng; - FakeRtc rtc(1704067200U); - NoopPacketManager packet_manager; - SimpleMeshTables tables; - TestChatMesh mesh(radio, ms, rng, rtc, packet_manager, tables); - uint8_t app_data[MAX_ADVERT_DATA_SIZE] = {}; size_t offset = 0; @@ -244,29 +261,23 @@ TEST(AdvertData, ParsesNameOnlyFromNetworkPacket) { // name field: raw bytes for "alice", consuming the rest of app_data. WriteStringLiteral(app_data, &offset, "alice"); - constexpr uint32_t advert_timestamp = 1704067201U; + constexpr uint32_t current_timestamp = 1704067200U; + constexpr uint32_t advert_timestamp = current_timestamp + 1; mesh::Packet packet = BuildSignedAdvertPacket(advert_timestamp, app_data, offset); - mesh.recv(&packet); + auto test_mesh = MakeTestMesh(current_timestamp); + test_mesh->recv(&packet); - ASSERT_TRUE(mesh.discovered_contact.has_value()); - EXPECT_EQ(ADV_TYPE_CHAT, mesh.discovered_contact->type); - EXPECT_STREQ("alice", mesh.discovered_contact->name); - EXPECT_EQ(advert_timestamp, mesh.discovered_contact->last_advert_timestamp); - EXPECT_EQ(1704067200U, mesh.discovered_contact->lastmod); - EXPECT_EQ(0, mesh.discovered_contact->gps_lat); - EXPECT_EQ(0, mesh.discovered_contact->gps_lon); + ASSERT_TRUE(test_mesh->discovered_contact.has_value()); + EXPECT_EQ(ADV_TYPE_CHAT, test_mesh->discovered_contact->type); + EXPECT_STREQ("alice", test_mesh->discovered_contact->name); + EXPECT_EQ(advert_timestamp, test_mesh->discovered_contact->last_advert_timestamp); + EXPECT_EQ(current_timestamp, test_mesh->discovered_contact->lastmod); + EXPECT_EQ(0, test_mesh->discovered_contact->gps_lat); + EXPECT_EQ(0, test_mesh->discovered_contact->gps_lon); } TEST(AdvertData, ParsesNameAndCoordinatesFromNetworkPacket) { - FakeRadio radio; - FakeMillis ms; - FakeRng rng; - FakeRtc rtc(1704067200U); - NoopPacketManager packet_manager; - SimpleMeshTables tables; - TestChatMesh mesh(radio, ms, rng, rtc, packet_manager, tables); - uint8_t app_data[MAX_ADVERT_DATA_SIZE] = {}; size_t offset = 0; @@ -279,27 +290,21 @@ TEST(AdvertData, ParsesNameAndCoordinatesFromNetworkPacket) { // name field: raw bytes for "node" after the coordinate fields. WriteStringLiteral(app_data, &offset, "node"); - constexpr uint32_t advert_timestamp = 1704067201U; + constexpr uint32_t current_timestamp = 1704067200U; + constexpr uint32_t advert_timestamp = current_timestamp + 1; mesh::Packet packet = BuildSignedAdvertPacket(advert_timestamp, app_data, offset); - mesh.recv(&packet); + auto test_mesh = MakeTestMesh(current_timestamp); + test_mesh->recv(&packet); - ASSERT_TRUE(mesh.discovered_contact.has_value()); - EXPECT_EQ(ADV_TYPE_REPEATER, mesh.discovered_contact->type); - EXPECT_STREQ("node", mesh.discovered_contact->name); - EXPECT_EQ(37774900, mesh.discovered_contact->gps_lat); - EXPECT_EQ(-122419400, mesh.discovered_contact->gps_lon); + ASSERT_TRUE(test_mesh->discovered_contact.has_value()); + EXPECT_EQ(ADV_TYPE_REPEATER, test_mesh->discovered_contact->type); + EXPECT_STREQ("node", test_mesh->discovered_contact->name); + EXPECT_EQ(37774900, test_mesh->discovered_contact->gps_lat); + EXPECT_EQ(-122419400, test_mesh->discovered_contact->gps_lon); } TEST(AdvertData, ParsesCoordinateExtremesFromNetworkPacket) { - FakeRadio radio; - FakeMillis ms; - FakeRng rng; - FakeRtc rtc(1704067200U); - NoopPacketManager packet_manager; - SimpleMeshTables tables; - TestChatMesh mesh(radio, ms, rng, rtc, packet_manager, tables); - uint8_t app_data[MAX_ADVERT_DATA_SIZE] = {}; size_t offset = 0; @@ -312,27 +317,21 @@ TEST(AdvertData, ParsesCoordinateExtremesFromNetworkPacket) { // name field: raw bytes for "edge". WriteStringLiteral(app_data, &offset, "edge"); - constexpr uint32_t advert_timestamp = 1704067201U; + constexpr uint32_t current_timestamp = 1704067200U; + constexpr uint32_t advert_timestamp = current_timestamp + 1; mesh::Packet packet = BuildSignedAdvertPacket(advert_timestamp, app_data, offset); - mesh.recv(&packet); + auto test_mesh = MakeTestMesh(current_timestamp); + test_mesh->recv(&packet); - ASSERT_TRUE(mesh.discovered_contact.has_value()); - EXPECT_EQ(ADV_TYPE_SENSOR, mesh.discovered_contact->type); - EXPECT_STREQ("edge", mesh.discovered_contact->name); - EXPECT_EQ(-90000000, mesh.discovered_contact->gps_lat); - EXPECT_EQ(180000000, mesh.discovered_contact->gps_lon); + ASSERT_TRUE(test_mesh->discovered_contact.has_value()); + EXPECT_EQ(ADV_TYPE_SENSOR, test_mesh->discovered_contact->type); + EXPECT_STREQ("edge", test_mesh->discovered_contact->name); + EXPECT_EQ(-90000000, test_mesh->discovered_contact->gps_lat); + EXPECT_EQ(180000000, test_mesh->discovered_contact->gps_lon); } TEST(AdvertData, RejectsLongitudeOutsideValidRangeFromNetworkPacket) { - FakeRadio radio; - FakeMillis ms; - FakeRng rng; - FakeRtc rtc(1704067200U); - NoopPacketManager packet_manager; - SimpleMeshTables tables; - TestChatMesh mesh(radio, ms, rng, rtc, packet_manager, tables); - uint8_t app_data[MAX_ADVERT_DATA_SIZE] = {}; size_t offset = 0; @@ -345,23 +344,17 @@ TEST(AdvertData, RejectsLongitudeOutsideValidRangeFromNetworkPacket) { // name field: parser should reject before the trailing name matters. WriteStringLiteral(app_data, &offset, "node"); - constexpr uint32_t advert_timestamp = 1704067201U; + constexpr uint32_t current_timestamp = 1704067200U; + constexpr uint32_t advert_timestamp = current_timestamp + 1; mesh::Packet packet = BuildSignedAdvertPacket(advert_timestamp, app_data, offset); - mesh.recv(&packet); + auto test_mesh = MakeTestMesh(current_timestamp); + test_mesh->recv(&packet); - EXPECT_FALSE(mesh.discovered_contact.has_value()); + EXPECT_FALSE(test_mesh->discovered_contact.has_value()); } TEST(AdvertData, RejectsLongitudeBelowValidRangeFromNetworkPacket) { - FakeRadio radio; - FakeMillis ms; - FakeRng rng; - FakeRtc rtc(1704067200U); - NoopPacketManager packet_manager; - SimpleMeshTables tables; - TestChatMesh mesh(radio, ms, rng, rtc, packet_manager, tables); - uint8_t app_data[MAX_ADVERT_DATA_SIZE] = {}; size_t offset = 0; @@ -374,23 +367,17 @@ TEST(AdvertData, RejectsLongitudeBelowValidRangeFromNetworkPacket) { // name field: included to keep the payload shape consistent. WriteStringLiteral(app_data, &offset, "node"); - constexpr uint32_t advert_timestamp = 1704067201U; + constexpr uint32_t current_timestamp = 1704067200U; + constexpr uint32_t advert_timestamp = current_timestamp + 1; mesh::Packet packet = BuildSignedAdvertPacket(advert_timestamp, app_data, offset); - mesh.recv(&packet); + auto test_mesh = MakeTestMesh(current_timestamp); + test_mesh->recv(&packet); - EXPECT_FALSE(mesh.discovered_contact.has_value()); + EXPECT_FALSE(test_mesh->discovered_contact.has_value()); } TEST(AdvertData, RejectsLatitudeOutsideValidRangeFromNetworkPacket) { - FakeRadio radio; - FakeMillis ms; - FakeRng rng; - FakeRtc rtc(1704067200U); - NoopPacketManager packet_manager; - SimpleMeshTables tables; - TestChatMesh mesh(radio, ms, rng, rtc, packet_manager, tables); - uint8_t app_data[MAX_ADVERT_DATA_SIZE] = {}; size_t offset = 0; @@ -403,23 +390,17 @@ TEST(AdvertData, RejectsLatitudeOutsideValidRangeFromNetworkPacket) { // name field: included to keep the payload shape consistent. WriteStringLiteral(app_data, &offset, "node"); - constexpr uint32_t advert_timestamp = 1704067201U; + constexpr uint32_t current_timestamp = 1704067200U; + constexpr uint32_t advert_timestamp = current_timestamp + 1; mesh::Packet packet = BuildSignedAdvertPacket(advert_timestamp, app_data, offset); - mesh.recv(&packet); + auto test_mesh = MakeTestMesh(current_timestamp); + test_mesh->recv(&packet); - EXPECT_FALSE(mesh.discovered_contact.has_value()); + EXPECT_FALSE(test_mesh->discovered_contact.has_value()); } TEST(AdvertData, RejectsLatitudeBelowValidRangeFromNetworkPacket) { - FakeRadio radio; - FakeMillis ms; - FakeRng rng; - FakeRtc rtc(1704067200U); - NoopPacketManager packet_manager; - SimpleMeshTables tables; - TestChatMesh mesh(radio, ms, rng, rtc, packet_manager, tables); - uint8_t app_data[MAX_ADVERT_DATA_SIZE] = {}; size_t offset = 0; @@ -432,23 +413,17 @@ TEST(AdvertData, RejectsLatitudeBelowValidRangeFromNetworkPacket) { // name field: included to keep the payload shape consistent. WriteStringLiteral(app_data, &offset, "node"); - constexpr uint32_t advert_timestamp = 1704067201U; + constexpr uint32_t current_timestamp = 1704067200U; + constexpr uint32_t advert_timestamp = current_timestamp + 1; mesh::Packet packet = BuildSignedAdvertPacket(advert_timestamp, app_data, offset); - mesh.recv(&packet); + auto test_mesh = MakeTestMesh(current_timestamp); + test_mesh->recv(&packet); - EXPECT_FALSE(mesh.discovered_contact.has_value()); + EXPECT_FALSE(test_mesh->discovered_contact.has_value()); } TEST(AdvertData, RejectsForgedSignatureFromNetworkPacket) { - FakeRadio radio; - FakeMillis ms; - FakeRng rng; - FakeRtc rtc(1704067200U); - NoopPacketManager packet_manager; - SimpleMeshTables tables; - TestChatMesh mesh(radio, ms, rng, rtc, packet_manager, tables); - uint8_t app_data[MAX_ADVERT_DATA_SIZE] = {}; size_t offset = 0; @@ -457,15 +432,17 @@ TEST(AdvertData, RejectsForgedSignatureFromNetworkPacket) { // name field: raw bytes for "mallory". WriteStringLiteral(app_data, &offset, "mallory"); - constexpr uint32_t advert_timestamp = 1704067201U; + constexpr uint32_t current_timestamp = 1704067200U; + constexpr uint32_t advert_timestamp = current_timestamp + 1; mesh::Packet packet = BuildSignedAdvertPacket(advert_timestamp, app_data, offset); // Corrupt the signature bytes after signing so verification must fail in Mesh::onRecvPacket(). packet.payload[PUB_KEY_SIZE + 4] ^= 0xFF; - mesh.recv(&packet); + auto test_mesh = MakeTestMesh(current_timestamp); + test_mesh->recv(&packet); - EXPECT_FALSE(mesh.discovered_contact.has_value()); + EXPECT_FALSE(test_mesh->discovered_contact.has_value()); } } // namespace From 339754c644fa6677e82066e7cfb10b512578b77c Mon Sep 17 00:00:00 2001 From: Michael Lynch Date: Sat, 25 Apr 2026 00:57:29 +0000 Subject: [PATCH 11/17] Clean up advert data test names --- test/test_helpers/test_advert_data.cpp | 48 +++++++------------------- 1 file changed, 13 insertions(+), 35 deletions(-) diff --git a/test/test_helpers/test_advert_data.cpp b/test/test_helpers/test_advert_data.cpp index b821bed42b..a40ba573e9 100644 --- a/test/test_helpers/test_advert_data.cpp +++ b/test/test_helpers/test_advert_data.cpp @@ -258,8 +258,8 @@ TEST(AdvertData, ParsesNameOnlyFromNetworkPacket) { // flags/type byte: chat advert with a trailing name field. WriteU8(app_data, &offset, ADV_TYPE_CHAT | ADV_NAME_MASK); - // name field: raw bytes for "alice", consuming the rest of app_data. - WriteStringLiteral(app_data, &offset, "alice"); + // name field: raw bytes for "dummy-node-name", consuming the rest of app_data. + WriteStringLiteral(app_data, &offset, "dummy-node-name"); constexpr uint32_t current_timestamp = 1704067200U; constexpr uint32_t advert_timestamp = current_timestamp + 1; @@ -270,7 +270,7 @@ TEST(AdvertData, ParsesNameOnlyFromNetworkPacket) { ASSERT_TRUE(test_mesh->discovered_contact.has_value()); EXPECT_EQ(ADV_TYPE_CHAT, test_mesh->discovered_contact->type); - EXPECT_STREQ("alice", test_mesh->discovered_contact->name); + EXPECT_STREQ("dummy-node-name", test_mesh->discovered_contact->name); EXPECT_EQ(advert_timestamp, test_mesh->discovered_contact->last_advert_timestamp); EXPECT_EQ(current_timestamp, test_mesh->discovered_contact->lastmod); EXPECT_EQ(0, test_mesh->discovered_contact->gps_lat); @@ -287,8 +287,8 @@ TEST(AdvertData, ParsesNameAndCoordinatesFromNetworkPacket) { WriteI32Le(app_data, &offset, 37774900); // longitude field: signed little-endian microdegrees for -122.4194. WriteI32Le(app_data, &offset, -122419400); - // name field: raw bytes for "node" after the coordinate fields. - WriteStringLiteral(app_data, &offset, "node"); + // name field: trailing contact name bytes after the coordinate fields. + WriteStringLiteral(app_data, &offset, "dummy-node-name"); constexpr uint32_t current_timestamp = 1704067200U; constexpr uint32_t advert_timestamp = current_timestamp + 1; @@ -299,7 +299,7 @@ TEST(AdvertData, ParsesNameAndCoordinatesFromNetworkPacket) { ASSERT_TRUE(test_mesh->discovered_contact.has_value()); EXPECT_EQ(ADV_TYPE_REPEATER, test_mesh->discovered_contact->type); - EXPECT_STREQ("node", test_mesh->discovered_contact->name); + EXPECT_STREQ("dummy-node-name", test_mesh->discovered_contact->name); EXPECT_EQ(37774900, test_mesh->discovered_contact->gps_lat); EXPECT_EQ(-122419400, test_mesh->discovered_contact->gps_lon); } @@ -314,8 +314,8 @@ TEST(AdvertData, ParsesCoordinateExtremesFromNetworkPacket) { WriteI32Le(app_data, &offset, -90000000); // longitude field: maximum supported longitude, 180.000000 degrees. WriteI32Le(app_data, &offset, 180000000); - // name field: raw bytes for "edge". - WriteStringLiteral(app_data, &offset, "edge"); + // name field: raw bytes for "dummy-node-name". + WriteStringLiteral(app_data, &offset, "dummy-node-name"); constexpr uint32_t current_timestamp = 1704067200U; constexpr uint32_t advert_timestamp = current_timestamp + 1; @@ -326,7 +326,7 @@ TEST(AdvertData, ParsesCoordinateExtremesFromNetworkPacket) { ASSERT_TRUE(test_mesh->discovered_contact.has_value()); EXPECT_EQ(ADV_TYPE_SENSOR, test_mesh->discovered_contact->type); - EXPECT_STREQ("edge", test_mesh->discovered_contact->name); + EXPECT_STREQ("dummy-node-name", test_mesh->discovered_contact->name); EXPECT_EQ(-90000000, test_mesh->discovered_contact->gps_lat); EXPECT_EQ(180000000, test_mesh->discovered_contact->gps_lon); } @@ -342,7 +342,7 @@ TEST(AdvertData, RejectsLongitudeOutsideValidRangeFromNetworkPacket) { // longitude field: one microdegree above +180.0, which is invalid. WriteI32Le(app_data, &offset, 180000001); // name field: parser should reject before the trailing name matters. - WriteStringLiteral(app_data, &offset, "node"); + WriteStringLiteral(app_data, &offset, "dummy-node-name"); constexpr uint32_t current_timestamp = 1704067200U; constexpr uint32_t advert_timestamp = current_timestamp + 1; @@ -365,7 +365,7 @@ TEST(AdvertData, RejectsLongitudeBelowValidRangeFromNetworkPacket) { // longitude field: one microdegree below -180.0, which is invalid. WriteI32Le(app_data, &offset, -180000001); // name field: included to keep the payload shape consistent. - WriteStringLiteral(app_data, &offset, "node"); + WriteStringLiteral(app_data, &offset, "dummy-node-name"); constexpr uint32_t current_timestamp = 1704067200U; constexpr uint32_t advert_timestamp = current_timestamp + 1; @@ -388,7 +388,7 @@ TEST(AdvertData, RejectsLatitudeOutsideValidRangeFromNetworkPacket) { // longitude field: valid longitude so the failure comes from latitude. WriteI32Le(app_data, &offset, -122419400); // name field: included to keep the payload shape consistent. - WriteStringLiteral(app_data, &offset, "node"); + WriteStringLiteral(app_data, &offset, "dummy-node-name"); constexpr uint32_t current_timestamp = 1704067200U; constexpr uint32_t advert_timestamp = current_timestamp + 1; @@ -411,7 +411,7 @@ TEST(AdvertData, RejectsLatitudeBelowValidRangeFromNetworkPacket) { // longitude field: valid longitude so the failure comes from latitude. WriteI32Le(app_data, &offset, -122419400); // name field: included to keep the payload shape consistent. - WriteStringLiteral(app_data, &offset, "node"); + WriteStringLiteral(app_data, &offset, "dummy-node-name"); constexpr uint32_t current_timestamp = 1704067200U; constexpr uint32_t advert_timestamp = current_timestamp + 1; @@ -423,26 +423,4 @@ TEST(AdvertData, RejectsLatitudeBelowValidRangeFromNetworkPacket) { EXPECT_FALSE(test_mesh->discovered_contact.has_value()); } -TEST(AdvertData, RejectsForgedSignatureFromNetworkPacket) { - uint8_t app_data[MAX_ADVERT_DATA_SIZE] = {}; - size_t offset = 0; - - // flags/type byte: chat advert with a trailing name field. - WriteU8(app_data, &offset, ADV_TYPE_CHAT | ADV_NAME_MASK); - // name field: raw bytes for "mallory". - WriteStringLiteral(app_data, &offset, "mallory"); - - constexpr uint32_t current_timestamp = 1704067200U; - constexpr uint32_t advert_timestamp = current_timestamp + 1; - mesh::Packet packet = BuildSignedAdvertPacket(advert_timestamp, app_data, offset); - - // Corrupt the signature bytes after signing so verification must fail in Mesh::onRecvPacket(). - packet.payload[PUB_KEY_SIZE + 4] ^= 0xFF; - - auto test_mesh = MakeTestMesh(current_timestamp); - test_mesh->recv(&packet); - - EXPECT_FALSE(test_mesh->discovered_contact.has_value()); -} - } // namespace From ac10c13337842e72c08da84c7d34ea10fe1aa30d Mon Sep 17 00:00:00 2001 From: Michael Lynch Date: Sat, 25 Apr 2026 01:18:30 +0000 Subject: [PATCH 12/17] Add sanitizer coverage for advert GPS parsing --- .github/workflows/run-unit-tests.yml | 5 + lib/ed25519/library.json | 7 + platformio.ini | 6 + src/helpers/AdvertDataHelpers.cpp | 18 ++- test/test_helpers/test_advert_data.cpp | 207 ++++++++++++++++++++++--- 5 files changed, 216 insertions(+), 27 deletions(-) create mode 100644 lib/ed25519/library.json diff --git a/.github/workflows/run-unit-tests.yml b/.github/workflows/run-unit-tests.yml index e3af3aafbf..dd03e8f954 100644 --- a/.github/workflows/run-unit-tests.yml +++ b/.github/workflows/run-unit-tests.yml @@ -21,6 +21,11 @@ jobs: - name: Run Unit Tests run: pio test -e native -vv + env: + # Fail CI on leaks or memory errors reported by ASAN. + ASAN_OPTIONS: detect_leaks=1:halt_on_error=1 + # Stop immediately and print a stack trace for undefined behavior. + UBSAN_OPTIONS: halt_on_error=1:print_stacktrace=1 - name: Upload Test Results # Upload test results even if the test step failed. diff --git a/lib/ed25519/library.json b/lib/ed25519/library.json new file mode 100644 index 0000000000..4cafd5e89e --- /dev/null +++ b/lib/ed25519/library.json @@ -0,0 +1,7 @@ +{ + "build": { + "flags": [ + "-fno-sanitize=undefined" + ] + } +} diff --git a/platformio.ini b/platformio.ini index 8e30c804ac..9696fc3f83 100644 --- a/platformio.ini +++ b/platformio.ini @@ -158,6 +158,12 @@ lib_deps = [env:native] platform = native build_flags = -std=c++17 + ; Preserve stack frames and debug info so ASAN/UBSAN reports are actionable. + -g + -fno-omit-frame-pointer + ; Run native unit tests with AddressSanitizer and UndefinedBehaviorSanitizer. + -fsanitize=address + -fsanitize=undefined -I src -I test/mocks test_build_src = yes diff --git a/src/helpers/AdvertDataHelpers.cpp b/src/helpers/AdvertDataHelpers.cpp index 0802cac952..9d5e344ff0 100644 --- a/src/helpers/AdvertDataHelpers.cpp +++ b/src/helpers/AdvertDataHelpers.cpp @@ -29,12 +29,20 @@ AdvertDataParser::AdvertDataParser(const uint8_t app_data[], uint8_t app_data_len) { _name[0] = 0; _lat = _lon = 0; - _flags = app_data[0]; + _flags = 0; _valid = false; _extra1 = _extra2 = 0; - + + if (app_data_len < 1) { + return; + } + + _flags = app_data[0]; int i = 1; if (_flags & ADV_LATLON_MASK) { + if (app_data_len < i + 8) { + return; + } memcpy(&_lat, &app_data[i], 4); i += 4; memcpy(&_lon, &app_data[i], 4); i += 4; if (_lat < -90000000 || _lat > 90000000 || _lon < -180000000 || _lon > 180000000) { @@ -42,9 +50,15 @@ } } if (_flags & ADV_FEAT1_MASK) { + if (app_data_len < i + 2) { + return; + } memcpy(&_extra1, &app_data[i], 2); i += 2; } if (_flags & ADV_FEAT2_MASK) { + if (app_data_len < i + 2) { + return; + } memcpy(&_extra2, &app_data[i], 2); i += 2; } diff --git a/test/test_helpers/test_advert_data.cpp b/test/test_helpers/test_advert_data.cpp index a40ba573e9..af0cb9d515 100644 --- a/test/test_helpers/test_advert_data.cpp +++ b/test/test_helpers/test_advert_data.cpp @@ -213,6 +213,19 @@ TestMeshContext MakeTestMesh(uint32_t current_timestamp) { return TestMeshContext(current_timestamp); } +ContactInfo MakeSenderContact(uint32_t advert_timestamp, int32_t gps_lat, int32_t gps_lon) { + ContactInfo contact = {}; + contact.id = mesh::Identity(kSenderPublicKeyHex); + strcpy(contact.name, "existing-contact"); + contact.type = ADV_TYPE_CHAT; + contact.out_path_len = OUT_PATH_UNKNOWN; + contact.last_advert_timestamp = advert_timestamp; + contact.lastmod = advert_timestamp; + contact.gps_lat = gps_lat; + contact.gps_lon = gps_lon; + return contact; +} + mesh::Packet BuildSignedAdvertPacket(uint32_t timestamp, const uint8_t* app_data, uint8_t app_data_len) { mesh::LocalIdentity sender(kSenderPrivateKeyHex, kSenderPublicKeyHex); mesh::Packet packet; @@ -252,31 +265,6 @@ mesh::Packet BuildSignedAdvertPacket(uint32_t timestamp, const uint8_t* app_data return packet; } -TEST(AdvertData, ParsesNameOnlyFromNetworkPacket) { - uint8_t app_data[MAX_ADVERT_DATA_SIZE] = {}; - size_t offset = 0; - - // flags/type byte: chat advert with a trailing name field. - WriteU8(app_data, &offset, ADV_TYPE_CHAT | ADV_NAME_MASK); - // name field: raw bytes for "dummy-node-name", consuming the rest of app_data. - WriteStringLiteral(app_data, &offset, "dummy-node-name"); - - constexpr uint32_t current_timestamp = 1704067200U; - constexpr uint32_t advert_timestamp = current_timestamp + 1; - mesh::Packet packet = BuildSignedAdvertPacket(advert_timestamp, app_data, offset); - - auto test_mesh = MakeTestMesh(current_timestamp); - test_mesh->recv(&packet); - - ASSERT_TRUE(test_mesh->discovered_contact.has_value()); - EXPECT_EQ(ADV_TYPE_CHAT, test_mesh->discovered_contact->type); - EXPECT_STREQ("dummy-node-name", test_mesh->discovered_contact->name); - EXPECT_EQ(advert_timestamp, test_mesh->discovered_contact->last_advert_timestamp); - EXPECT_EQ(current_timestamp, test_mesh->discovered_contact->lastmod); - EXPECT_EQ(0, test_mesh->discovered_contact->gps_lat); - EXPECT_EQ(0, test_mesh->discovered_contact->gps_lon); -} - TEST(AdvertData, ParsesNameAndCoordinatesFromNetworkPacket) { uint8_t app_data[MAX_ADVERT_DATA_SIZE] = {}; size_t offset = 0; @@ -331,6 +319,52 @@ TEST(AdvertData, ParsesCoordinateExtremesFromNetworkPacket) { EXPECT_EQ(180000000, test_mesh->discovered_contact->gps_lon); } +TEST(AdvertData, ParsesPositiveLatitudeAndNegativeLongitudeBoundariesFromNetworkPacket) { + uint8_t app_data[MAX_ADVERT_DATA_SIZE] = {}; + size_t offset = 0; + + // flags/type byte: sensor advert with both location fields and a name. + WriteU8(app_data, &offset, ADV_TYPE_SENSOR | ADV_LATLON_MASK | ADV_NAME_MASK); + // latitude field: maximum supported latitude, +90.000000 degrees. + WriteI32Le(app_data, &offset, 90000000); + // longitude field: minimum supported longitude, -180.000000 degrees. + WriteI32Le(app_data, &offset, -180000000); + WriteStringLiteral(app_data, &offset, "dummy-node-name"); + + constexpr uint32_t current_timestamp = 1704067200U; + constexpr uint32_t advert_timestamp = current_timestamp + 1; + mesh::Packet packet = BuildSignedAdvertPacket(advert_timestamp, app_data, offset); + + auto test_mesh = MakeTestMesh(current_timestamp); + test_mesh->recv(&packet); + + ASSERT_TRUE(test_mesh->discovered_contact.has_value()); + EXPECT_EQ(90000000, test_mesh->discovered_contact->gps_lat); + EXPECT_EQ(-180000000, test_mesh->discovered_contact->gps_lon); +} + +TEST(AdvertData, ParsesNullIslandCoordinatesFromNetworkPacket) { + uint8_t app_data[MAX_ADVERT_DATA_SIZE] = {}; + size_t offset = 0; + + // flags/type byte: chat advert with zero-valued coordinates and a name. + WriteU8(app_data, &offset, ADV_TYPE_CHAT | ADV_LATLON_MASK | ADV_NAME_MASK); + WriteI32Le(app_data, &offset, 0); + WriteI32Le(app_data, &offset, 0); + WriteStringLiteral(app_data, &offset, "dummy-node-name"); + + constexpr uint32_t current_timestamp = 1704067200U; + constexpr uint32_t advert_timestamp = current_timestamp + 1; + mesh::Packet packet = BuildSignedAdvertPacket(advert_timestamp, app_data, offset); + + auto test_mesh = MakeTestMesh(current_timestamp); + test_mesh->recv(&packet); + + ASSERT_TRUE(test_mesh->discovered_contact.has_value()); + EXPECT_EQ(0, test_mesh->discovered_contact->gps_lat); + EXPECT_EQ(0, test_mesh->discovered_contact->gps_lon); +} + TEST(AdvertData, RejectsLongitudeOutsideValidRangeFromNetworkPacket) { uint8_t app_data[MAX_ADVERT_DATA_SIZE] = {}; size_t offset = 0; @@ -377,6 +411,42 @@ TEST(AdvertData, RejectsLongitudeBelowValidRangeFromNetworkPacket) { EXPECT_FALSE(test_mesh->discovered_contact.has_value()); } +void ExpectTruncatedGpsPayloadIsRejected(uint8_t app_data_len) { + uint8_t app_data[MAX_ADVERT_DATA_SIZE] = {}; + size_t offset = 0; + + // Advert claims to carry GPS coordinates and a name, but the payload is truncated. + WriteU8(app_data, &offset, ADV_TYPE_CHAT | ADV_LATLON_MASK | ADV_NAME_MASK); + WriteI32Le(app_data, &offset, 37774900); + WriteI32Le(app_data, &offset, -122419400); + WriteStringLiteral(app_data, &offset, "dummy-node-name"); + + constexpr uint32_t current_timestamp = 1704067200U; + constexpr uint32_t advert_timestamp = current_timestamp + 1; + mesh::Packet packet = BuildSignedAdvertPacket(advert_timestamp, app_data, app_data_len); + + auto test_mesh = MakeTestMesh(current_timestamp); + test_mesh->recv(&packet); + + EXPECT_FALSE(test_mesh->discovered_contact.has_value()); +} + +TEST(AdvertData, RejectsGpsPayloadWithMissingFlagsByte) { + ExpectTruncatedGpsPayloadIsRejected(0); +} + +TEST(AdvertData, RejectsGpsPayloadWithOnlyFlagsByte) { + ExpectTruncatedGpsPayloadIsRejected(1); +} + +TEST(AdvertData, RejectsGpsPayloadWithLatitudeButMissingLongitude) { + ExpectTruncatedGpsPayloadIsRejected(5); +} + +TEST(AdvertData, RejectsGpsPayloadOneByteShortOfFullCoordinates) { + ExpectTruncatedGpsPayloadIsRejected(8); +} + TEST(AdvertData, RejectsLatitudeOutsideValidRangeFromNetworkPacket) { uint8_t app_data[MAX_ADVERT_DATA_SIZE] = {}; size_t offset = 0; @@ -423,4 +493,91 @@ TEST(AdvertData, RejectsLatitudeBelowValidRangeFromNetworkPacket) { EXPECT_FALSE(test_mesh->discovered_contact.has_value()); } +TEST(AdvertData, KeepsExistingGpsWhenUpdatedAdvertOmitsCoordinates) { + uint8_t app_data[MAX_ADVERT_DATA_SIZE] = {}; + size_t offset = 0; + + // flags/type byte: chat advert with a new name but no GPS fields. + WriteU8(app_data, &offset, ADV_TYPE_CHAT | ADV_NAME_MASK); + WriteStringLiteral(app_data, &offset, "updated-name"); + + constexpr uint32_t current_timestamp = 1704067200U; + constexpr uint32_t existing_advert_timestamp = current_timestamp - 10; + constexpr uint32_t new_advert_timestamp = current_timestamp + 1; + mesh::Packet packet = BuildSignedAdvertPacket(new_advert_timestamp, app_data, offset); + + auto test_mesh = MakeTestMesh(current_timestamp); + ASSERT_TRUE(test_mesh->addContact(MakeSenderContact(existing_advert_timestamp, 37774900, -122419400))); + + test_mesh->recv(&packet); + + ContactInfo* updated = test_mesh->lookupContactByPubKey(mesh::Identity(kSenderPublicKeyHex).pub_key, PUB_KEY_SIZE); + ASSERT_NE(nullptr, updated); + EXPECT_STREQ("updated-name", updated->name); + EXPECT_EQ(37774900, updated->gps_lat); + EXPECT_EQ(-122419400, updated->gps_lon); + ASSERT_TRUE(test_mesh->discovered_contact.has_value()); + EXPECT_EQ(37774900, test_mesh->discovered_contact->gps_lat); + EXPECT_EQ(-122419400, test_mesh->discovered_contact->gps_lon); +} + +TEST(AdvertData, OverwritesExistingGpsWhenUpdatedAdvertIncludesCoordinates) { + uint8_t app_data[MAX_ADVERT_DATA_SIZE] = {}; + size_t offset = 0; + + // flags/type byte: chat advert with replacement GPS coordinates and a new name. + WriteU8(app_data, &offset, ADV_TYPE_CHAT | ADV_LATLON_MASK | ADV_NAME_MASK); + WriteI32Le(app_data, &offset, 40712800); + WriteI32Le(app_data, &offset, -74006000); + WriteStringLiteral(app_data, &offset, "updated-name"); + + constexpr uint32_t current_timestamp = 1704067200U; + constexpr uint32_t existing_advert_timestamp = current_timestamp - 10; + constexpr uint32_t new_advert_timestamp = current_timestamp + 1; + mesh::Packet packet = BuildSignedAdvertPacket(new_advert_timestamp, app_data, offset); + + auto test_mesh = MakeTestMesh(current_timestamp); + ASSERT_TRUE(test_mesh->addContact(MakeSenderContact(existing_advert_timestamp, 37774900, -122419400))); + + test_mesh->recv(&packet); + + ContactInfo* updated = test_mesh->lookupContactByPubKey(mesh::Identity(kSenderPublicKeyHex).pub_key, PUB_KEY_SIZE); + ASSERT_NE(nullptr, updated); + EXPECT_STREQ("updated-name", updated->name); + EXPECT_EQ(40712800, updated->gps_lat); + EXPECT_EQ(-74006000, updated->gps_lon); + ASSERT_TRUE(test_mesh->discovered_contact.has_value()); + EXPECT_EQ(40712800, test_mesh->discovered_contact->gps_lat); + EXPECT_EQ(-74006000, test_mesh->discovered_contact->gps_lon); +} + +TEST(AdvertData, LeavesExistingGpsUntouchedWhenUpdatedAdvertHasInvalidCoordinates) { + uint8_t app_data[MAX_ADVERT_DATA_SIZE] = {}; + size_t offset = 0; + + // flags/type byte: chat advert with invalid longitude and a new name. + WriteU8(app_data, &offset, ADV_TYPE_CHAT | ADV_LATLON_MASK | ADV_NAME_MASK); + WriteI32Le(app_data, &offset, 37774900); + WriteI32Le(app_data, &offset, 180000001); + WriteStringLiteral(app_data, &offset, "updated-name"); + + constexpr uint32_t current_timestamp = 1704067200U; + constexpr uint32_t existing_advert_timestamp = current_timestamp - 10; + constexpr uint32_t new_advert_timestamp = current_timestamp + 1; + mesh::Packet packet = BuildSignedAdvertPacket(new_advert_timestamp, app_data, offset); + + auto test_mesh = MakeTestMesh(current_timestamp); + ASSERT_TRUE(test_mesh->addContact(MakeSenderContact(existing_advert_timestamp, 37774900, -122419400))); + + test_mesh->recv(&packet); + + ContactInfo* existing = test_mesh->lookupContactByPubKey(mesh::Identity(kSenderPublicKeyHex).pub_key, PUB_KEY_SIZE); + ASSERT_NE(nullptr, existing); + EXPECT_STREQ("existing-contact", existing->name); + EXPECT_EQ(37774900, existing->gps_lat); + EXPECT_EQ(-122419400, existing->gps_lon); + EXPECT_EQ(existing_advert_timestamp, existing->last_advert_timestamp); + EXPECT_FALSE(test_mesh->discovered_contact.has_value()); +} + } // namespace From 5b2b577c6a539d292bb8a17db63c8e1f63345195 Mon Sep 17 00:00:00 2001 From: Michael Lynch Date: Sat, 25 Apr 2026 01:29:20 +0000 Subject: [PATCH 13/17] Clarify native mocks and advert truncation tests --- test/mocks/Arduino.h | 2 + test/mocks/Ed25519.h | 2 + test/mocks/SHA256.h | 2 + test/mocks/Stream.h | 2 + test/test_helpers/test_advert_data.cpp | 86 +++++++++++++++++++++----- 5 files changed, 78 insertions(+), 16 deletions(-) diff --git a/test/mocks/Arduino.h b/test/mocks/Arduino.h index d35b9fb9ef..b7acefbc2a 100644 --- a/test/mocks/Arduino.h +++ b/test/mocks/Arduino.h @@ -6,6 +6,8 @@ #include #include +// Mock Arduino compatibility header for native testing. +// Provides the small libc-backed helpers needed by Arduino-oriented code. inline char* ltoa(long value, char* buffer, int base) { if (base == 10) { snprintf(buffer, 32, "%ld", value); diff --git a/test/mocks/Ed25519.h b/test/mocks/Ed25519.h index 1a57f5d1ea..2c649505c3 100644 --- a/test/mocks/Ed25519.h +++ b/test/mocks/Ed25519.h @@ -10,6 +10,8 @@ extern "C" { #include +// Mock Ed25519 wrapper for native testing. +// Adapts the test-only C ed25519 implementation to the Arduino-style class API. class Ed25519 { public: static bool verify(const uint8_t* sig, const uint8_t* pub_key, const uint8_t* message, int msg_len) { diff --git a/test/mocks/SHA256.h b/test/mocks/SHA256.h index 0c783dc02a..38e779b7d1 100644 --- a/test/mocks/SHA256.h +++ b/test/mocks/SHA256.h @@ -4,6 +4,8 @@ #include #include +// Mock SHA256 class for native testing. +// Provides a deterministic stand-in so code can hash data without Arduino crypto deps. class SHA256 { uint32_t state_ = 2166136261u; diff --git a/test/mocks/Stream.h b/test/mocks/Stream.h index 5cc399eb3e..e83b04294e 100644 --- a/test/mocks/Stream.h +++ b/test/mocks/Stream.h @@ -3,6 +3,8 @@ #include #include +// Mock Stream class for native testing. +// Provides the minimal interface needed by code that depends on Arduino Stream. class Stream { public: virtual size_t readBytes(uint8_t*, size_t) { return 0; } diff --git a/test/test_helpers/test_advert_data.cpp b/test/test_helpers/test_advert_data.cpp index af0cb9d515..83daa4288b 100644 --- a/test/test_helpers/test_advert_data.cpp +++ b/test/test_helpers/test_advert_data.cpp @@ -329,6 +329,7 @@ TEST(AdvertData, ParsesPositiveLatitudeAndNegativeLongitudeBoundariesFromNetwork WriteI32Le(app_data, &offset, 90000000); // longitude field: minimum supported longitude, -180.000000 degrees. WriteI32Le(app_data, &offset, -180000000); + // name field: raw bytes for "dummy-node-name". WriteStringLiteral(app_data, &offset, "dummy-node-name"); constexpr uint32_t current_timestamp = 1704067200U; @@ -349,8 +350,11 @@ TEST(AdvertData, ParsesNullIslandCoordinatesFromNetworkPacket) { // flags/type byte: chat advert with zero-valued coordinates and a name. WriteU8(app_data, &offset, ADV_TYPE_CHAT | ADV_LATLON_MASK | ADV_NAME_MASK); + // latitude field: Null Island latitude at exactly 0.000000 degrees. WriteI32Le(app_data, &offset, 0); + // longitude field: Null Island longitude at exactly 0.000000 degrees. WriteI32Le(app_data, &offset, 0); + // name field: raw bytes for "dummy-node-name". WriteStringLiteral(app_data, &offset, "dummy-node-name"); constexpr uint32_t current_timestamp = 1704067200U; @@ -411,19 +415,15 @@ TEST(AdvertData, RejectsLongitudeBelowValidRangeFromNetworkPacket) { EXPECT_FALSE(test_mesh->discovered_contact.has_value()); } -void ExpectTruncatedGpsPayloadIsRejected(uint8_t app_data_len) { - uint8_t app_data[MAX_ADVERT_DATA_SIZE] = {}; +TEST(AdvertData, RejectsGpsPayloadWithMissingFlagsByte) { + // Backing storage is unused because the advertised app_data length is zero. + uint8_t app_data[1] = {}; size_t offset = 0; - // Advert claims to carry GPS coordinates and a name, but the payload is truncated. - WriteU8(app_data, &offset, ADV_TYPE_CHAT | ADV_LATLON_MASK | ADV_NAME_MASK); - WriteI32Le(app_data, &offset, 37774900); - WriteI32Le(app_data, &offset, -122419400); - WriteStringLiteral(app_data, &offset, "dummy-node-name"); - constexpr uint32_t current_timestamp = 1704067200U; constexpr uint32_t advert_timestamp = current_timestamp + 1; - mesh::Packet packet = BuildSignedAdvertPacket(advert_timestamp, app_data, app_data_len); + // Leave the app_data length at zero so the parser never sees the flags byte. + mesh::Packet packet = BuildSignedAdvertPacket(advert_timestamp, app_data, offset); auto test_mesh = MakeTestMesh(current_timestamp); test_mesh->recv(&packet); @@ -431,20 +431,67 @@ void ExpectTruncatedGpsPayloadIsRejected(uint8_t app_data_len) { EXPECT_FALSE(test_mesh->discovered_contact.has_value()); } -TEST(AdvertData, RejectsGpsPayloadWithMissingFlagsByte) { - ExpectTruncatedGpsPayloadIsRejected(0); -} - TEST(AdvertData, RejectsGpsPayloadWithOnlyFlagsByte) { - ExpectTruncatedGpsPayloadIsRejected(1); + uint8_t app_data[1] = {}; + size_t offset = 0; + + // flags/type byte: chat advert that claims to carry coordinates and a name. + WriteU8(app_data, &offset, ADV_TYPE_CHAT | ADV_LATLON_MASK | ADV_NAME_MASK); + + constexpr uint32_t current_timestamp = 1704067200U; + constexpr uint32_t advert_timestamp = current_timestamp + 1; + // Pass only the flags byte so no latitude bytes remain. + mesh::Packet packet = BuildSignedAdvertPacket(advert_timestamp, app_data, offset); + + auto test_mesh = MakeTestMesh(current_timestamp); + test_mesh->recv(&packet); + + EXPECT_FALSE(test_mesh->discovered_contact.has_value()); } TEST(AdvertData, RejectsGpsPayloadWithLatitudeButMissingLongitude) { - ExpectTruncatedGpsPayloadIsRejected(5); + uint8_t app_data[5] = {}; + size_t offset = 0; + + // flags/type byte: chat advert that claims to carry coordinates and a name. + WriteU8(app_data, &offset, ADV_TYPE_CHAT | ADV_LATLON_MASK | ADV_NAME_MASK); + // latitude field: complete latitude bytes are present before truncation. + WriteI32Le(app_data, &offset, 37774900); + + constexpr uint32_t current_timestamp = 1704067200U; + constexpr uint32_t advert_timestamp = current_timestamp + 1; + // Pass only the flags byte and latitude field so longitude is missing. + mesh::Packet packet = BuildSignedAdvertPacket(advert_timestamp, app_data, offset); + + auto test_mesh = MakeTestMesh(current_timestamp); + test_mesh->recv(&packet); + + EXPECT_FALSE(test_mesh->discovered_contact.has_value()); } TEST(AdvertData, RejectsGpsPayloadOneByteShortOfFullCoordinates) { - ExpectTruncatedGpsPayloadIsRejected(8); + uint8_t app_data[8] = {}; + size_t offset = 0; + uint8_t lon_bytes[sizeof(int32_t)] = {}; + size_t lon_offset = 0; + + // flags/type byte: chat advert that claims to carry coordinates and a name. + WriteU8(app_data, &offset, ADV_TYPE_CHAT | ADV_LATLON_MASK | ADV_NAME_MASK); + // latitude field: complete latitude bytes are present before truncation. + WriteI32Le(app_data, &offset, 37774900); + // longitude field: only the first three longitude bytes are present before truncation. + WriteI32Le(lon_bytes, &lon_offset, -122419400); + WriteBytes(app_data, &offset, lon_bytes, 3); + + constexpr uint32_t current_timestamp = 1704067200U; + constexpr uint32_t advert_timestamp = current_timestamp + 1; + // Pass only the flags byte, latitude field, and three longitude bytes. + mesh::Packet packet = BuildSignedAdvertPacket(advert_timestamp, app_data, offset); + + auto test_mesh = MakeTestMesh(current_timestamp); + test_mesh->recv(&packet); + + EXPECT_FALSE(test_mesh->discovered_contact.has_value()); } TEST(AdvertData, RejectsLatitudeOutsideValidRangeFromNetworkPacket) { @@ -499,6 +546,7 @@ TEST(AdvertData, KeepsExistingGpsWhenUpdatedAdvertOmitsCoordinates) { // flags/type byte: chat advert with a new name but no GPS fields. WriteU8(app_data, &offset, ADV_TYPE_CHAT | ADV_NAME_MASK); + // name field: replacement contact name with no coordinate payload following it. WriteStringLiteral(app_data, &offset, "updated-name"); constexpr uint32_t current_timestamp = 1704067200U; @@ -527,8 +575,11 @@ TEST(AdvertData, OverwritesExistingGpsWhenUpdatedAdvertIncludesCoordinates) { // flags/type byte: chat advert with replacement GPS coordinates and a new name. WriteU8(app_data, &offset, ADV_TYPE_CHAT | ADV_LATLON_MASK | ADV_NAME_MASK); + // latitude field: replacement latitude for 40.712800 degrees. WriteI32Le(app_data, &offset, 40712800); + // longitude field: replacement longitude for -74.006000 degrees. WriteI32Le(app_data, &offset, -74006000); + // name field: replacement contact name applied with the new coordinates. WriteStringLiteral(app_data, &offset, "updated-name"); constexpr uint32_t current_timestamp = 1704067200U; @@ -557,8 +608,11 @@ TEST(AdvertData, LeavesExistingGpsUntouchedWhenUpdatedAdvertHasInvalidCoordinate // flags/type byte: chat advert with invalid longitude and a new name. WriteU8(app_data, &offset, ADV_TYPE_CHAT | ADV_LATLON_MASK | ADV_NAME_MASK); + // latitude field: valid latitude so the update failure comes from longitude. WriteI32Le(app_data, &offset, 37774900); + // longitude field: one microdegree above +180.0, which should reject the update. WriteI32Le(app_data, &offset, 180000001); + // name field: replacement name that should not be applied when parsing fails. WriteStringLiteral(app_data, &offset, "updated-name"); constexpr uint32_t current_timestamp = 1704067200U; From 5a9c0a2a21bfb819875d1bbffad66a89aa71d14a Mon Sep 17 00:00:00 2001 From: Michael Lynch Date: Fri, 1 May 2026 07:56:28 -0400 Subject: [PATCH 14/17] Split test helpers to mocks.h --- test/test_helpers/mocks.h | 178 +++++++++++++++++++++++++ test/test_helpers/test_advert_data.cpp | 173 +----------------------- 2 files changed, 179 insertions(+), 172 deletions(-) create mode 100644 test/test_helpers/mocks.h diff --git a/test/test_helpers/mocks.h b/test/test_helpers/mocks.h new file mode 100644 index 0000000000..655a44cea5 --- /dev/null +++ b/test/test_helpers/mocks.h @@ -0,0 +1,178 @@ +#pragma once + +#include +#include +#include +#include + +#include "helpers/BaseChatMesh.h" +#include "helpers/SimpleMeshTables.h" + +class FakeMillis final : public mesh::MillisecondClock { +public: + unsigned long getMillis() override { + return 0; + } +}; + +class FakeRtc final : public mesh::RTCClock { +public: + explicit FakeRtc(uint32_t initial_time) : _time(initial_time) {} + + uint32_t getCurrentTime() override { + return _time; + } + + void setCurrentTime(uint32_t time) override { + _time = time; + } + +private: + uint32_t _time; +}; + +class FakeRng final : public mesh::RNG { +public: + void random(uint8_t* dest, size_t sz) override { + memset(dest, 0x5A, sz); + } +}; + +class FakeRadio final : public mesh::Radio { +public: + int recvRaw(uint8_t*, int) override { + return 0; + } + + uint32_t getEstAirtimeFor(int) override { + return 1; + } + + float packetScore(float, int) override { + return 1.0f; + } + + bool startSendRaw(const uint8_t*, int) override { + return true; + } + + bool isSendComplete() override { + return true; + } + + void onSendFinished() override {} + + bool isInRecvMode() const override { + return true; + } +}; + +class NoopPacketManager final : public mesh::PacketManager { +public: + mesh::Packet* allocNew() override { + return nullptr; + } + + void free(mesh::Packet*) override {} + + void queueOutbound(mesh::Packet*, uint8_t, uint32_t) override {} + + mesh::Packet* getNextOutbound(uint32_t) override { + return nullptr; + } + + int getOutboundCount(uint32_t) const override { + return 0; + } + + int getOutboundTotal() const override { + return 0; + } + + int getFreeCount() const override { + return 0; + } + + mesh::Packet* getOutboundByIdx(int) override { + return nullptr; + } + + mesh::Packet* removeOutboundByIdx(int) override { + return nullptr; + } + + void queueInbound(mesh::Packet*, uint32_t) override {} + + mesh::Packet* getNextInbound(uint32_t) override { + return nullptr; + } +}; + +class TestChatMesh final : public BaseChatMesh { +public: + TestChatMesh(mesh::Radio& radio, mesh::MillisecondClock& ms, mesh::RNG& rng, mesh::RTCClock& rtc, + mesh::PacketManager& mgr, mesh::MeshTables& tables) + : BaseChatMesh(radio, ms, rng, rtc, mgr, tables) {} + + mesh::DispatcherAction recv(mesh::Packet* pkt) { + return onRecvPacket(pkt); + } + + std::optional discovered_contact; + +protected: + void onDiscoveredContact(ContactInfo& contact, bool, uint8_t, const uint8_t*) override { + discovered_contact = contact; + } + + ContactInfo* processAck(const uint8_t*) override { + return nullptr; + } + + void onContactPathUpdated(const ContactInfo&) override {} + + void onMessageRecv(const ContactInfo&, mesh::Packet*, uint32_t, const char*) override {} + + void onCommandDataRecv(const ContactInfo&, mesh::Packet*, uint32_t, const char*) override {} + + void onSignedMessageRecv(const ContactInfo&, mesh::Packet*, uint32_t, const uint8_t*, const char*) override {} + + uint32_t calcFloodTimeoutMillisFor(uint32_t) const override { + return 0; + } + + uint32_t calcDirectTimeoutMillisFor(uint32_t, uint8_t) const override { + return 0; + } + + void onSendTimeout() override {} + + void onChannelMessageRecv(const mesh::GroupChannel&, mesh::Packet*, uint32_t, const char*) override {} + + uint8_t onContactRequest(const ContactInfo&, uint32_t, const uint8_t*, uint8_t, uint8_t*) override { + return 0; + } + + void onContactResponse(const ContactInfo&, const uint8_t*, uint8_t) override {} +}; + +struct TestMeshContext { + explicit TestMeshContext(uint32_t current_timestamp) + : rtc(current_timestamp), mesh(radio, ms, rng, rtc, packet_manager, tables) {} + + TestChatMesh* operator->() { + return &mesh; + } + + const TestChatMesh* operator->() const { + return &mesh; + } + + FakeRadio radio; + FakeMillis ms; + FakeRng rng; + FakeRtc rtc; + NoopPacketManager packet_manager; + SimpleMeshTables tables; + TestChatMesh mesh; +}; diff --git a/test/test_helpers/test_advert_data.cpp b/test/test_helpers/test_advert_data.cpp index 83daa4288b..9d9f77095b 100644 --- a/test/test_helpers/test_advert_data.cpp +++ b/test/test_helpers/test_advert_data.cpp @@ -1,12 +1,10 @@ #include #include #include -#include #include -#include "helpers/BaseChatMesh.h" -#include "helpers/SimpleMeshTables.h" +#include "mocks.h" namespace { @@ -40,175 +38,6 @@ void WriteStringLiteral(uint8_t* dest, size_t* offset, const char (&value)[N]) { WriteBytes(dest, offset, reinterpret_cast(value), N - 1); } -class FakeMillis final : public mesh::MillisecondClock { -public: - unsigned long getMillis() override { - return 0; - } -}; - -class FakeRtc final : public mesh::RTCClock { -public: - explicit FakeRtc(uint32_t initial_time) : _time(initial_time) {} - - uint32_t getCurrentTime() override { - return _time; - } - - void setCurrentTime(uint32_t time) override { - _time = time; - } - -private: - uint32_t _time; -}; - -class FakeRng final : public mesh::RNG { -public: - void random(uint8_t* dest, size_t sz) override { - memset(dest, 0x5A, sz); - } -}; - -class FakeRadio final : public mesh::Radio { -public: - int recvRaw(uint8_t*, int) override { - return 0; - } - - uint32_t getEstAirtimeFor(int) override { - return 1; - } - - float packetScore(float, int) override { - return 1.0f; - } - - bool startSendRaw(const uint8_t*, int) override { - return true; - } - - bool isSendComplete() override { - return true; - } - - void onSendFinished() override {} - - bool isInRecvMode() const override { - return true; - } -}; - -class NoopPacketManager final : public mesh::PacketManager { -public: - mesh::Packet* allocNew() override { - return nullptr; - } - - void free(mesh::Packet*) override {} - - void queueOutbound(mesh::Packet*, uint8_t, uint32_t) override {} - - mesh::Packet* getNextOutbound(uint32_t) override { - return nullptr; - } - - int getOutboundCount(uint32_t) const override { - return 0; - } - - int getOutboundTotal() const override { - return 0; - } - - int getFreeCount() const override { - return 0; - } - - mesh::Packet* getOutboundByIdx(int) override { - return nullptr; - } - - mesh::Packet* removeOutboundByIdx(int) override { - return nullptr; - } - - void queueInbound(mesh::Packet*, uint32_t) override {} - - mesh::Packet* getNextInbound(uint32_t) override { - return nullptr; - } -}; - -class TestChatMesh final : public BaseChatMesh { -public: - TestChatMesh(mesh::Radio& radio, mesh::MillisecondClock& ms, mesh::RNG& rng, mesh::RTCClock& rtc, - mesh::PacketManager& mgr, mesh::MeshTables& tables) - : BaseChatMesh(radio, ms, rng, rtc, mgr, tables) {} - - mesh::DispatcherAction recv(mesh::Packet* pkt) { - return onRecvPacket(pkt); - } - - std::optional discovered_contact; - -protected: - void onDiscoveredContact(ContactInfo& contact, bool, uint8_t, const uint8_t*) override { - discovered_contact = contact; - } - - ContactInfo* processAck(const uint8_t*) override { - return nullptr; - } - - void onContactPathUpdated(const ContactInfo&) override {} - - void onMessageRecv(const ContactInfo&, mesh::Packet*, uint32_t, const char*) override {} - - void onCommandDataRecv(const ContactInfo&, mesh::Packet*, uint32_t, const char*) override {} - - void onSignedMessageRecv(const ContactInfo&, mesh::Packet*, uint32_t, const uint8_t*, const char*) override {} - - uint32_t calcFloodTimeoutMillisFor(uint32_t) const override { - return 0; - } - - uint32_t calcDirectTimeoutMillisFor(uint32_t, uint8_t) const override { - return 0; - } - - void onSendTimeout() override {} - - void onChannelMessageRecv(const mesh::GroupChannel&, mesh::Packet*, uint32_t, const char*) override {} - - uint8_t onContactRequest(const ContactInfo&, uint32_t, const uint8_t*, uint8_t, uint8_t*) override { - return 0; - } - - void onContactResponse(const ContactInfo&, const uint8_t*, uint8_t) override {} -}; - -struct TestMeshContext { - explicit TestMeshContext(uint32_t current_timestamp) - : rtc(current_timestamp), mesh(radio, ms, rng, rtc, packet_manager, tables) {} - - TestChatMesh* operator->() { - return &mesh; - } - - const TestChatMesh* operator->() const { - return &mesh; - } - - FakeRadio radio; - FakeMillis ms; - FakeRng rng; - FakeRtc rtc; - NoopPacketManager packet_manager; - SimpleMeshTables tables; - TestChatMesh mesh; -}; - TestMeshContext MakeTestMesh(uint32_t current_timestamp) { return TestMeshContext(current_timestamp); } From 9bb9d31f432dc7859989c2011011ca9f68ecc93d Mon Sep 17 00:00:00 2001 From: Michael Lynch Date: Fri, 1 May 2026 08:12:12 -0400 Subject: [PATCH 15/17] Split up mocks.h --- test/{test_helpers/mocks.h => mocks/mesh.h} | 70 +++------------------ test/mocks/radio.h | 36 +++++++++++ test/mocks/random.h | 25 ++++++++ test/mocks/time.h | 42 +++++++++++++ test/test_helpers/test_advert_data.cpp | 2 +- 5 files changed, 113 insertions(+), 62 deletions(-) rename test/{test_helpers/mocks.h => mocks/mesh.h} (73%) create mode 100644 test/mocks/radio.h create mode 100644 test/mocks/random.h create mode 100644 test/mocks/time.h diff --git a/test/test_helpers/mocks.h b/test/mocks/mesh.h similarity index 73% rename from test/test_helpers/mocks.h rename to test/mocks/mesh.h index 655a44cea5..9b2598fd99 100644 --- a/test/test_helpers/mocks.h +++ b/test/mocks/mesh.h @@ -1,72 +1,16 @@ #pragma once -#include #include -#include #include #include "helpers/BaseChatMesh.h" #include "helpers/SimpleMeshTables.h" +#include "radio.h" +#include "random.h" +#include "time.h" -class FakeMillis final : public mesh::MillisecondClock { -public: - unsigned long getMillis() override { - return 0; - } -}; - -class FakeRtc final : public mesh::RTCClock { -public: - explicit FakeRtc(uint32_t initial_time) : _time(initial_time) {} - - uint32_t getCurrentTime() override { - return _time; - } - - void setCurrentTime(uint32_t time) override { - _time = time; - } - -private: - uint32_t _time; -}; - -class FakeRng final : public mesh::RNG { -public: - void random(uint8_t* dest, size_t sz) override { - memset(dest, 0x5A, sz); - } -}; - -class FakeRadio final : public mesh::Radio { -public: - int recvRaw(uint8_t*, int) override { - return 0; - } - - uint32_t getEstAirtimeFor(int) override { - return 1; - } - - float packetScore(float, int) override { - return 1.0f; - } - - bool startSendRaw(const uint8_t*, int) override { - return true; - } - - bool isSendComplete() override { - return true; - } - - void onSendFinished() override {} - - bool isInRecvMode() const override { - return true; - } -}; - +// No-op packet manager for native tests. +// Satisfies Mesh dependencies while preventing packet allocation or queuing. class NoopPacketManager final : public mesh::PacketManager { public: mesh::Packet* allocNew() override { @@ -108,6 +52,8 @@ class NoopPacketManager final : public mesh::PacketManager { } }; +// Test chat mesh for native tests. +// Exposes packet receive handling and captures discovered contacts for assertions. class TestChatMesh final : public BaseChatMesh { public: TestChatMesh(mesh::Radio& radio, mesh::MillisecondClock& ms, mesh::RNG& rng, mesh::RTCClock& rtc, @@ -156,6 +102,8 @@ class TestChatMesh final : public BaseChatMesh { void onContactResponse(const ContactInfo&, const uint8_t*, uint8_t) override {} }; +// Test mesh context for native tests. +// Owns mock dependencies in construction order and exposes the mesh via operator->. struct TestMeshContext { explicit TestMeshContext(uint32_t current_timestamp) : rtc(current_timestamp), mesh(radio, ms, rng, rtc, packet_manager, tables) {} diff --git a/test/mocks/radio.h b/test/mocks/radio.h new file mode 100644 index 0000000000..59a6939547 --- /dev/null +++ b/test/mocks/radio.h @@ -0,0 +1,36 @@ +#pragma once + +#include + +#include "Dispatcher.h" + +// Fake radio for native tests. +// Provides successful no-op send/receive behavior without hardware access. +class FakeRadio final : public mesh::Radio { +public: + int recvRaw(uint8_t*, int) override { + return 0; + } + + uint32_t getEstAirtimeFor(int) override { + return 1; + } + + float packetScore(float, int) override { + return 1.0f; + } + + bool startSendRaw(const uint8_t*, int) override { + return true; + } + + bool isSendComplete() override { + return true; + } + + void onSendFinished() override {} + + bool isInRecvMode() const override { + return true; + } +}; diff --git a/test/mocks/random.h b/test/mocks/random.h new file mode 100644 index 0000000000..4b6897360b --- /dev/null +++ b/test/mocks/random.h @@ -0,0 +1,25 @@ +#pragma once + +#if defined(__has_include_next) + #if __has_include_next() + #include_next + #endif +#endif + +#ifdef __cplusplus + +#include +#include + +#include "Utils.h" + +// Fake random generator for native tests. +// Fills buffers with deterministic bytes so generated packets are repeatable. +class FakeRng final : public mesh::RNG { +public: + void random(uint8_t* dest, size_t sz) override { + memset(dest, 0x5A, sz); + } +}; + +#endif diff --git a/test/mocks/time.h b/test/mocks/time.h new file mode 100644 index 0000000000..b16f5d309b --- /dev/null +++ b/test/mocks/time.h @@ -0,0 +1,42 @@ +#pragma once + +#if defined(__has_include_next) + #if __has_include_next() + #include_next + #endif +#endif + +#ifdef __cplusplus + +#include + +#include "Dispatcher.h" + +// Fake millisecond clock for native tests. +// Returns a stable timestamp so timer-dependent code stays deterministic. +class FakeMillis final : public mesh::MillisecondClock { +public: + unsigned long getMillis() override { + return 0; + } +}; + +// Fake RTC clock for native tests. +// Stores caller-controlled Unix time without depending on hardware RTC APIs. +class FakeRtc final : public mesh::RTCClock { +public: + explicit FakeRtc(uint32_t initial_time) : _time(initial_time) {} + + uint32_t getCurrentTime() override { + return _time; + } + + void setCurrentTime(uint32_t time) override { + _time = time; + } + +private: + uint32_t _time; +}; + +#endif diff --git a/test/test_helpers/test_advert_data.cpp b/test/test_helpers/test_advert_data.cpp index 9d9f77095b..a6ad03a9c6 100644 --- a/test/test_helpers/test_advert_data.cpp +++ b/test/test_helpers/test_advert_data.cpp @@ -4,7 +4,7 @@ #include -#include "mocks.h" +#include "../mocks/mesh.h" namespace { From 837fcf96237944178c4ded37357b8bc03d46e029 Mon Sep 17 00:00:00 2001 From: Michael Lynch Date: Fri, 1 May 2026 12:12:49 -0400 Subject: [PATCH 16/17] Simplify to a more obviously fake SHA256 algorithm --- test/mocks/SHA256.h | 33 ++++++++------------------------- 1 file changed, 8 insertions(+), 25 deletions(-) diff --git a/test/mocks/SHA256.h b/test/mocks/SHA256.h index 38e779b7d1..d81c1add70 100644 --- a/test/mocks/SHA256.h +++ b/test/mocks/SHA256.h @@ -2,43 +2,26 @@ #include #include -#include // Mock SHA256 class for native testing. -// Provides a deterministic stand-in so code can hash data without Arduino crypto deps. +// Provides a fixed stand-in so code can compile without Arduino crypto deps. +// update() and resetHMAC() ignore all input; finalize() and finalizeHMAC() +// fill the requested output buffer with 0x11 bytes. class SHA256 { - uint32_t state_ = 2166136261u; - public: - void update(const void* data, size_t len) { - update(static_cast(data), len); - } + void update(const void*, size_t) {} - void update(const uint8_t* data, size_t len) { - for (size_t i = 0; i < len; ++i) { - state_ ^= data[i]; - state_ *= 16777619u; - state_ += 0x9E3779B9u; - } - } + void update(const uint8_t*, size_t) {} void finalize(uint8_t* hash, size_t hashLen) { - uint32_t value = state_; for (size_t i = 0; i < hashLen; ++i) { - value ^= value >> 13; - value *= 1274126177u; - hash[i] = static_cast((value >> ((i & 3) * 8)) & 0xFF); + hash[i] = 0x11; } } - void resetHMAC(const uint8_t* key, size_t keyLen) { - state_ = 2166136261u; - update(key, keyLen); - state_ ^= 0x36363636u; - } + void resetHMAC(const uint8_t*, size_t) {} - void finalizeHMAC(const uint8_t* key, size_t keyLen, uint8_t* hash, size_t hashLen) { - update(key, keyLen); + void finalizeHMAC(const uint8_t*, size_t, uint8_t* hash, size_t hashLen) { finalize(hash, hashLen); } }; From 5375246409c7a753a614f76aa2c3f1ba2b936107 Mon Sep 17 00:00:00 2001 From: Michael Lynch Date: Fri, 1 May 2026 16:38:23 -0400 Subject: [PATCH 17/17] Simplify to just test AdvertDataParser --- lib/ed25519/library.json | 7 - platformio.ini | 6 - test/mocks/Ed25519.h | 20 -- test/mocks/SHA256.h | 25 +- test/mocks/Stream.h | 13 +- test/mocks/mesh.h | 126 -------- test/mocks/radio.h | 36 --- test/mocks/random.h | 25 -- test/mocks/time.h | 42 --- test/test_helpers/test_advert_data.cpp | 400 ++++++++----------------- 10 files changed, 128 insertions(+), 572 deletions(-) delete mode 100644 lib/ed25519/library.json delete mode 100644 test/mocks/Ed25519.h delete mode 100644 test/mocks/mesh.h delete mode 100644 test/mocks/radio.h delete mode 100644 test/mocks/random.h delete mode 100644 test/mocks/time.h diff --git a/lib/ed25519/library.json b/lib/ed25519/library.json deleted file mode 100644 index 4cafd5e89e..0000000000 --- a/lib/ed25519/library.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "build": { - "flags": [ - "-fno-sanitize=undefined" - ] - } -} diff --git a/platformio.ini b/platformio.ini index 9696fc3f83..e70f21c584 100644 --- a/platformio.ini +++ b/platformio.ini @@ -169,13 +169,7 @@ build_flags = -std=c++17 test_build_src = yes build_src_filter = -<*> - +<../src/Dispatcher.cpp> - +<../src/Identity.cpp> - +<../src/Mesh.cpp> - +<../src/Packet.cpp> +<../src/Utils.cpp> +<../src/helpers/AdvertDataHelpers.cpp> - +<../src/helpers/BaseChatMesh.cpp> - +<../src/helpers/TxtDataHelpers.cpp> lib_deps = google/googletest @ 1.17.0 diff --git a/test/mocks/Ed25519.h b/test/mocks/Ed25519.h deleted file mode 100644 index 2c649505c3..0000000000 --- a/test/mocks/Ed25519.h +++ /dev/null @@ -1,20 +0,0 @@ -#pragma once - -#ifdef __cplusplus -extern "C" { -#endif -#include -#ifdef __cplusplus -} -#endif - -#include - -// Mock Ed25519 wrapper for native testing. -// Adapts the test-only C ed25519 implementation to the Arduino-style class API. -class Ed25519 { -public: - static bool verify(const uint8_t* sig, const uint8_t* pub_key, const uint8_t* message, int msg_len) { - return ed25519_verify(sig, message, msg_len, pub_key) != 0; - } -}; diff --git a/test/mocks/SHA256.h b/test/mocks/SHA256.h index d81c1add70..b6e551a077 100644 --- a/test/mocks/SHA256.h +++ b/test/mocks/SHA256.h @@ -3,25 +3,12 @@ #include #include -// Mock SHA256 class for native testing. -// Provides a fixed stand-in so code can compile without Arduino crypto deps. -// update() and resetHMAC() ignore all input; finalize() and finalizeHMAC() -// fill the requested output buffer with 0x11 bytes. +// Mock SHA256 class for testing +// Provides minimal interface to allow Utils.cpp to compile class SHA256 { public: - void update(const void*, size_t) {} - - void update(const uint8_t*, size_t) {} - - void finalize(uint8_t* hash, size_t hashLen) { - for (size_t i = 0; i < hashLen; ++i) { - hash[i] = 0x11; - } - } - - void resetHMAC(const uint8_t*, size_t) {} - - void finalizeHMAC(const uint8_t*, size_t, uint8_t* hash, size_t hashLen) { - finalize(hash, hashLen); - } + void update(const uint8_t* data, size_t len) {} + void finalize(uint8_t* hash, size_t hashLen) {} + void resetHMAC(const uint8_t* key, size_t keyLen) {} + void finalizeHMAC(const uint8_t* key, size_t keyLen, uint8_t* hash, size_t hashLen) {} }; diff --git a/test/mocks/Stream.h b/test/mocks/Stream.h index e83b04294e..195a302973 100644 --- a/test/mocks/Stream.h +++ b/test/mocks/Stream.h @@ -1,15 +1,10 @@ #pragma once -#include -#include +// Mock Stream class for native testing +// Provides minimal interface needed by Utils.h -// Mock Stream class for native testing. -// Provides the minimal interface needed by code that depends on Arduino Stream. class Stream { public: - virtual size_t readBytes(uint8_t*, size_t) { return 0; } - virtual size_t write(const uint8_t*, size_t len) { return len; } - virtual void print(char) {} - virtual void print(const char*) {} - virtual void println() {} + virtual void print(char c) {} + virtual void print(const char* str) {} }; diff --git a/test/mocks/mesh.h b/test/mocks/mesh.h deleted file mode 100644 index 9b2598fd99..0000000000 --- a/test/mocks/mesh.h +++ /dev/null @@ -1,126 +0,0 @@ -#pragma once - -#include -#include - -#include "helpers/BaseChatMesh.h" -#include "helpers/SimpleMeshTables.h" -#include "radio.h" -#include "random.h" -#include "time.h" - -// No-op packet manager for native tests. -// Satisfies Mesh dependencies while preventing packet allocation or queuing. -class NoopPacketManager final : public mesh::PacketManager { -public: - mesh::Packet* allocNew() override { - return nullptr; - } - - void free(mesh::Packet*) override {} - - void queueOutbound(mesh::Packet*, uint8_t, uint32_t) override {} - - mesh::Packet* getNextOutbound(uint32_t) override { - return nullptr; - } - - int getOutboundCount(uint32_t) const override { - return 0; - } - - int getOutboundTotal() const override { - return 0; - } - - int getFreeCount() const override { - return 0; - } - - mesh::Packet* getOutboundByIdx(int) override { - return nullptr; - } - - mesh::Packet* removeOutboundByIdx(int) override { - return nullptr; - } - - void queueInbound(mesh::Packet*, uint32_t) override {} - - mesh::Packet* getNextInbound(uint32_t) override { - return nullptr; - } -}; - -// Test chat mesh for native tests. -// Exposes packet receive handling and captures discovered contacts for assertions. -class TestChatMesh final : public BaseChatMesh { -public: - TestChatMesh(mesh::Radio& radio, mesh::MillisecondClock& ms, mesh::RNG& rng, mesh::RTCClock& rtc, - mesh::PacketManager& mgr, mesh::MeshTables& tables) - : BaseChatMesh(radio, ms, rng, rtc, mgr, tables) {} - - mesh::DispatcherAction recv(mesh::Packet* pkt) { - return onRecvPacket(pkt); - } - - std::optional discovered_contact; - -protected: - void onDiscoveredContact(ContactInfo& contact, bool, uint8_t, const uint8_t*) override { - discovered_contact = contact; - } - - ContactInfo* processAck(const uint8_t*) override { - return nullptr; - } - - void onContactPathUpdated(const ContactInfo&) override {} - - void onMessageRecv(const ContactInfo&, mesh::Packet*, uint32_t, const char*) override {} - - void onCommandDataRecv(const ContactInfo&, mesh::Packet*, uint32_t, const char*) override {} - - void onSignedMessageRecv(const ContactInfo&, mesh::Packet*, uint32_t, const uint8_t*, const char*) override {} - - uint32_t calcFloodTimeoutMillisFor(uint32_t) const override { - return 0; - } - - uint32_t calcDirectTimeoutMillisFor(uint32_t, uint8_t) const override { - return 0; - } - - void onSendTimeout() override {} - - void onChannelMessageRecv(const mesh::GroupChannel&, mesh::Packet*, uint32_t, const char*) override {} - - uint8_t onContactRequest(const ContactInfo&, uint32_t, const uint8_t*, uint8_t, uint8_t*) override { - return 0; - } - - void onContactResponse(const ContactInfo&, const uint8_t*, uint8_t) override {} -}; - -// Test mesh context for native tests. -// Owns mock dependencies in construction order and exposes the mesh via operator->. -struct TestMeshContext { - explicit TestMeshContext(uint32_t current_timestamp) - : rtc(current_timestamp), mesh(radio, ms, rng, rtc, packet_manager, tables) {} - - TestChatMesh* operator->() { - return &mesh; - } - - const TestChatMesh* operator->() const { - return &mesh; - } - - FakeRadio radio; - FakeMillis ms; - FakeRng rng; - FakeRtc rtc; - NoopPacketManager packet_manager; - SimpleMeshTables tables; - TestChatMesh mesh; -}; diff --git a/test/mocks/radio.h b/test/mocks/radio.h deleted file mode 100644 index 59a6939547..0000000000 --- a/test/mocks/radio.h +++ /dev/null @@ -1,36 +0,0 @@ -#pragma once - -#include - -#include "Dispatcher.h" - -// Fake radio for native tests. -// Provides successful no-op send/receive behavior without hardware access. -class FakeRadio final : public mesh::Radio { -public: - int recvRaw(uint8_t*, int) override { - return 0; - } - - uint32_t getEstAirtimeFor(int) override { - return 1; - } - - float packetScore(float, int) override { - return 1.0f; - } - - bool startSendRaw(const uint8_t*, int) override { - return true; - } - - bool isSendComplete() override { - return true; - } - - void onSendFinished() override {} - - bool isInRecvMode() const override { - return true; - } -}; diff --git a/test/mocks/random.h b/test/mocks/random.h deleted file mode 100644 index 4b6897360b..0000000000 --- a/test/mocks/random.h +++ /dev/null @@ -1,25 +0,0 @@ -#pragma once - -#if defined(__has_include_next) - #if __has_include_next() - #include_next - #endif -#endif - -#ifdef __cplusplus - -#include -#include - -#include "Utils.h" - -// Fake random generator for native tests. -// Fills buffers with deterministic bytes so generated packets are repeatable. -class FakeRng final : public mesh::RNG { -public: - void random(uint8_t* dest, size_t sz) override { - memset(dest, 0x5A, sz); - } -}; - -#endif diff --git a/test/mocks/time.h b/test/mocks/time.h deleted file mode 100644 index b16f5d309b..0000000000 --- a/test/mocks/time.h +++ /dev/null @@ -1,42 +0,0 @@ -#pragma once - -#if defined(__has_include_next) - #if __has_include_next() - #include_next - #endif -#endif - -#ifdef __cplusplus - -#include - -#include "Dispatcher.h" - -// Fake millisecond clock for native tests. -// Returns a stable timestamp so timer-dependent code stays deterministic. -class FakeMillis final : public mesh::MillisecondClock { -public: - unsigned long getMillis() override { - return 0; - } -}; - -// Fake RTC clock for native tests. -// Stores caller-controlled Unix time without depending on hardware RTC APIs. -class FakeRtc final : public mesh::RTCClock { -public: - explicit FakeRtc(uint32_t initial_time) : _time(initial_time) {} - - uint32_t getCurrentTime() override { - return _time; - } - - void setCurrentTime(uint32_t time) override { - _time = time; - } - -private: - uint32_t _time; -}; - -#endif diff --git a/test/test_helpers/test_advert_data.cpp b/test/test_helpers/test_advert_data.cpp index a6ad03a9c6..cc1c5a2db5 100644 --- a/test/test_helpers/test_advert_data.cpp +++ b/test/test_helpers/test_advert_data.cpp @@ -4,17 +4,10 @@ #include -#include "../mocks/mesh.h" +#include "helpers/AdvertDataHelpers.h" namespace { -constexpr char kSenderPrivateKeyHex[] = - "70" - "65e18fd9fabb70c1ed90dca19907de698c88b709ea146eafd93d9b830c7b60" - "c4681193c79bbc39945ba8064104bb618f8fd7a84a0af6f57033d6e8ddcd6471"; -constexpr char kSenderPublicKeyHex[] = - "1ec77175b0918ed206f9ae04ec136d6d5d4315bb26305427f645b492e9350c10"; - void WriteU8(uint8_t* dest, size_t* offset, uint8_t value) { dest[(*offset)++] = value; } @@ -38,63 +31,11 @@ void WriteStringLiteral(uint8_t* dest, size_t* offset, const char (&value)[N]) { WriteBytes(dest, offset, reinterpret_cast(value), N - 1); } -TestMeshContext MakeTestMesh(uint32_t current_timestamp) { - return TestMeshContext(current_timestamp); -} - -ContactInfo MakeSenderContact(uint32_t advert_timestamp, int32_t gps_lat, int32_t gps_lon) { - ContactInfo contact = {}; - contact.id = mesh::Identity(kSenderPublicKeyHex); - strcpy(contact.name, "existing-contact"); - contact.type = ADV_TYPE_CHAT; - contact.out_path_len = OUT_PATH_UNKNOWN; - contact.last_advert_timestamp = advert_timestamp; - contact.lastmod = advert_timestamp; - contact.gps_lat = gps_lat; - contact.gps_lon = gps_lon; - return contact; +AdvertDataParser Parse(const uint8_t* app_data, size_t app_data_len) { + return AdvertDataParser(app_data, static_cast(app_data_len)); } -mesh::Packet BuildSignedAdvertPacket(uint32_t timestamp, const uint8_t* app_data, uint8_t app_data_len) { - mesh::LocalIdentity sender(kSenderPrivateKeyHex, kSenderPublicKeyHex); - mesh::Packet packet; - - // Wire header: flood-routed advert packet with no path hashes yet. - packet.header = ROUTE_TYPE_FLOOD | (PAYLOAD_TYPE_ADVERT << PH_TYPE_SHIFT); - packet.path_len = 0; - - int offset = 0; - // Sender public key: used by the receiver to identify who signed the advert. - memcpy(&packet.payload[offset], sender.pub_key, PUB_KEY_SIZE); - offset += PUB_KEY_SIZE; - - // Advert timestamp: the sender's monotonic advert time used for replay checks. - memcpy(&packet.payload[offset], ×tamp, sizeof(timestamp)); - offset += sizeof(timestamp); - - // Signature field: filled after the signed message bytes are assembled below. - uint8_t* signature = &packet.payload[offset]; - offset += SIGNATURE_SIZE; - - // Raw advert app_data: arbitrary bytes authored by the test, not by createAdvert(). - memcpy(&packet.payload[offset], app_data, app_data_len); - offset += app_data_len; - packet.payload_len = offset; - - uint8_t message[PUB_KEY_SIZE + 4 + MAX_ADVERT_DATA_SIZE]; - int message_len = 0; - memcpy(&message[message_len], sender.pub_key, PUB_KEY_SIZE); - message_len += PUB_KEY_SIZE; - memcpy(&message[message_len], ×tamp, sizeof(timestamp)); - message_len += sizeof(timestamp); - memcpy(&message[message_len], app_data, app_data_len); - message_len += app_data_len; - - sender.sign(signature, message, message_len); - return packet; -} - -TEST(AdvertData, ParsesNameAndCoordinatesFromNetworkPacket) { +TEST(AdvertDataParser, ParsesNameAndCoordinates) { uint8_t app_data[MAX_ADVERT_DATA_SIZE] = {}; size_t offset = 0; @@ -107,21 +48,18 @@ TEST(AdvertData, ParsesNameAndCoordinatesFromNetworkPacket) { // name field: trailing contact name bytes after the coordinate fields. WriteStringLiteral(app_data, &offset, "dummy-node-name"); - constexpr uint32_t current_timestamp = 1704067200U; - constexpr uint32_t advert_timestamp = current_timestamp + 1; - mesh::Packet packet = BuildSignedAdvertPacket(advert_timestamp, app_data, offset); - - auto test_mesh = MakeTestMesh(current_timestamp); - test_mesh->recv(&packet); + const AdvertDataParser parser = Parse(app_data, offset); - ASSERT_TRUE(test_mesh->discovered_contact.has_value()); - EXPECT_EQ(ADV_TYPE_REPEATER, test_mesh->discovered_contact->type); - EXPECT_STREQ("dummy-node-name", test_mesh->discovered_contact->name); - EXPECT_EQ(37774900, test_mesh->discovered_contact->gps_lat); - EXPECT_EQ(-122419400, test_mesh->discovered_contact->gps_lon); + ASSERT_TRUE(parser.isValid()); + EXPECT_EQ(ADV_TYPE_REPEATER, parser.getType()); + ASSERT_TRUE(parser.hasLatLon()); + EXPECT_EQ(37774900, parser.getIntLat()); + EXPECT_EQ(-122419400, parser.getIntLon()); + ASSERT_TRUE(parser.hasName()); + EXPECT_STREQ("dummy-node-name", parser.getName()); } -TEST(AdvertData, ParsesCoordinateExtremesFromNetworkPacket) { +TEST(AdvertDataParser, ParsesCoordinateExtremes) { uint8_t app_data[MAX_ADVERT_DATA_SIZE] = {}; size_t offset = 0; @@ -134,21 +72,16 @@ TEST(AdvertData, ParsesCoordinateExtremesFromNetworkPacket) { // name field: raw bytes for "dummy-node-name". WriteStringLiteral(app_data, &offset, "dummy-node-name"); - constexpr uint32_t current_timestamp = 1704067200U; - constexpr uint32_t advert_timestamp = current_timestamp + 1; - mesh::Packet packet = BuildSignedAdvertPacket(advert_timestamp, app_data, offset); - - auto test_mesh = MakeTestMesh(current_timestamp); - test_mesh->recv(&packet); + const AdvertDataParser parser = Parse(app_data, offset); - ASSERT_TRUE(test_mesh->discovered_contact.has_value()); - EXPECT_EQ(ADV_TYPE_SENSOR, test_mesh->discovered_contact->type); - EXPECT_STREQ("dummy-node-name", test_mesh->discovered_contact->name); - EXPECT_EQ(-90000000, test_mesh->discovered_contact->gps_lat); - EXPECT_EQ(180000000, test_mesh->discovered_contact->gps_lon); + ASSERT_TRUE(parser.isValid()); + EXPECT_EQ(ADV_TYPE_SENSOR, parser.getType()); + EXPECT_EQ(-90000000, parser.getIntLat()); + EXPECT_EQ(180000000, parser.getIntLon()); + EXPECT_STREQ("dummy-node-name", parser.getName()); } -TEST(AdvertData, ParsesPositiveLatitudeAndNegativeLongitudeBoundariesFromNetworkPacket) { +TEST(AdvertDataParser, ParsesPositiveLatitudeAndNegativeLongitudeBoundaries) { uint8_t app_data[MAX_ADVERT_DATA_SIZE] = {}; size_t offset = 0; @@ -161,19 +94,14 @@ TEST(AdvertData, ParsesPositiveLatitudeAndNegativeLongitudeBoundariesFromNetwork // name field: raw bytes for "dummy-node-name". WriteStringLiteral(app_data, &offset, "dummy-node-name"); - constexpr uint32_t current_timestamp = 1704067200U; - constexpr uint32_t advert_timestamp = current_timestamp + 1; - mesh::Packet packet = BuildSignedAdvertPacket(advert_timestamp, app_data, offset); + const AdvertDataParser parser = Parse(app_data, offset); - auto test_mesh = MakeTestMesh(current_timestamp); - test_mesh->recv(&packet); - - ASSERT_TRUE(test_mesh->discovered_contact.has_value()); - EXPECT_EQ(90000000, test_mesh->discovered_contact->gps_lat); - EXPECT_EQ(-180000000, test_mesh->discovered_contact->gps_lon); + ASSERT_TRUE(parser.isValid()); + EXPECT_EQ(90000000, parser.getIntLat()); + EXPECT_EQ(-180000000, parser.getIntLon()); } -TEST(AdvertData, ParsesNullIslandCoordinatesFromNetworkPacket) { +TEST(AdvertDataParser, ParsesNullIslandCoordinates) { uint8_t app_data[MAX_ADVERT_DATA_SIZE] = {}; size_t offset = 0; @@ -186,19 +114,32 @@ TEST(AdvertData, ParsesNullIslandCoordinatesFromNetworkPacket) { // name field: raw bytes for "dummy-node-name". WriteStringLiteral(app_data, &offset, "dummy-node-name"); - constexpr uint32_t current_timestamp = 1704067200U; - constexpr uint32_t advert_timestamp = current_timestamp + 1; - mesh::Packet packet = BuildSignedAdvertPacket(advert_timestamp, app_data, offset); + const AdvertDataParser parser = Parse(app_data, offset); - auto test_mesh = MakeTestMesh(current_timestamp); - test_mesh->recv(&packet); + ASSERT_TRUE(parser.isValid()); + EXPECT_EQ(0, parser.getIntLat()); + EXPECT_EQ(0, parser.getIntLon()); +} - ASSERT_TRUE(test_mesh->discovered_contact.has_value()); - EXPECT_EQ(0, test_mesh->discovered_contact->gps_lat); - EXPECT_EQ(0, test_mesh->discovered_contact->gps_lon); +TEST(AdvertDataParser, ParsesNameWithoutCoordinates) { + uint8_t app_data[MAX_ADVERT_DATA_SIZE] = {}; + size_t offset = 0; + + // flags/type byte: chat advert with a name but no GPS fields. + WriteU8(app_data, &offset, ADV_TYPE_CHAT | ADV_NAME_MASK); + // name field: contact name with no coordinate payload before it. + WriteStringLiteral(app_data, &offset, "updated-name"); + + const AdvertDataParser parser = Parse(app_data, offset); + + ASSERT_TRUE(parser.isValid()); + EXPECT_EQ(ADV_TYPE_CHAT, parser.getType()); + EXPECT_FALSE(parser.hasLatLon()); + ASSERT_TRUE(parser.hasName()); + EXPECT_STREQ("updated-name", parser.getName()); } -TEST(AdvertData, RejectsLongitudeOutsideValidRangeFromNetworkPacket) { +TEST(AdvertDataParser, RejectsLongitudeOutsideValidRange) { uint8_t app_data[MAX_ADVERT_DATA_SIZE] = {}; size_t offset = 0; @@ -211,17 +152,12 @@ TEST(AdvertData, RejectsLongitudeOutsideValidRangeFromNetworkPacket) { // name field: parser should reject before the trailing name matters. WriteStringLiteral(app_data, &offset, "dummy-node-name"); - constexpr uint32_t current_timestamp = 1704067200U; - constexpr uint32_t advert_timestamp = current_timestamp + 1; - mesh::Packet packet = BuildSignedAdvertPacket(advert_timestamp, app_data, offset); + const AdvertDataParser parser = Parse(app_data, offset); - auto test_mesh = MakeTestMesh(current_timestamp); - test_mesh->recv(&packet); - - EXPECT_FALSE(test_mesh->discovered_contact.has_value()); + EXPECT_FALSE(parser.isValid()); } -TEST(AdvertData, RejectsLongitudeBelowValidRangeFromNetworkPacket) { +TEST(AdvertDataParser, RejectsLongitudeBelowValidRange) { uint8_t app_data[MAX_ADVERT_DATA_SIZE] = {}; size_t offset = 0; @@ -234,51 +170,70 @@ TEST(AdvertData, RejectsLongitudeBelowValidRangeFromNetworkPacket) { // name field: included to keep the payload shape consistent. WriteStringLiteral(app_data, &offset, "dummy-node-name"); - constexpr uint32_t current_timestamp = 1704067200U; - constexpr uint32_t advert_timestamp = current_timestamp + 1; - mesh::Packet packet = BuildSignedAdvertPacket(advert_timestamp, app_data, offset); + const AdvertDataParser parser = Parse(app_data, offset); - auto test_mesh = MakeTestMesh(current_timestamp); - test_mesh->recv(&packet); + EXPECT_FALSE(parser.isValid()); +} + +TEST(AdvertDataParser, RejectsLatitudeOutsideValidRange) { + uint8_t app_data[MAX_ADVERT_DATA_SIZE] = {}; + size_t offset = 0; - EXPECT_FALSE(test_mesh->discovered_contact.has_value()); + // flags/type byte: chat advert with location and name fields present. + WriteU8(app_data, &offset, ADV_TYPE_CHAT | ADV_LATLON_MASK | ADV_NAME_MASK); + // latitude field: one microdegree above +90.0, which is invalid. + WriteI32Le(app_data, &offset, 90000001); + // longitude field: valid longitude so the failure comes from latitude. + WriteI32Le(app_data, &offset, -122419400); + // name field: included to keep the payload shape consistent. + WriteStringLiteral(app_data, &offset, "dummy-node-name"); + + const AdvertDataParser parser = Parse(app_data, offset); + + EXPECT_FALSE(parser.isValid()); } -TEST(AdvertData, RejectsGpsPayloadWithMissingFlagsByte) { - // Backing storage is unused because the advertised app_data length is zero. - uint8_t app_data[1] = {}; +TEST(AdvertDataParser, RejectsLatitudeBelowValidRange) { + uint8_t app_data[MAX_ADVERT_DATA_SIZE] = {}; size_t offset = 0; - constexpr uint32_t current_timestamp = 1704067200U; - constexpr uint32_t advert_timestamp = current_timestamp + 1; - // Leave the app_data length at zero so the parser never sees the flags byte. - mesh::Packet packet = BuildSignedAdvertPacket(advert_timestamp, app_data, offset); + // flags/type byte: chat advert with location and name fields present. + WriteU8(app_data, &offset, ADV_TYPE_CHAT | ADV_LATLON_MASK | ADV_NAME_MASK); + // latitude field: one microdegree below -90.0, which is invalid. + WriteI32Le(app_data, &offset, -90000001); + // longitude field: valid longitude so the failure comes from latitude. + WriteI32Le(app_data, &offset, -122419400); + // name field: included to keep the payload shape consistent. + WriteStringLiteral(app_data, &offset, "dummy-node-name"); + + const AdvertDataParser parser = Parse(app_data, offset); + + EXPECT_FALSE(parser.isValid()); +} + +TEST(AdvertDataParser, RejectsPayloadWithMissingFlagsByte) { + // Backing storage is unused because the advertised app_data length is zero. + const uint8_t app_data[1] = {}; - auto test_mesh = MakeTestMesh(current_timestamp); - test_mesh->recv(&packet); + const AdvertDataParser parser = Parse(app_data, 0); - EXPECT_FALSE(test_mesh->discovered_contact.has_value()); + EXPECT_FALSE(parser.isValid()); } -TEST(AdvertData, RejectsGpsPayloadWithOnlyFlagsByte) { +TEST(AdvertDataParser, RejectsPayloadWithOnlyFlagsByte) { uint8_t app_data[1] = {}; size_t offset = 0; // flags/type byte: chat advert that claims to carry coordinates and a name. WriteU8(app_data, &offset, ADV_TYPE_CHAT | ADV_LATLON_MASK | ADV_NAME_MASK); - constexpr uint32_t current_timestamp = 1704067200U; - constexpr uint32_t advert_timestamp = current_timestamp + 1; // Pass only the flags byte so no latitude bytes remain. - mesh::Packet packet = BuildSignedAdvertPacket(advert_timestamp, app_data, offset); + const AdvertDataParser parser = Parse(app_data, offset); - auto test_mesh = MakeTestMesh(current_timestamp); - test_mesh->recv(&packet); - - EXPECT_FALSE(test_mesh->discovered_contact.has_value()); + EXPECT_FALSE(parser.isValid()); } -TEST(AdvertData, RejectsGpsPayloadWithLatitudeButMissingLongitude) { +TEST(AdvertDataParser, RejectsPayloadWithLatitudeButMissingLongitude) { uint8_t app_data[5] = {}; size_t offset = 0; @@ -287,18 +242,13 @@ TEST(AdvertData, RejectsGpsPayloadWithLatitudeButMissingLongitude) { // latitude field: complete latitude bytes are present before truncation. WriteI32Le(app_data, &offset, 37774900); - constexpr uint32_t current_timestamp = 1704067200U; - constexpr uint32_t advert_timestamp = current_timestamp + 1; // Pass only the flags byte and latitude field so longitude is missing. - mesh::Packet packet = BuildSignedAdvertPacket(advert_timestamp, app_data, offset); - - auto test_mesh = MakeTestMesh(current_timestamp); - test_mesh->recv(&packet); + const AdvertDataParser parser = Parse(app_data, offset); - EXPECT_FALSE(test_mesh->discovered_contact.has_value()); + EXPECT_FALSE(parser.isValid()); } -TEST(AdvertData, RejectsGpsPayloadOneByteShortOfFullCoordinates) { +TEST(AdvertDataParser, RejectsPayloadOneByteShortOfFullCoordinates) { uint8_t app_data[8] = {}; size_t offset = 0; uint8_t lon_bytes[sizeof(int32_t)] = {}; @@ -312,155 +262,41 @@ TEST(AdvertData, RejectsGpsPayloadOneByteShortOfFullCoordinates) { WriteI32Le(lon_bytes, &lon_offset, -122419400); WriteBytes(app_data, &offset, lon_bytes, 3); - constexpr uint32_t current_timestamp = 1704067200U; - constexpr uint32_t advert_timestamp = current_timestamp + 1; // Pass only the flags byte, latitude field, and three longitude bytes. - mesh::Packet packet = BuildSignedAdvertPacket(advert_timestamp, app_data, offset); - - auto test_mesh = MakeTestMesh(current_timestamp); - test_mesh->recv(&packet); + const AdvertDataParser parser = Parse(app_data, offset); - EXPECT_FALSE(test_mesh->discovered_contact.has_value()); + EXPECT_FALSE(parser.isValid()); } -TEST(AdvertData, RejectsLatitudeOutsideValidRangeFromNetworkPacket) { - uint8_t app_data[MAX_ADVERT_DATA_SIZE] = {}; +TEST(AdvertDataParser, RejectsPayloadWithIncompleteFeat1) { + uint8_t app_data[2] = {}; size_t offset = 0; - // flags/type byte: chat advert with location and name fields present. - WriteU8(app_data, &offset, ADV_TYPE_CHAT | ADV_LATLON_MASK | ADV_NAME_MASK); - // latitude field: one microdegree above +90.0, which is invalid. - WriteI32Le(app_data, &offset, 90000001); - // longitude field: valid longitude so the failure comes from latitude. - WriteI32Le(app_data, &offset, -122419400); - // name field: included to keep the payload shape consistent. - WriteStringLiteral(app_data, &offset, "dummy-node-name"); - - constexpr uint32_t current_timestamp = 1704067200U; - constexpr uint32_t advert_timestamp = current_timestamp + 1; - mesh::Packet packet = BuildSignedAdvertPacket(advert_timestamp, app_data, offset); - - auto test_mesh = MakeTestMesh(current_timestamp); - test_mesh->recv(&packet); - - EXPECT_FALSE(test_mesh->discovered_contact.has_value()); -} - -TEST(AdvertData, RejectsLatitudeBelowValidRangeFromNetworkPacket) { - uint8_t app_data[MAX_ADVERT_DATA_SIZE] = {}; - size_t offset = 0; - - // flags/type byte: chat advert with location and name fields present. - WriteU8(app_data, &offset, ADV_TYPE_CHAT | ADV_LATLON_MASK | ADV_NAME_MASK); - // latitude field: one microdegree below -90.0, which is invalid. - WriteI32Le(app_data, &offset, -90000001); - // longitude field: valid longitude so the failure comes from latitude. - WriteI32Le(app_data, &offset, -122419400); - // name field: included to keep the payload shape consistent. - WriteStringLiteral(app_data, &offset, "dummy-node-name"); - - constexpr uint32_t current_timestamp = 1704067200U; - constexpr uint32_t advert_timestamp = current_timestamp + 1; - mesh::Packet packet = BuildSignedAdvertPacket(advert_timestamp, app_data, offset); - - auto test_mesh = MakeTestMesh(current_timestamp); - test_mesh->recv(&packet); - - EXPECT_FALSE(test_mesh->discovered_contact.has_value()); -} + // flags/type byte: chat advert that claims to carry a two-byte feature field. + WriteU8(app_data, &offset, ADV_TYPE_CHAT | ADV_FEAT1_MASK); + // feature field: only the first byte is present before truncation. + WriteU8(app_data, &offset, 0x34); -TEST(AdvertData, KeepsExistingGpsWhenUpdatedAdvertOmitsCoordinates) { - uint8_t app_data[MAX_ADVERT_DATA_SIZE] = {}; - size_t offset = 0; - - // flags/type byte: chat advert with a new name but no GPS fields. - WriteU8(app_data, &offset, ADV_TYPE_CHAT | ADV_NAME_MASK); - // name field: replacement contact name with no coordinate payload following it. - WriteStringLiteral(app_data, &offset, "updated-name"); - - constexpr uint32_t current_timestamp = 1704067200U; - constexpr uint32_t existing_advert_timestamp = current_timestamp - 10; - constexpr uint32_t new_advert_timestamp = current_timestamp + 1; - mesh::Packet packet = BuildSignedAdvertPacket(new_advert_timestamp, app_data, offset); - - auto test_mesh = MakeTestMesh(current_timestamp); - ASSERT_TRUE(test_mesh->addContact(MakeSenderContact(existing_advert_timestamp, 37774900, -122419400))); - - test_mesh->recv(&packet); - - ContactInfo* updated = test_mesh->lookupContactByPubKey(mesh::Identity(kSenderPublicKeyHex).pub_key, PUB_KEY_SIZE); - ASSERT_NE(nullptr, updated); - EXPECT_STREQ("updated-name", updated->name); - EXPECT_EQ(37774900, updated->gps_lat); - EXPECT_EQ(-122419400, updated->gps_lon); - ASSERT_TRUE(test_mesh->discovered_contact.has_value()); - EXPECT_EQ(37774900, test_mesh->discovered_contact->gps_lat); - EXPECT_EQ(-122419400, test_mesh->discovered_contact->gps_lon); -} - -TEST(AdvertData, OverwritesExistingGpsWhenUpdatedAdvertIncludesCoordinates) { - uint8_t app_data[MAX_ADVERT_DATA_SIZE] = {}; - size_t offset = 0; + const AdvertDataParser parser = Parse(app_data, offset); - // flags/type byte: chat advert with replacement GPS coordinates and a new name. - WriteU8(app_data, &offset, ADV_TYPE_CHAT | ADV_LATLON_MASK | ADV_NAME_MASK); - // latitude field: replacement latitude for 40.712800 degrees. - WriteI32Le(app_data, &offset, 40712800); - // longitude field: replacement longitude for -74.006000 degrees. - WriteI32Le(app_data, &offset, -74006000); - // name field: replacement contact name applied with the new coordinates. - WriteStringLiteral(app_data, &offset, "updated-name"); - - constexpr uint32_t current_timestamp = 1704067200U; - constexpr uint32_t existing_advert_timestamp = current_timestamp - 10; - constexpr uint32_t new_advert_timestamp = current_timestamp + 1; - mesh::Packet packet = BuildSignedAdvertPacket(new_advert_timestamp, app_data, offset); - - auto test_mesh = MakeTestMesh(current_timestamp); - ASSERT_TRUE(test_mesh->addContact(MakeSenderContact(existing_advert_timestamp, 37774900, -122419400))); - - test_mesh->recv(&packet); - - ContactInfo* updated = test_mesh->lookupContactByPubKey(mesh::Identity(kSenderPublicKeyHex).pub_key, PUB_KEY_SIZE); - ASSERT_NE(nullptr, updated); - EXPECT_STREQ("updated-name", updated->name); - EXPECT_EQ(40712800, updated->gps_lat); - EXPECT_EQ(-74006000, updated->gps_lon); - ASSERT_TRUE(test_mesh->discovered_contact.has_value()); - EXPECT_EQ(40712800, test_mesh->discovered_contact->gps_lat); - EXPECT_EQ(-74006000, test_mesh->discovered_contact->gps_lon); + EXPECT_FALSE(parser.isValid()); } -TEST(AdvertData, LeavesExistingGpsUntouchedWhenUpdatedAdvertHasInvalidCoordinates) { - uint8_t app_data[MAX_ADVERT_DATA_SIZE] = {}; +TEST(AdvertDataParser, RejectsPayloadWithIncompleteFeat2) { + uint8_t app_data[4] = {}; size_t offset = 0; - // flags/type byte: chat advert with invalid longitude and a new name. - WriteU8(app_data, &offset, ADV_TYPE_CHAT | ADV_LATLON_MASK | ADV_NAME_MASK); - // latitude field: valid latitude so the update failure comes from longitude. - WriteI32Le(app_data, &offset, 37774900); - // longitude field: one microdegree above +180.0, which should reject the update. - WriteI32Le(app_data, &offset, 180000001); - // name field: replacement name that should not be applied when parsing fails. - WriteStringLiteral(app_data, &offset, "updated-name"); - - constexpr uint32_t current_timestamp = 1704067200U; - constexpr uint32_t existing_advert_timestamp = current_timestamp - 10; - constexpr uint32_t new_advert_timestamp = current_timestamp + 1; - mesh::Packet packet = BuildSignedAdvertPacket(new_advert_timestamp, app_data, offset); - - auto test_mesh = MakeTestMesh(current_timestamp); - ASSERT_TRUE(test_mesh->addContact(MakeSenderContact(existing_advert_timestamp, 37774900, -122419400))); + // flags/type byte: chat advert that claims to carry both two-byte feature fields. + WriteU8(app_data, &offset, ADV_TYPE_CHAT | ADV_FEAT1_MASK | ADV_FEAT2_MASK); + // feature 1 field: complete two-byte value before the truncated feature 2 field. + WriteU8(app_data, &offset, 0x34); + WriteU8(app_data, &offset, 0x12); + // feature 2 field: only the first byte is present before truncation. + WriteU8(app_data, &offset, 0x78); - test_mesh->recv(&packet); + const AdvertDataParser parser = Parse(app_data, offset); - ContactInfo* existing = test_mesh->lookupContactByPubKey(mesh::Identity(kSenderPublicKeyHex).pub_key, PUB_KEY_SIZE); - ASSERT_NE(nullptr, existing); - EXPECT_STREQ("existing-contact", existing->name); - EXPECT_EQ(37774900, existing->gps_lat); - EXPECT_EQ(-122419400, existing->gps_lon); - EXPECT_EQ(existing_advert_timestamp, existing->last_advert_timestamp); - EXPECT_FALSE(test_mesh->discovered_contact.has_value()); + EXPECT_FALSE(parser.isValid()); } } // namespace