Files
firmware/test/test_mesh_module/test_main.cpp
2026-07-21 10:19:38 +02:00

502 lines
15 KiB
C++

#include "MeshModule.h"
#include "MeshTypes.h"
#include "TestUtil.h"
#include <unity.h>
#include "configuration.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:
~MockRouter()
{
delete cryptLock;
cryptLock = nullptr;
}
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) override
{
(void)hopLimit;
(void)ackWantsAck;
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;
}
};
static TestModule *testModule;
static meshtastic_MeshPacket testPacket;
static MockNodeDB *mockNodeDB;
static MockMeshService *mockService;
static MockRouter *mockRouter;
static MockRoutingModule *mockRoutingModule;
static NeighborInfoModule *realNeighborInfoModule;
static std::vector<MeshModule *> dispatchModules;
template <typename T> static T *registerDispatchModule(T *module)
{
dispatchModules.push_back(module);
return module;
}
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);
}
} // 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;
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 mockRoutingModule;
mockRoutingModule = nullptr;
routingModule = nullptr;
while (auto *status = mockService->getQueueStatusForPhone())
mockService->releaseQueueStatusToPool(status);
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());
}
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);
exit(UNITY_END());
}
void loop() {}