diff --git a/lib/ed25519/library.json b/lib/ed25519/library.json deleted file mode 100644 index 4cafd5e89..000000000 --- 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 9696fc3f8..e70f21c58 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 2c649505c..000000000 --- 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 d81c1add7..b6e551a07 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 e83b04294..195a30297 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 9b2598fd9..000000000 --- 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 59a693954..000000000 --- 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 4b6897360..000000000 --- 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 b16f5d309..000000000 --- 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 a6ad03a9c..cc1c5a2db 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