mirror of
https://github.com/meshtastic/firmware.git
synced 2026-10-31 12:31:02 -04:00
NeighborInfoModule::alterReceivedProtobuf set last_sent_by_id to the relay's node number and re-encoded the payload. The XEdDSA signature covers Data.payload, so receivers that know the sender's key dropped the relayed copy. Relays now forward the payload unmodified, and senders no longer set last_sent_by_id. Receivers attribute a NeighborInfo to the node they heard it from using the packet header: the sender when heard directly, otherwise the relay named by relay_node when it resolves to a single known direct neighbor. A last_sent_by_id rewritten by an older relay is still used when it matches relay_node. Claude-Session: https://claude.ai/code/session_01NmhtcXBe3B3NvS9RttzwHW Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
1044 lines
38 KiB
C++
1044 lines
38 KiB
C++
#include "MeshModule.h"
|
|
#include "MeshTypes.h"
|
|
#include "TestUtil.h"
|
|
#include <unity.h>
|
|
|
|
#include "configuration.h"
|
|
#include "gps/RTC.h"
|
|
#include "mesh/CryptoEngine.h"
|
|
#include "mesh/MeshService.h"
|
|
#include "mesh/NodeDB.h"
|
|
#include "mesh/RadioInterface.h"
|
|
#include "mesh/Router.h"
|
|
#include "modules/NeighborInfoModule.h"
|
|
#include "modules/RoutingModule.h"
|
|
#include "support/MockMeshService.h"
|
|
#include <memory>
|
|
#include <vector>
|
|
|
|
namespace
|
|
{
|
|
constexpr NodeNum LOCAL_NODE = 0x11111111;
|
|
constexpr NodeNum REMOTE_NODE = 0x22222222;
|
|
|
|
// Minimal concrete subclass for testing the base class helper
|
|
class TestModule : public MeshModule
|
|
{
|
|
public:
|
|
TestModule() : MeshModule("TestModule") {}
|
|
virtual bool wantPacket(const meshtastic_MeshPacket *p) override { return true; }
|
|
using MeshModule::currentRequest;
|
|
using MeshModule::isMultiHopBroadcastRequest;
|
|
};
|
|
|
|
class MockNodeDB : public NodeDB
|
|
{
|
|
};
|
|
|
|
class MockRadioInterface : public RadioInterface
|
|
{
|
|
public:
|
|
ErrorCode send(meshtastic_MeshPacket *p) override
|
|
{
|
|
packetPool.release(p);
|
|
return ERRNO_OK;
|
|
}
|
|
|
|
uint32_t getPacketTime(uint32_t totalPacketLen, bool received = false) override
|
|
{
|
|
(void)totalPacketLen;
|
|
(void)received;
|
|
return 0;
|
|
}
|
|
};
|
|
|
|
class MockRouter : public Router
|
|
{
|
|
public:
|
|
ErrorCode send(meshtastic_MeshPacket *p) override
|
|
{
|
|
sentPackets.push_back(*p);
|
|
packetPool.release(p);
|
|
return ERRNO_OK;
|
|
}
|
|
|
|
std::vector<meshtastic_MeshPacket> sentPackets;
|
|
};
|
|
|
|
struct AckNak {
|
|
meshtastic_Routing_Error error;
|
|
NodeNum to;
|
|
PacketId requestId;
|
|
ChannelIndex channel;
|
|
};
|
|
|
|
class MockRoutingModule : public RoutingModule
|
|
{
|
|
public:
|
|
void sendAckNak(meshtastic_Routing_Error err, NodeNum to, PacketId idFrom, ChannelIndex chIndex, uint8_t hopLimit = 0,
|
|
bool ackWantsAck = false, const meshtastic_MeshPacket *relaySource = nullptr) override
|
|
{
|
|
(void)hopLimit;
|
|
(void)ackWantsAck;
|
|
(void)relaySource;
|
|
ackNaks.push_back({err, to, idFrom, chIndex});
|
|
}
|
|
|
|
std::vector<AckNak> ackNaks;
|
|
|
|
protected:
|
|
bool wantPacket(const meshtastic_MeshPacket *p) override
|
|
{
|
|
(void)p;
|
|
return false;
|
|
}
|
|
};
|
|
|
|
class SyntheticReplyModule : public MeshModule
|
|
{
|
|
public:
|
|
SyntheticReplyModule(const char *name, meshtastic_PortNum modulePort, meshtastic_PortNum replyPort,
|
|
bool acceptsEveryPort = false)
|
|
: MeshModule(name, modulePort), replyPort(replyPort), acceptsEveryPort(acceptsEveryPort)
|
|
{
|
|
isPromiscuous = acceptsEveryPort;
|
|
}
|
|
|
|
uint32_t allocReplyCalls = 0;
|
|
|
|
protected:
|
|
bool wantPacket(const meshtastic_MeshPacket *p) override { return acceptsEveryPort || p->decoded.portnum == ourPortNum; }
|
|
|
|
meshtastic_MeshPacket *allocReply() override
|
|
{
|
|
allocReplyCalls++;
|
|
meshtastic_MeshPacket *reply = router->allocForSending();
|
|
reply->decoded.portnum = replyPort;
|
|
return reply;
|
|
}
|
|
|
|
private:
|
|
meshtastic_PortNum replyPort;
|
|
bool acceptsEveryPort;
|
|
};
|
|
|
|
class ObservingIgnoreModule : public MeshModule
|
|
{
|
|
public:
|
|
ObservingIgnoreModule() : MeshModule("storeforward-shaped", meshtastic_PortNum_STORE_FORWARD_APP) {}
|
|
|
|
uint32_t allocReplyCalls = 0;
|
|
|
|
protected:
|
|
bool wantPacket(const meshtastic_MeshPacket *p) override { return p->decoded.portnum == meshtastic_PortNum_TEXT_MESSAGE_APP; }
|
|
|
|
ProcessMessage handleReceived(const meshtastic_MeshPacket &mp) override
|
|
{
|
|
(void)mp;
|
|
ignoreRequest = true;
|
|
return ProcessMessage::CONTINUE;
|
|
}
|
|
|
|
meshtastic_MeshPacket *allocReply() override
|
|
{
|
|
allocReplyCalls++;
|
|
return nullptr;
|
|
}
|
|
};
|
|
|
|
class ReplyIgnoreModule : public MeshModule
|
|
{
|
|
public:
|
|
ReplyIgnoreModule() : MeshModule("reply-ignore", meshtastic_PortNum_NEIGHBORINFO_APP) {}
|
|
|
|
uint32_t allocReplyCalls = 0;
|
|
|
|
protected:
|
|
bool wantPacket(const meshtastic_MeshPacket *p) override { return p->decoded.portnum == ourPortNum; }
|
|
|
|
meshtastic_MeshPacket *allocReply() override
|
|
{
|
|
allocReplyCalls++;
|
|
ignoreRequest = true;
|
|
return nullptr;
|
|
}
|
|
};
|
|
|
|
// Sends, from inside callModules(), a self-addressed reply and a local broadcast - the fan-out an
|
|
// AdminModule config save performs. Both go out through service->sendToMesh() while a
|
|
// handleReceived() frame is live, so both must be deferred rather than handled re-entrantly.
|
|
class NestedFanoutModule : public MeshModule
|
|
{
|
|
public:
|
|
NestedFanoutModule() : MeshModule("nested-fanout", meshtastic_PortNum_PRIVATE_APP) {}
|
|
uint32_t handleCalls = 0;
|
|
|
|
protected:
|
|
bool wantPacket(const meshtastic_MeshPacket *p) override { return p->decoded.portnum == ourPortNum; }
|
|
|
|
ProcessMessage handleReceived(const meshtastic_MeshPacket &mp) override
|
|
{
|
|
(void)mp;
|
|
handleCalls++;
|
|
|
|
// (a) a reply addressed to ourselves - exercises the isToUs() deferral branch
|
|
meshtastic_MeshPacket *reply = router->allocForSending();
|
|
reply->to = LOCAL_NODE;
|
|
reply->decoded.portnum = meshtastic_PortNum_TEXT_MESSAGE_APP;
|
|
reply->decoded.request_id = 0xBEEF;
|
|
service->sendToMesh(reply); // default src == RX_SRC_LOCAL
|
|
|
|
// (b) a local broadcast - exercises the broadcast branch (loopback deferred, TX immediate)
|
|
meshtastic_MeshPacket *bcast = router->allocForSending();
|
|
bcast->to = NODENUM_BROADCAST;
|
|
bcast->decoded.portnum = meshtastic_PortNum_TEXT_MESSAGE_APP;
|
|
service->sendToMesh(bcast);
|
|
|
|
return ProcessMessage::STOP;
|
|
}
|
|
};
|
|
|
|
// loopbackOk so it also runs for drained RX_SRC_LOCAL packets; each generation sends the next, so
|
|
// the drain must keep processing a chain that grows while it is draining.
|
|
class ChainSendModule : public MeshModule
|
|
{
|
|
public:
|
|
ChainSendModule() : MeshModule("chain-send", meshtastic_PortNum_PRIVATE_APP) { loopbackOk = true; }
|
|
uint32_t handleCalls = 0;
|
|
uint32_t maxGeneration = 0;
|
|
|
|
protected:
|
|
bool wantPacket(const meshtastic_MeshPacket *p) override { return p->decoded.portnum == ourPortNum; }
|
|
|
|
ProcessMessage handleReceived(const meshtastic_MeshPacket &mp) override
|
|
{
|
|
handleCalls++;
|
|
uint32_t generation = mp.decoded.request_id;
|
|
if (generation > maxGeneration)
|
|
maxGeneration = generation;
|
|
|
|
if (generation < 3) {
|
|
meshtastic_MeshPacket *next = router->allocForSending();
|
|
next->to = LOCAL_NODE;
|
|
next->decoded.portnum = ourPortNum;
|
|
next->decoded.request_id = generation + 1;
|
|
service->sendToMesh(next); // default src == RX_SRC_LOCAL
|
|
}
|
|
return ProcessMessage::STOP;
|
|
}
|
|
};
|
|
|
|
// Sends a burst of self-addressed replies from one dispatch, to overflow the deferred queue.
|
|
class BurstSendModule : public MeshModule
|
|
{
|
|
public:
|
|
explicit BurstSendModule(uint32_t count) : MeshModule("burst-send", meshtastic_PortNum_PRIVATE_APP), count(count) {}
|
|
|
|
protected:
|
|
bool wantPacket(const meshtastic_MeshPacket *p) override { return p->decoded.portnum == ourPortNum; }
|
|
|
|
ProcessMessage handleReceived(const meshtastic_MeshPacket &mp) override
|
|
{
|
|
(void)mp;
|
|
for (uint32_t i = 0; i < count; i++) {
|
|
meshtastic_MeshPacket *reply = router->allocForSending();
|
|
reply->to = LOCAL_NODE;
|
|
reply->decoded.portnum = meshtastic_PortNum_TEXT_MESSAGE_APP;
|
|
reply->decoded.request_id = 0x100 + i;
|
|
service->sendToMesh(reply); // default src == RX_SRC_LOCAL
|
|
}
|
|
return ProcessMessage::STOP;
|
|
}
|
|
|
|
private:
|
|
uint32_t count;
|
|
};
|
|
|
|
static TestModule *testModule;
|
|
static meshtastic_MeshPacket testPacket;
|
|
static MockNodeDB *mockNodeDB;
|
|
static MockMeshService *mockService;
|
|
static MockRouter *mockRouter;
|
|
static MockRoutingModule *mockRoutingModule;
|
|
static NeighborInfoModule *realNeighborInfoModule;
|
|
static RoutingModule *realRoutingModule;
|
|
static std::vector<MeshModule *> dispatchModules;
|
|
|
|
template <typename T> static T *registerDispatchModule(T *module)
|
|
{
|
|
dispatchModules.push_back(module);
|
|
return module;
|
|
}
|
|
|
|
// Swap the mocked RoutingModule for a real one. tearDown() owns the cleanup because a failed
|
|
// assertion longjmps out of the test, which would otherwise leave it registered in MeshModule::modules.
|
|
static void installRealRoutingModule()
|
|
{
|
|
realRoutingModule = new RoutingModule();
|
|
routingModule = realRoutingModule;
|
|
}
|
|
|
|
static meshtastic_MeshPacket makeRequest(meshtastic_PortNum port)
|
|
{
|
|
meshtastic_MeshPacket packet = meshtastic_MeshPacket_init_zero;
|
|
packet.from = REMOTE_NODE;
|
|
packet.to = LOCAL_NODE;
|
|
packet.id = 0x12345678;
|
|
packet.channel = 0;
|
|
packet.hop_start = 3;
|
|
packet.hop_limit = 3;
|
|
packet.which_payload_variant = meshtastic_MeshPacket_decoded_tag;
|
|
packet.decoded.portnum = port;
|
|
packet.decoded.want_response = true;
|
|
return packet;
|
|
}
|
|
|
|
static void dispatch(meshtastic_PortNum port)
|
|
{
|
|
meshtastic_MeshPacket request = makeRequest(port);
|
|
MeshModule::callModules(request);
|
|
}
|
|
|
|
// Dispatch a want_response==false trigger addressed to us through the real router, so a module's
|
|
// handler runs inside a live handleReceived() frame (handleDepth == 1) and any packet it sends is
|
|
// deferred. requestId is carried in decoded.request_id for modules that key on a generation.
|
|
static void dispatchTrigger(meshtastic_PortNum port, uint32_t requestId = 0)
|
|
{
|
|
meshtastic_MeshPacket trigger = meshtastic_MeshPacket_init_zero;
|
|
trigger.from = LOCAL_NODE;
|
|
trigger.to = LOCAL_NODE;
|
|
trigger.id = 0x7A190000 + requestId;
|
|
trigger.which_payload_variant = meshtastic_MeshPacket_decoded_tag;
|
|
trigger.decoded.portnum = port;
|
|
trigger.decoded.request_id = requestId;
|
|
service->sendToMesh(packetPool.allocCopy(trigger), RX_SRC_USER);
|
|
}
|
|
|
|
} // namespace
|
|
|
|
void setUp(void)
|
|
{
|
|
config = meshtastic_LocalConfig_init_zero;
|
|
moduleConfig = meshtastic_LocalModuleConfig_init_zero;
|
|
channelFile = meshtastic_ChannelFile_init_zero;
|
|
owner = meshtastic_User_init_zero;
|
|
myNodeInfo.my_node_num = LOCAL_NODE;
|
|
|
|
mockNodeDB = new MockNodeDB();
|
|
nodeDB = mockNodeDB;
|
|
myNodeInfo.my_node_num = LOCAL_NODE;
|
|
|
|
mockService = new MockMeshService();
|
|
service = mockService;
|
|
|
|
channels.initDefaults();
|
|
channels.onConfigChanged();
|
|
|
|
mockRouter = new MockRouter();
|
|
mockRouter->addInterface(std::unique_ptr<RadioInterface>(new MockRadioInterface()));
|
|
router = mockRouter;
|
|
|
|
mockRoutingModule = new MockRoutingModule();
|
|
routingModule = mockRoutingModule;
|
|
realRoutingModule = nullptr;
|
|
|
|
testModule = new TestModule();
|
|
memset(&testPacket, 0, sizeof(testPacket));
|
|
TestModule::currentRequest = &testPacket;
|
|
}
|
|
|
|
void tearDown(void)
|
|
{
|
|
TestModule::currentRequest = NULL;
|
|
|
|
for (auto it = dispatchModules.rbegin(); it != dispatchModules.rend(); ++it)
|
|
delete *it;
|
|
dispatchModules.clear();
|
|
|
|
delete realNeighborInfoModule;
|
|
realNeighborInfoModule = nullptr;
|
|
|
|
delete testModule;
|
|
testModule = nullptr;
|
|
|
|
delete realRoutingModule;
|
|
realRoutingModule = nullptr;
|
|
|
|
delete mockRoutingModule;
|
|
mockRoutingModule = nullptr;
|
|
routingModule = nullptr;
|
|
|
|
while (auto *status = mockService->getQueueStatusForPhone())
|
|
mockService->releaseQueueStatusToPool(status);
|
|
while (auto *toPhone = mockService->getForPhone())
|
|
mockService->releaseToPool(toPhone);
|
|
delete mockService;
|
|
mockService = nullptr;
|
|
service = nullptr;
|
|
|
|
delete mockRouter;
|
|
mockRouter = nullptr;
|
|
router = nullptr;
|
|
|
|
delete mockNodeDB;
|
|
mockNodeDB = nullptr;
|
|
nodeDB = nullptr;
|
|
}
|
|
|
|
// Zero-hop broadcast (hop_limit == hop_start): should be allowed
|
|
static void test_zeroHopBroadcast_isAllowed()
|
|
{
|
|
testPacket.to = NODENUM_BROADCAST;
|
|
testPacket.hop_start = 3;
|
|
testPacket.hop_limit = 3; // Not yet relayed
|
|
|
|
TEST_ASSERT_FALSE(testModule->isMultiHopBroadcastRequest());
|
|
}
|
|
|
|
// Multi-hop broadcast (hop_limit < hop_start): should be blocked
|
|
static void test_multiHopBroadcast_isBlocked()
|
|
{
|
|
testPacket.to = NODENUM_BROADCAST;
|
|
testPacket.hop_start = 7;
|
|
testPacket.hop_limit = 4; // Already relayed 3 hops
|
|
|
|
TEST_ASSERT_TRUE(testModule->isMultiHopBroadcastRequest());
|
|
}
|
|
|
|
// Direct message (not broadcast): should always be allowed regardless of hops
|
|
static void test_directMessage_isAllowed()
|
|
{
|
|
testPacket.to = 0x12345678; // Specific node
|
|
testPacket.hop_start = 7;
|
|
testPacket.hop_limit = 4;
|
|
|
|
TEST_ASSERT_FALSE(testModule->isMultiHopBroadcastRequest());
|
|
}
|
|
|
|
// Broadcast with hop_limit == 0 (fully relayed): should be blocked
|
|
static void test_fullyRelayedBroadcast_isBlocked()
|
|
{
|
|
testPacket.to = NODENUM_BROADCAST;
|
|
testPacket.hop_start = 3;
|
|
testPacket.hop_limit = 0;
|
|
|
|
TEST_ASSERT_TRUE(testModule->isMultiHopBroadcastRequest());
|
|
}
|
|
|
|
// No current request: should not crash, should return false
|
|
static void test_noCurrentRequest_isAllowed()
|
|
{
|
|
TestModule::currentRequest = NULL;
|
|
|
|
TEST_ASSERT_FALSE(testModule->isMultiHopBroadcastRequest());
|
|
}
|
|
|
|
// Broadcast with hop_start == 0 (legacy or local): should be allowed
|
|
static void test_legacyPacket_zeroHopStart_isAllowed()
|
|
{
|
|
testPacket.to = NODENUM_BROADCAST;
|
|
testPacket.hop_start = 0;
|
|
testPacket.hop_limit = 0;
|
|
|
|
// hop_limit == hop_start, so not multi-hop
|
|
TEST_ASSERT_FALSE(testModule->isMultiHopBroadcastRequest());
|
|
}
|
|
|
|
// Single hop relayed broadcast (hop_limit = hop_start - 1): should be blocked
|
|
static void test_singleHopRelayedBroadcast_isBlocked()
|
|
{
|
|
testPacket.to = NODENUM_BROADCAST;
|
|
testPacket.hop_start = 3;
|
|
testPacket.hop_limit = 2;
|
|
|
|
TEST_ASSERT_TRUE(testModule->isMultiHopBroadcastRequest());
|
|
}
|
|
|
|
static void test_replyPortMatches_ownPort()
|
|
{
|
|
meshtastic_MeshPacket request = makeRequest(meshtastic_PortNum_TELEMETRY_APP);
|
|
|
|
TEST_ASSERT_TRUE(MeshModule::replyPortMatches(meshtastic_PortNum_TELEMETRY_APP, request));
|
|
}
|
|
|
|
static void test_replyPortMatches_neighborInfoVsTelemetry()
|
|
{
|
|
meshtastic_MeshPacket request = makeRequest(meshtastic_PortNum_TELEMETRY_APP);
|
|
|
|
TEST_ASSERT_FALSE(MeshModule::replyPortMatches(meshtastic_PortNum_NEIGHBORINFO_APP, request));
|
|
}
|
|
|
|
static void test_replyPortMatches_positionRequest()
|
|
{
|
|
meshtastic_MeshPacket request = makeRequest(meshtastic_PortNum_POSITION_APP);
|
|
|
|
TEST_ASSERT_TRUE(MeshModule::replyPortMatches(meshtastic_PortNum_POSITION_APP, request));
|
|
}
|
|
|
|
static void test_replyPortMatches_unknownModulePort()
|
|
{
|
|
meshtastic_MeshPacket request = makeRequest(meshtastic_PortNum_TELEMETRY_APP);
|
|
|
|
TEST_ASSERT_FALSE(MeshModule::replyPortMatches(meshtastic_PortNum_UNKNOWN_APP, request));
|
|
}
|
|
|
|
static void test_replyPortMatches_unknownRequestPort()
|
|
{
|
|
meshtastic_MeshPacket request = makeRequest(meshtastic_PortNum_UNKNOWN_APP);
|
|
|
|
TEST_ASSERT_FALSE(MeshModule::replyPortMatches(meshtastic_PortNum_TELEMETRY_APP, request));
|
|
}
|
|
|
|
static void test_dispatch_foreignPortOffenderCannotShadowOwner()
|
|
{
|
|
auto *offender = registerDispatchModule(new SyntheticReplyModule("neighbor-shaped", meshtastic_PortNum_NEIGHBORINFO_APP,
|
|
meshtastic_PortNum_NEIGHBORINFO_APP, true));
|
|
auto *owner = registerDispatchModule(
|
|
new SyntheticReplyModule("telemetry-owner", meshtastic_PortNum_TELEMETRY_APP, meshtastic_PortNum_TELEMETRY_APP));
|
|
|
|
dispatch(meshtastic_PortNum_TELEMETRY_APP);
|
|
|
|
TEST_ASSERT_EQUAL_UINT32(0, offender->allocReplyCalls);
|
|
TEST_ASSERT_EQUAL_UINT32(1, owner->allocReplyCalls);
|
|
TEST_ASSERT_EQUAL_UINT32(1, mockRouter->sentPackets.size());
|
|
TEST_ASSERT_EQUAL(meshtastic_PortNum_TELEMETRY_APP, mockRouter->sentPackets[0].decoded.portnum);
|
|
TEST_ASSERT_EQUAL_UINT32(0, mockRoutingModule->ackNaks.size());
|
|
}
|
|
|
|
static void test_dispatch_ownerPortStillReplies()
|
|
{
|
|
auto *offender = registerDispatchModule(new SyntheticReplyModule("neighbor-shaped", meshtastic_PortNum_NEIGHBORINFO_APP,
|
|
meshtastic_PortNum_NEIGHBORINFO_APP, true));
|
|
auto *owner = registerDispatchModule(
|
|
new SyntheticReplyModule("telemetry-owner", meshtastic_PortNum_TELEMETRY_APP, meshtastic_PortNum_TELEMETRY_APP));
|
|
|
|
dispatch(meshtastic_PortNum_NEIGHBORINFO_APP);
|
|
|
|
TEST_ASSERT_EQUAL_UINT32(1, offender->allocReplyCalls);
|
|
TEST_ASSERT_EQUAL_UINT32(0, owner->allocReplyCalls);
|
|
TEST_ASSERT_EQUAL_UINT32(1, mockRouter->sentPackets.size());
|
|
TEST_ASSERT_EQUAL(meshtastic_PortNum_NEIGHBORINFO_APP, mockRouter->sentPackets[0].decoded.portnum);
|
|
TEST_ASSERT_EQUAL_UINT32(0, mockRoutingModule->ackNaks.size());
|
|
}
|
|
|
|
static void test_dispatch_crossPortReplyUsesRequestOwner()
|
|
{
|
|
auto *position = registerDispatchModule(
|
|
new SyntheticReplyModule("position-owner", meshtastic_PortNum_POSITION_APP, meshtastic_PortNum_ATAK_PLUGIN_V2));
|
|
|
|
dispatch(meshtastic_PortNum_POSITION_APP);
|
|
|
|
TEST_ASSERT_EQUAL_UINT32(1, position->allocReplyCalls);
|
|
TEST_ASSERT_EQUAL_UINT32(1, mockRouter->sentPackets.size());
|
|
TEST_ASSERT_EQUAL(meshtastic_PortNum_ATAK_PLUGIN_V2, mockRouter->sentPackets[0].decoded.portnum);
|
|
TEST_ASSERT_EQUAL_UINT32(0, mockRoutingModule->ackNaks.size());
|
|
}
|
|
|
|
static void test_dispatch_foreignPortObserverCanSuppressNak()
|
|
{
|
|
auto *observer = registerDispatchModule(new ObservingIgnoreModule());
|
|
|
|
dispatch(meshtastic_PortNum_TEXT_MESSAGE_APP);
|
|
|
|
TEST_ASSERT_EQUAL_UINT32(0, observer->allocReplyCalls);
|
|
TEST_ASSERT_EQUAL_UINT32(0, mockRouter->sentPackets.size());
|
|
TEST_ASSERT_EQUAL_UINT32(0, mockRoutingModule->ackNaks.size());
|
|
}
|
|
|
|
static void test_dispatch_noResponderSendsNak()
|
|
{
|
|
dispatch(meshtastic_PortNum_TELEMETRY_APP);
|
|
|
|
TEST_ASSERT_EQUAL_UINT32(0, mockRouter->sentPackets.size());
|
|
TEST_ASSERT_EQUAL_UINT32(1, mockRoutingModule->ackNaks.size());
|
|
TEST_ASSERT_EQUAL(meshtastic_Routing_Error_NO_RESPONSE, mockRoutingModule->ackNaks[0].error);
|
|
TEST_ASSERT_EQUAL_HEX32(REMOTE_NODE, mockRoutingModule->ackNaks[0].to);
|
|
}
|
|
|
|
static void test_dispatch_ignoreRequestIsClearedPerPacket()
|
|
{
|
|
auto *ignoring = registerDispatchModule(new ReplyIgnoreModule());
|
|
|
|
dispatch(meshtastic_PortNum_NEIGHBORINFO_APP);
|
|
|
|
TEST_ASSERT_EQUAL_UINT32(1, ignoring->allocReplyCalls);
|
|
TEST_ASSERT_EQUAL_UINT32(0, mockRouter->sentPackets.size());
|
|
TEST_ASSERT_EQUAL_UINT32(0, mockRoutingModule->ackNaks.size());
|
|
|
|
dispatch(meshtastic_PortNum_TELEMETRY_APP);
|
|
|
|
TEST_ASSERT_EQUAL_UINT32(1, ignoring->allocReplyCalls);
|
|
TEST_ASSERT_EQUAL_UINT32(0, mockRouter->sentPackets.size());
|
|
TEST_ASSERT_EQUAL_UINT32(1, mockRoutingModule->ackNaks.size());
|
|
TEST_ASSERT_EQUAL(meshtastic_Routing_Error_NO_RESPONSE, mockRoutingModule->ackNaks[0].error);
|
|
}
|
|
|
|
static void test_dispatch_realNeighborInfoCannotShadowTelemetryOwner()
|
|
{
|
|
moduleConfig.neighbor_info.enabled = true;
|
|
realNeighborInfoModule = new NeighborInfoModule();
|
|
registerDispatchModule(
|
|
new SyntheticReplyModule("telemetry-owner", meshtastic_PortNum_TELEMETRY_APP, meshtastic_PortNum_TELEMETRY_APP));
|
|
|
|
dispatch(meshtastic_PortNum_TELEMETRY_APP);
|
|
|
|
TEST_ASSERT_EQUAL_UINT32(1, mockRouter->sentPackets.size());
|
|
TEST_ASSERT_EQUAL(meshtastic_PortNum_TELEMETRY_APP, mockRouter->sentPackets[0].decoded.portnum);
|
|
TEST_ASSERT_EQUAL_UINT32(0, mockRoutingModule->ackNaks.size());
|
|
}
|
|
|
|
// A reply addressed to ourselves reaches the phone queue, with SHOULD_RELEASE reported as success.
|
|
static void test_localReplyToSelf_isDeliveredToPhone()
|
|
{
|
|
meshtastic_MeshPacket reply = meshtastic_MeshPacket_init_zero;
|
|
reply.from = LOCAL_NODE;
|
|
reply.to = LOCAL_NODE;
|
|
reply.id = 0xABCD1234;
|
|
reply.which_payload_variant = meshtastic_MeshPacket_decoded_tag;
|
|
reply.decoded.portnum = meshtastic_PortNum_ADMIN_APP;
|
|
reply.decoded.request_id = 0x12345678;
|
|
|
|
service->sendToMesh(packetPool.allocCopy(reply)); // default src == RX_SRC_LOCAL, as callModules sends replies
|
|
|
|
meshtastic_MeshPacket *toPhone = mockService->getForPhone();
|
|
TEST_ASSERT_NOT_NULL(toPhone);
|
|
TEST_ASSERT_EQUAL_UINT32(0xABCD1234, toPhone->id);
|
|
TEST_ASSERT_EQUAL_UINT32(0x12345678, toPhone->decoded.request_id);
|
|
mockService->releaseToPool(toPhone);
|
|
TEST_ASSERT_NULL(mockService->getForPhone()); // exactly one delivery
|
|
|
|
meshtastic_QueueStatus *qs = mockService->getQueueStatusForPhone();
|
|
TEST_ASSERT_NOT_NULL(qs);
|
|
TEST_ASSERT_EQUAL(ERRNO_OK, qs->res);
|
|
mockService->releaseQueueStatusToPool(qs);
|
|
|
|
TEST_ASSERT_EQUAL_UINT32(0, mockRouter->sentPackets.size()); // nothing went toward the radio
|
|
}
|
|
|
|
// handleFromRadio() is private to MeshService, which befriends RoutingModule and, under
|
|
// PIO_UNIT_TESTING, this seam.
|
|
class MeshServicePhoneDeliveryTest
|
|
{
|
|
public:
|
|
static void deliver(const meshtastic_MeshPacket &p) { service->handleFromRadio(&p); }
|
|
};
|
|
|
|
static void test_handleFromRadio_remotePacketReachesPhone()
|
|
{
|
|
meshtastic_MeshPacket rx = meshtastic_MeshPacket_init_zero;
|
|
rx.from = REMOTE_NODE;
|
|
rx.to = NODENUM_BROADCAST;
|
|
rx.id = 0x0BADF00D;
|
|
rx.which_payload_variant = meshtastic_MeshPacket_decoded_tag;
|
|
rx.decoded.portnum = meshtastic_PortNum_TEXT_MESSAGE_APP;
|
|
|
|
MeshServicePhoneDeliveryTest::deliver(rx);
|
|
|
|
meshtastic_MeshPacket *toPhone = mockService->getForPhone();
|
|
TEST_ASSERT_NOT_NULL(toPhone);
|
|
TEST_ASSERT_EQUAL_UINT32(0x0BADF00D, toPhone->id);
|
|
mockService->releaseToPool(toPhone);
|
|
TEST_ASSERT_NULL(mockService->getForPhone());
|
|
}
|
|
|
|
// A packet we originated, coming back around, must not be echoed to the client that sent it.
|
|
static void test_handleFromRadio_ownPacketIsNotEchoedToPhone()
|
|
{
|
|
meshtastic_MeshPacket ours = meshtastic_MeshPacket_init_zero;
|
|
ours.from = LOCAL_NODE;
|
|
ours.to = NODENUM_BROADCAST;
|
|
ours.id = 0x5E1F0001;
|
|
ours.which_payload_variant = meshtastic_MeshPacket_decoded_tag;
|
|
ours.decoded.portnum = meshtastic_PortNum_TEXT_MESSAGE_APP;
|
|
|
|
MeshServicePhoneDeliveryTest::deliver(ours);
|
|
TEST_ASSERT_NULL(mockService->getForPhone());
|
|
|
|
// Same for the from==0 spelling handleToRadio stamps on phone-originated packets.
|
|
ours.from = 0;
|
|
ours.id = 0x5E1F0002;
|
|
MeshServicePhoneDeliveryTest::deliver(ours);
|
|
TEST_ASSERT_NULL(mockService->getForPhone());
|
|
}
|
|
|
|
// A packet from us *addressed to us* is locally-generated feedback, not an echo, and must still be
|
|
// delivered - suppressing it would silently drop every ACK/NAK the client relies on.
|
|
static void test_handleFromRadio_ownPacketAddressedToUsReachesPhone()
|
|
{
|
|
meshtastic_MeshPacket ack = meshtastic_MeshPacket_init_zero;
|
|
ack.from = LOCAL_NODE;
|
|
ack.to = LOCAL_NODE;
|
|
ack.id = 0x5E1F0003;
|
|
ack.which_payload_variant = meshtastic_MeshPacket_decoded_tag;
|
|
ack.decoded.portnum = meshtastic_PortNum_ROUTING_APP;
|
|
ack.decoded.request_id = 0x0C0FFEE0;
|
|
|
|
MeshServicePhoneDeliveryTest::deliver(ack);
|
|
|
|
meshtastic_MeshPacket *toPhone = mockService->getForPhone();
|
|
TEST_ASSERT_NOT_NULL(toPhone);
|
|
TEST_ASSERT_EQUAL_UINT32(0x0C0FFEE0, toPhone->decoded.request_id);
|
|
mockService->releaseToPool(toPhone);
|
|
TEST_ASSERT_NULL(mockService->getForPhone());
|
|
}
|
|
|
|
// sendAckNak stamps from == our nodenum and to == us, and sendLocal defaults to RX_SRC_RADIO, so the
|
|
// loopback gate never applies and only handleFromRadio's filter gates the implicit ACK / NAK path.
|
|
static void test_localAckNak_reachesPhoneViaRealRoutingModule()
|
|
{
|
|
installRealRoutingModule();
|
|
|
|
realRoutingModule->sendAckNak(meshtastic_Routing_Error_NONE, LOCAL_NODE, 0xFEEDBEEF, 0);
|
|
|
|
meshtastic_MeshPacket *toPhone = mockService->getForPhone();
|
|
TEST_ASSERT_NOT_NULL(toPhone);
|
|
TEST_ASSERT_EQUAL(meshtastic_PortNum_ROUTING_APP, toPhone->decoded.portnum);
|
|
TEST_ASSERT_EQUAL_UINT32(0xFEEDBEEF, toPhone->decoded.request_id);
|
|
TEST_ASSERT_EQUAL_UINT32(LOCAL_NODE, toPhone->to);
|
|
TEST_ASSERT_EQUAL_UINT32(LOCAL_NODE, toPhone->from);
|
|
mockService->releaseToPool(toPhone);
|
|
}
|
|
|
|
// #10767: the implicit ACK for an overheard rebroadcast of our own packet carries that copy's
|
|
// relaying node and the link metrics we heard it at, so the phone can attribute them to the relayer.
|
|
// rx_rssi has explicit presence, so has_rx_rssi has to travel with it or the reading never encodes.
|
|
static void test_localAck_carriesRelaySourceToPhone()
|
|
{
|
|
installRealRoutingModule();
|
|
|
|
meshtastic_MeshPacket overheard = meshtastic_MeshPacket_init_zero;
|
|
overheard.from = LOCAL_NODE;
|
|
overheard.to = REMOTE_NODE;
|
|
overheard.id = 0xFEEDBEEF;
|
|
overheard.relay_node = 0xAB;
|
|
overheard.has_rx_rssi = true;
|
|
overheard.rx_rssi = -93;
|
|
overheard.rx_snr = 4.75f;
|
|
|
|
realRoutingModule->sendAckNak(meshtastic_Routing_Error_NONE, LOCAL_NODE, overheard.id, 0, /*hopLimit=*/0,
|
|
/*ackWantsAck=*/false, &overheard);
|
|
|
|
meshtastic_MeshPacket *toPhone = mockService->getForPhone();
|
|
TEST_ASSERT_NOT_NULL(toPhone);
|
|
TEST_ASSERT_EQUAL_UINT32(0xFEEDBEEF, toPhone->decoded.request_id);
|
|
TEST_ASSERT_EQUAL_HEX8(0xAB, toPhone->relay_node);
|
|
TEST_ASSERT_TRUE(toPhone->has_rx_rssi);
|
|
TEST_ASSERT_EQUAL_INT32(-93, toPhone->rx_rssi);
|
|
TEST_ASSERT_EQUAL_FLOAT(4.75f, toPhone->rx_snr);
|
|
mockService->releaseToPool(toPhone);
|
|
}
|
|
|
|
// Every other ACK/NAK passes no relay source and must reach the phone with the relay fields clear,
|
|
// so the client is never told a relayer we did not hear.
|
|
static void test_localAck_withoutRelaySource_leavesRelayFieldsUnset()
|
|
{
|
|
installRealRoutingModule();
|
|
|
|
realRoutingModule->sendAckNak(meshtastic_Routing_Error_NONE, LOCAL_NODE, 0x0BADF00D, 0);
|
|
|
|
meshtastic_MeshPacket *toPhone = mockService->getForPhone();
|
|
TEST_ASSERT_NOT_NULL(toPhone);
|
|
TEST_ASSERT_EQUAL_UINT32(0x0BADF00D, toPhone->decoded.request_id);
|
|
TEST_ASSERT_EQUAL_HEX8(NO_RELAY_NODE, toPhone->relay_node);
|
|
TEST_ASSERT_FALSE(toPhone->has_rx_rssi);
|
|
mockService->releaseToPool(toPhone);
|
|
}
|
|
|
|
// The mirror of the above: a broadcast we originated, heard back off the mesh, must not reach the
|
|
// phone even though it travels the same RoutingModule path.
|
|
static void test_ownBroadcastEcho_isDroppedByRealRoutingModule()
|
|
{
|
|
installRealRoutingModule();
|
|
|
|
meshtastic_MeshPacket echo = meshtastic_MeshPacket_init_zero;
|
|
echo.from = LOCAL_NODE;
|
|
echo.to = NODENUM_BROADCAST;
|
|
echo.id = 0x5E1F0004;
|
|
echo.which_payload_variant = meshtastic_MeshPacket_decoded_tag;
|
|
echo.decoded.portnum = meshtastic_PortNum_TEXT_MESSAGE_APP;
|
|
|
|
MeshModule::callModules(echo, RX_SRC_RADIO);
|
|
|
|
TEST_ASSERT_NULL(mockService->getForPhone());
|
|
}
|
|
|
|
// Full loop: a phone-originated want_response request (from == 0, RX_SRC_USER) dispatched
|
|
// through the real router must produce a module reply that reaches the phone queue.
|
|
static void test_phoneRequest_replyReachesPhone()
|
|
{
|
|
auto *replyOwner = registerDispatchModule(
|
|
new SyntheticReplyModule("private-owner", meshtastic_PortNum_PRIVATE_APP, meshtastic_PortNum_PRIVATE_APP));
|
|
|
|
meshtastic_MeshPacket request = meshtastic_MeshPacket_init_zero;
|
|
request.from = 0; // phone-originated, as MeshService::handleToRadio stamps it
|
|
request.to = LOCAL_NODE;
|
|
request.id = 0x5EED0001;
|
|
request.which_payload_variant = meshtastic_MeshPacket_decoded_tag;
|
|
request.decoded.portnum = meshtastic_PortNum_PRIVATE_APP;
|
|
request.decoded.want_response = true;
|
|
|
|
service->sendToMesh(packetPool.allocCopy(request), RX_SRC_USER);
|
|
|
|
TEST_ASSERT_EQUAL_UINT32(1, replyOwner->allocReplyCalls);
|
|
|
|
meshtastic_MeshPacket *toPhone = mockService->getForPhone();
|
|
TEST_ASSERT_NOT_NULL(toPhone);
|
|
TEST_ASSERT_EQUAL(meshtastic_PortNum_PRIVATE_APP, toPhone->decoded.portnum);
|
|
TEST_ASSERT_EQUAL_UINT32(0x5EED0001, toPhone->decoded.request_id);
|
|
TEST_ASSERT_EQUAL_UINT32(LOCAL_NODE, toPhone->to);
|
|
mockService->releaseToPool(toPhone);
|
|
TEST_ASSERT_NULL(mockService->getForPhone()); // the request itself must not echo back
|
|
|
|
// One QueueStatus for the request, one for the reply, both reporting success
|
|
uint32_t statuses = 0;
|
|
while (auto *qs = mockService->getQueueStatusForPhone()) {
|
|
TEST_ASSERT_EQUAL(ERRNO_OK, qs->res);
|
|
mockService->releaseQueueStatusToPool(qs);
|
|
statuses++;
|
|
}
|
|
TEST_ASSERT_EQUAL_UINT32(2, statuses);
|
|
|
|
TEST_ASSERT_EQUAL_UINT32(0, mockRouter->sentPackets.size());
|
|
TEST_ASSERT_EQUAL_UINT32(0, mockRoutingModule->ackNaks.size());
|
|
}
|
|
|
|
// A module that fans out a self-addressed reply and a local broadcast from inside callModules()
|
|
// must not re-enter handleReceived(): the sends are deferred and drained flat (max depth 1).
|
|
static void test_nestedLocalSend_isDeferred_notReentrant()
|
|
{
|
|
auto *mod = registerDispatchModule(new NestedFanoutModule());
|
|
|
|
dispatchTrigger(meshtastic_PortNum_PRIVATE_APP);
|
|
|
|
// The module ran once and was never re-entered; the drain left nothing behind.
|
|
TEST_ASSERT_EQUAL_UINT32(1, mod->handleCalls);
|
|
TEST_ASSERT_EQUAL_UINT8(1, mockRouter->maxHandleDepthObserved);
|
|
TEST_ASSERT_EQUAL_UINT8(0, mockRouter->deferredLocalPending());
|
|
|
|
// The self-addressed reply reached the phone exactly once (composition with #11185's ccToPhone).
|
|
meshtastic_MeshPacket *toPhone = mockService->getForPhone();
|
|
TEST_ASSERT_NOT_NULL(toPhone);
|
|
TEST_ASSERT_EQUAL_UINT32(LOCAL_NODE, toPhone->to);
|
|
TEST_ASSERT_EQUAL_UINT32(0xBEEF, toPhone->decoded.request_id);
|
|
mockService->releaseToPool(toPhone);
|
|
TEST_ASSERT_NULL(mockService->getForPhone()); // the broadcast did not also go to the phone
|
|
|
|
// The broadcast still reached the radio TX path exactly once.
|
|
TEST_ASSERT_EQUAL_UINT32(1, mockRouter->sentPackets.size());
|
|
TEST_ASSERT_TRUE(isBroadcast(mockRouter->sentPackets[0].to));
|
|
}
|
|
|
|
// A drained packet whose own module sends again is picked up by the same drain pass, so a chain of
|
|
// local sends is processed breadth-first with the stack held flat (max depth 1).
|
|
static void test_deferredChain_drainsBreadthFirst()
|
|
{
|
|
auto *mod = registerDispatchModule(new ChainSendModule());
|
|
|
|
dispatchTrigger(meshtastic_PortNum_PRIVATE_APP, 1); // generation 1
|
|
|
|
// Generations 1 -> 2 -> 3 were all processed in the single outermost drain, never nesting.
|
|
TEST_ASSERT_EQUAL_UINT32(3, mod->handleCalls);
|
|
TEST_ASSERT_EQUAL_UINT32(3, mod->maxGeneration);
|
|
TEST_ASSERT_EQUAL_UINT8(1, mockRouter->maxHandleDepthObserved);
|
|
TEST_ASSERT_EQUAL_UINT8(0, mockRouter->deferredLocalPending());
|
|
}
|
|
|
|
// Overflowing the fixed deferred queue drops the excess deferrals gracefully: no crash, no leak,
|
|
// depth still capped, and every send's phone cc still happens (return codes unchanged).
|
|
static void test_deferredQueueOverflow_dropsGracefully()
|
|
{
|
|
const uint32_t burst = 6; // deferredLocalCapacity (4) + 2 - keep in sync with Router.h
|
|
|
|
registerDispatchModule(new BurstSendModule(burst));
|
|
|
|
dispatchTrigger(meshtastic_PortNum_PRIVATE_APP);
|
|
|
|
// Two deferrals were dropped (queue full) and nothing re-entered handleReceived().
|
|
TEST_ASSERT_EQUAL_UINT32(2, mockRouter->deferredLocalDropped);
|
|
TEST_ASSERT_EQUAL_UINT8(1, mockRouter->maxHandleDepthObserved);
|
|
TEST_ASSERT_EQUAL_UINT8(0, mockRouter->deferredLocalPending()); // drained clean
|
|
|
|
// Return codes were unchanged: every reply still cc'd to the phone, dropped deferral or not.
|
|
uint32_t delivered = 0;
|
|
while (auto *toPhone = mockService->getForPhone()) {
|
|
mockService->releaseToPool(toPhone);
|
|
delivered++;
|
|
}
|
|
TEST_ASSERT_EQUAL_UINT32(burst, delivered);
|
|
}
|
|
|
|
// NeighborInfo: relays forward the payload unmodified (rewriting it breaks the sender's XEdDSA signature), so a
|
|
// receiver attributes each copy to the node it heard it from using the packet header, not last_sent_by_id.
|
|
class NeighborInfoModuleTestShim : public NeighborInfoModule
|
|
{
|
|
public:
|
|
using MeshModule::alterReceived;
|
|
using NeighborInfoModule::collectNeighborInfo;
|
|
using NeighborInfoModule::updateNeighbors;
|
|
};
|
|
|
|
static constexpr NodeNum NI_NEIGHBOR = 0x33333333;
|
|
|
|
static NeighborInfoModuleTestShim *makeNeighborInfoModule()
|
|
{
|
|
moduleConfig.neighbor_info.enabled = true;
|
|
moduleConfig.neighbor_info.update_interval = 3600;
|
|
auto *module = new NeighborInfoModuleTestShim();
|
|
realNeighborInfoModule = module; // tearDown() deletes it
|
|
return module;
|
|
}
|
|
|
|
static meshtastic_MeshPacket makeNeighborInfoPacket(NodeNum lastSentBy, uint8_t hopStart, uint8_t hopLimit, uint8_t relayNode,
|
|
meshtastic_NeighborInfo *ni)
|
|
{
|
|
*ni = meshtastic_NeighborInfo_init_zero;
|
|
ni->node_id = REMOTE_NODE;
|
|
ni->last_sent_by_id = lastSentBy;
|
|
ni->node_broadcast_interval_secs = 3600;
|
|
ni->neighbors_count = 1;
|
|
ni->neighbors[0].node_id = NI_NEIGHBOR;
|
|
ni->neighbors[0].snr = 5.0f;
|
|
|
|
meshtastic_MeshPacket p = meshtastic_MeshPacket_init_zero;
|
|
p.from = REMOTE_NODE;
|
|
p.to = NODENUM_BROADCAST;
|
|
p.id = 0x0A0B0C0D;
|
|
p.hop_start = hopStart;
|
|
p.hop_limit = hopLimit;
|
|
p.relay_node = relayNode;
|
|
p.rx_snr = 7.0f;
|
|
p.which_payload_variant = meshtastic_MeshPacket_decoded_tag;
|
|
p.decoded.portnum = meshtastic_PortNum_NEIGHBORINFO_APP;
|
|
p.decoded.payload.size =
|
|
pb_encode_to_bytes(p.decoded.payload.bytes, sizeof(p.decoded.payload.bytes), &meshtastic_NeighborInfo_msg, ni);
|
|
return p;
|
|
}
|
|
|
|
// Our recorded neighbours, as we would broadcast them.
|
|
static meshtastic_NeighborInfo collectedNeighbors(NeighborInfoModuleTestShim *module)
|
|
{
|
|
meshtastic_NeighborInfo out = meshtastic_NeighborInfo_init_zero;
|
|
module->collectNeighborInfo(&out);
|
|
return out;
|
|
}
|
|
|
|
static void test_neighborInfo_relayedPayloadIsNotModified()
|
|
{
|
|
auto *module = makeNeighborInfoModule();
|
|
meshtastic_NeighborInfo ni;
|
|
meshtastic_MeshPacket p = makeNeighborInfoPacket(REMOTE_NODE, 3, 2, 0x44, &ni);
|
|
const meshtastic_Data_payload_t original = p.decoded.payload;
|
|
|
|
module->alterReceived(p);
|
|
|
|
TEST_ASSERT_EQUAL_UINT32(original.size, p.decoded.payload.size);
|
|
TEST_ASSERT_EQUAL_MEMORY(original.bytes, p.decoded.payload.bytes, original.size);
|
|
}
|
|
|
|
static void test_neighborInfo_directCopyRecordsSenderAndOursOmitsLastSentBy()
|
|
{
|
|
auto *module = makeNeighborInfoModule();
|
|
meshtastic_NeighborInfo ni;
|
|
meshtastic_MeshPacket p = makeNeighborInfoPacket(0, 3, 3, 0x22, &ni);
|
|
|
|
module->updateNeighbors(p, &ni);
|
|
|
|
meshtastic_NeighborInfo out = collectedNeighbors(module);
|
|
TEST_ASSERT_EQUAL_UINT32(1, out.neighbors_count);
|
|
TEST_ASSERT_EQUAL_HEX32(REMOTE_NODE, out.neighbors[0].node_id);
|
|
TEST_ASSERT_EQUAL_HEX32(LOCAL_NODE, out.node_id);
|
|
TEST_ASSERT_EQUAL_HEX32(0, out.last_sent_by_id);
|
|
}
|
|
|
|
static void test_neighborInfo_olderRelayRewriteMatchingHeaderIsTrusted()
|
|
{
|
|
auto *module = makeNeighborInfoModule();
|
|
const NodeNum oldRelay = 0x55555544;
|
|
meshtastic_NeighborInfo ni;
|
|
meshtastic_MeshPacket p = makeNeighborInfoPacket(oldRelay, 3, 2, 0x44, &ni);
|
|
|
|
module->updateNeighbors(p, &ni);
|
|
|
|
meshtastic_NeighborInfo out = collectedNeighbors(module);
|
|
TEST_ASSERT_EQUAL_UINT32(1, out.neighbors_count);
|
|
TEST_ASSERT_EQUAL_HEX32(oldRelay, out.neighbors[0].node_id);
|
|
}
|
|
|
|
static void test_neighborInfo_relayResolvedFromHeaderByte()
|
|
{
|
|
auto *module = makeNeighborInfoModule();
|
|
const NodeNum relay = 0x77777766;
|
|
meshtastic_NodeInfoLite *node = nodeDB->getOrCreateMeshNode(relay);
|
|
TEST_ASSERT_NOT_NULL(node);
|
|
node->has_hops_away = true;
|
|
node->hops_away = 0;
|
|
node->last_heard = getTime();
|
|
|
|
// Unmodified payload from an older sender: last_sent_by_id still names the originator.
|
|
meshtastic_NeighborInfo ni;
|
|
meshtastic_MeshPacket p = makeNeighborInfoPacket(REMOTE_NODE, 3, 2, 0x66, &ni);
|
|
|
|
module->updateNeighbors(p, &ni);
|
|
|
|
meshtastic_NeighborInfo out = collectedNeighbors(module);
|
|
TEST_ASSERT_EQUAL_UINT32(1, out.neighbors_count);
|
|
TEST_ASSERT_EQUAL_HEX32(relay, out.neighbors[0].node_id);
|
|
}
|
|
|
|
static void test_neighborInfo_unresolvableRelayRecordsNothing()
|
|
{
|
|
auto *module = makeNeighborInfoModule();
|
|
meshtastic_NeighborInfo ni;
|
|
meshtastic_MeshPacket p = makeNeighborInfoPacket(0, 3, 2, 0x99, &ni);
|
|
|
|
module->updateNeighbors(p, &ni);
|
|
|
|
TEST_ASSERT_EQUAL_UINT32(0, collectedNeighbors(module).neighbors_count);
|
|
}
|
|
|
|
void setup()
|
|
{
|
|
initializeTestEnvironment();
|
|
|
|
UNITY_BEGIN();
|
|
RUN_TEST(test_zeroHopBroadcast_isAllowed);
|
|
RUN_TEST(test_multiHopBroadcast_isBlocked);
|
|
RUN_TEST(test_directMessage_isAllowed);
|
|
RUN_TEST(test_fullyRelayedBroadcast_isBlocked);
|
|
RUN_TEST(test_noCurrentRequest_isAllowed);
|
|
RUN_TEST(test_legacyPacket_zeroHopStart_isAllowed);
|
|
RUN_TEST(test_singleHopRelayedBroadcast_isBlocked);
|
|
RUN_TEST(test_replyPortMatches_ownPort);
|
|
RUN_TEST(test_replyPortMatches_neighborInfoVsTelemetry);
|
|
RUN_TEST(test_replyPortMatches_positionRequest);
|
|
RUN_TEST(test_replyPortMatches_unknownModulePort);
|
|
RUN_TEST(test_replyPortMatches_unknownRequestPort);
|
|
RUN_TEST(test_dispatch_foreignPortOffenderCannotShadowOwner);
|
|
RUN_TEST(test_dispatch_ownerPortStillReplies);
|
|
RUN_TEST(test_dispatch_crossPortReplyUsesRequestOwner);
|
|
RUN_TEST(test_dispatch_foreignPortObserverCanSuppressNak);
|
|
RUN_TEST(test_dispatch_noResponderSendsNak);
|
|
RUN_TEST(test_dispatch_ignoreRequestIsClearedPerPacket);
|
|
RUN_TEST(test_dispatch_realNeighborInfoCannotShadowTelemetryOwner);
|
|
RUN_TEST(test_localReplyToSelf_isDeliveredToPhone);
|
|
RUN_TEST(test_handleFromRadio_remotePacketReachesPhone);
|
|
RUN_TEST(test_handleFromRadio_ownPacketIsNotEchoedToPhone);
|
|
RUN_TEST(test_handleFromRadio_ownPacketAddressedToUsReachesPhone);
|
|
RUN_TEST(test_localAckNak_reachesPhoneViaRealRoutingModule);
|
|
RUN_TEST(test_localAck_carriesRelaySourceToPhone);
|
|
RUN_TEST(test_localAck_withoutRelaySource_leavesRelayFieldsUnset);
|
|
RUN_TEST(test_ownBroadcastEcho_isDroppedByRealRoutingModule);
|
|
RUN_TEST(test_phoneRequest_replyReachesPhone);
|
|
RUN_TEST(test_nestedLocalSend_isDeferred_notReentrant);
|
|
RUN_TEST(test_deferredChain_drainsBreadthFirst);
|
|
RUN_TEST(test_deferredQueueOverflow_dropsGracefully);
|
|
RUN_TEST(test_neighborInfo_relayedPayloadIsNotModified);
|
|
RUN_TEST(test_neighborInfo_directCopyRecordsSenderAndOursOmitsLastSentBy);
|
|
RUN_TEST(test_neighborInfo_olderRelayRewriteMatchingHeaderIsTrusted);
|
|
RUN_TEST(test_neighborInfo_relayResolvedFromHeaderByte);
|
|
RUN_TEST(test_neighborInfo_unresolvableRelayRecordsNothing);
|
|
exit(UNITY_END());
|
|
}
|
|
|
|
void loop() {}
|