Browse Source

Simplify to just test AdvertDataParser

pull/2459/head
Michael Lynch 1 month ago
parent
commit
5375246409
  1. 7
      lib/ed25519/library.json
  2. 6
      platformio.ini
  3. 20
      test/mocks/Ed25519.h
  4. 25
      test/mocks/SHA256.h
  5. 13
      test/mocks/Stream.h
  6. 126
      test/mocks/mesh.h
  7. 36
      test/mocks/radio.h
  8. 25
      test/mocks/random.h
  9. 42
      test/mocks/time.h
  10. 400
      test/test_helpers/test_advert_data.cpp

7
lib/ed25519/library.json

@ -1,7 +0,0 @@
{
"build": {
"flags": [
"-fno-sanitize=undefined"
]
}
}

6
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

20
test/mocks/Ed25519.h

@ -1,20 +0,0 @@
#pragma once
#ifdef __cplusplus
extern "C" {
#endif
#include <ed_25519.h>
#ifdef __cplusplus
}
#endif
#include <stdint.h>
// 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;
}
};

25
test/mocks/SHA256.h

@ -3,25 +3,12 @@
#include <stdint.h>
#include <stddef.h>
// 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) {}
};

13
test/mocks/Stream.h

@ -1,15 +1,10 @@
#pragma once
#include <stddef.h>
#include <stdint.h>
// 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) {}
};

126
test/mocks/mesh.h

@ -1,126 +0,0 @@
#pragma once
#include <cstdint>
#include <optional>
#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<ContactInfo> 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;
};

36
test/mocks/radio.h

@ -1,36 +0,0 @@
#pragma once
#include <cstdint>
#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;
}
};

25
test/mocks/random.h

@ -1,25 +0,0 @@
#pragma once
#if defined(__has_include_next)
#if __has_include_next(<random.h>)
#include_next <random.h>
#endif
#endif
#ifdef __cplusplus
#include <cstddef>
#include <cstring>
#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

42
test/mocks/time.h

@ -1,42 +0,0 @@
#pragma once
#if defined(__has_include_next)
#if __has_include_next(<time.h>)
#include_next <time.h>
#endif
#endif
#ifdef __cplusplus
#include <cstdint>
#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

400
test/test_helpers/test_advert_data.cpp

@ -4,17 +4,10 @@
#include <gtest/gtest.h>
#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<const uint8_t*>(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<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.
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], &timestamp, 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], &timestamp, 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

Loading…
Cancel
Save