mirror of
https://github.com/meshtastic/firmware.git
synced 2026-08-01 10:58:30 -04:00
* feat: sign licensed plaintext packets Preserve and publish identity keys in licensed mode so existing XEdDSA signatures can authenticate plaintext traffic. Sign licensed broadcasts and direct messages when they fit, while keeping PKI encryption disabled and normal routing behavior unchanged. * fix(security): persist licensed channel sanitation * fix(security): close licensed migration lifecycle gaps * fix(security): address licensed signing review feedback * test: restore signing globals from Unity teardown * fix(baseui): allow confirmed ham region selection * fix(baseui): guard region picker validation * style: trunk fmt --------- Co-authored-by: Ben Meadors <benmmeadors@gmail.com> Co-authored-by: Austin <vidplace7@gmail.com>
1811 lines
77 KiB
C++
1811 lines
77 KiB
C++
/**
|
|
* Tests for the radio configuration validation and clamping functions
|
|
* introduced in the radio_interface_cherrypick branch.
|
|
*
|
|
* Targets:
|
|
* 1. getRegion()
|
|
* 2. RadioInterface::validateConfigRegion()
|
|
* 3. RadioInterface::validateConfigLora()
|
|
* 4. RadioInterface::clampConfigLora()
|
|
* 5. RegionInfo preset lists (PRESETS_STD, PRESETS_EU_868, PRESETS_UNDEF)
|
|
* 6. Channel spacing calculation (placeholder for future protobuf changes)
|
|
*/
|
|
|
|
#include "Channels.h"
|
|
#include "DisplayFormatters.h"
|
|
#include "FSCommon.h"
|
|
#include "MeshRadio.h"
|
|
#include "MeshService.h"
|
|
#include "NodeDB.h"
|
|
#include "RadioInterface.h"
|
|
#include "TestUtil.h"
|
|
#include "mesh/Channels.h"
|
|
#include "modules/AdminModule.h"
|
|
#include "modules/NodeInfoModule.h"
|
|
#include <pb_decode.h>
|
|
#include <pb_encode.h>
|
|
#include <string>
|
|
#include <unity.h>
|
|
#include <vector>
|
|
|
|
#include "meshtastic/config.pb.h"
|
|
#include "support/AdminModuleTestShim.h"
|
|
|
|
// hash() is a file-scope function in RadioInterface.cpp; link it in for slot-formula tests
|
|
extern uint32_t hash(const char *str);
|
|
|
|
// Every client notification the AdminModule emits flows through sendClientNotification();
|
|
// capture each formatted message so the warning/coalescing tests can assert on the exact
|
|
// set of messages produced by a sequence of admin messages. This shadows test/support/MockMeshService.h's
|
|
// release-only stub because these tests need to inspect the captured message text, not just avoid leaks.
|
|
static std::vector<std::string> capturedWarnings;
|
|
|
|
class MockMeshService : public MeshService
|
|
{
|
|
public:
|
|
void sendClientNotification(meshtastic_ClientNotification *n) override
|
|
{
|
|
capturedWarnings.push_back(n->message);
|
|
releaseClientNotificationToPool(n);
|
|
}
|
|
};
|
|
|
|
static MockMeshService *mockMeshService;
|
|
|
|
// -----------------------------------------------------------------------
|
|
// getRegion() tests
|
|
// -----------------------------------------------------------------------
|
|
|
|
static void test_getRegion_returnsCorrectRegion_US()
|
|
{
|
|
const RegionInfo *r = getRegion(meshtastic_Config_LoRaConfig_RegionCode_US);
|
|
TEST_ASSERT_NOT_NULL(r);
|
|
TEST_ASSERT_EQUAL(meshtastic_Config_LoRaConfig_RegionCode_US, r->code);
|
|
TEST_ASSERT_EQUAL_STRING("US", r->name);
|
|
}
|
|
|
|
static void test_getRegion_returnsCorrectRegion_EU868()
|
|
{
|
|
const RegionInfo *r = getRegion(meshtastic_Config_LoRaConfig_RegionCode_EU_868);
|
|
TEST_ASSERT_NOT_NULL(r);
|
|
TEST_ASSERT_EQUAL(meshtastic_Config_LoRaConfig_RegionCode_EU_868, r->code);
|
|
TEST_ASSERT_EQUAL_STRING("EU_868", r->name);
|
|
}
|
|
|
|
static void test_getRegion_returnsCorrectRegion_LORA24()
|
|
{
|
|
const RegionInfo *r = getRegion(meshtastic_Config_LoRaConfig_RegionCode_LORA_24);
|
|
TEST_ASSERT_NOT_NULL(r);
|
|
TEST_ASSERT_EQUAL(meshtastic_Config_LoRaConfig_RegionCode_LORA_24, r->code);
|
|
TEST_ASSERT_TRUE(r->wideLora);
|
|
}
|
|
|
|
static void test_getRegion_unsetCodeReturnsUnsetEntry()
|
|
{
|
|
const RegionInfo *r = getRegion(meshtastic_Config_LoRaConfig_RegionCode_UNSET);
|
|
TEST_ASSERT_NOT_NULL(r);
|
|
TEST_ASSERT_EQUAL(meshtastic_Config_LoRaConfig_RegionCode_UNSET, r->code);
|
|
TEST_ASSERT_EQUAL_STRING("UNSET", r->name);
|
|
}
|
|
|
|
static void test_getRegion_unknownCodeFallsToUnset()
|
|
{
|
|
// A code not in the table should iterate to the UNSET sentinel
|
|
const RegionInfo *r = getRegion((meshtastic_Config_LoRaConfig_RegionCode)255);
|
|
TEST_ASSERT_NOT_NULL(r);
|
|
TEST_ASSERT_EQUAL(meshtastic_Config_LoRaConfig_RegionCode_UNSET, r->code);
|
|
}
|
|
|
|
// -----------------------------------------------------------------------
|
|
// validateConfigRegion() tests
|
|
// -----------------------------------------------------------------------
|
|
|
|
static void test_validateConfigRegion_validRegionReturnsTrue()
|
|
{
|
|
meshtastic_Config_LoRaConfig cfg = meshtastic_Config_LoRaConfig_init_zero;
|
|
cfg.region = meshtastic_Config_LoRaConfig_RegionCode_US;
|
|
|
|
// Ensure owner is not licensed (should not matter for non-licensed-only regions)
|
|
devicestate.owner.is_licensed = false;
|
|
|
|
TEST_ASSERT_TRUE(RadioInterface::validateConfigRegion(cfg));
|
|
}
|
|
|
|
static void test_validateConfigRegion_unsetRegionReturnsTrue()
|
|
{
|
|
meshtastic_Config_LoRaConfig cfg = meshtastic_Config_LoRaConfig_init_zero;
|
|
cfg.region = meshtastic_Config_LoRaConfig_RegionCode_UNSET;
|
|
|
|
devicestate.owner.is_licensed = false;
|
|
|
|
// UNSET region has licensedOnly=false, so should pass
|
|
TEST_ASSERT_TRUE(RadioInterface::validateConfigRegion(cfg));
|
|
}
|
|
|
|
static void test_validateConfigRegion_unknownCodeReturnsFalse()
|
|
{
|
|
meshtastic_Config_LoRaConfig cfg = meshtastic_Config_LoRaConfig_init_zero;
|
|
cfg.region = (meshtastic_Config_LoRaConfig_RegionCode)255;
|
|
|
|
devicestate.owner.is_licensed = false;
|
|
|
|
// Unknown code is not in the regions table; getRegion() returns the UNSET sentinel,
|
|
// whose .code != 255, so validateConfigRegion should reject it.
|
|
TEST_ASSERT_FALSE(RadioInterface::validateConfigRegion(cfg));
|
|
}
|
|
|
|
static void test_validateConfigRegion_anotherUnknownCodeReturnsFalse()
|
|
{
|
|
meshtastic_Config_LoRaConfig cfg = meshtastic_Config_LoRaConfig_init_zero;
|
|
cfg.region = (meshtastic_Config_LoRaConfig_RegionCode)99;
|
|
|
|
devicestate.owner.is_licensed = true;
|
|
|
|
// Unknown code should be rejected even when owner is licensed.
|
|
TEST_ASSERT_FALSE(RadioInterface::validateConfigRegion(cfg));
|
|
}
|
|
|
|
// -----------------------------------------------------------------------
|
|
// Shadow tables for testing (preset lists → profiles → regions → lookup)
|
|
// -----------------------------------------------------------------------
|
|
|
|
// A minimal preset list with only one entry
|
|
static const meshtastic_Config_LoRaConfig_ModemPreset TEST_PRESETS_SINGLE[] = {
|
|
meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST,
|
|
MODEM_PRESET_END,
|
|
};
|
|
|
|
// A preset list that includes all turbo variants only
|
|
static const meshtastic_Config_LoRaConfig_ModemPreset TEST_PRESETS_TURBO_ONLY[] = {
|
|
meshtastic_Config_LoRaConfig_ModemPreset_SHORT_TURBO,
|
|
meshtastic_Config_LoRaConfig_ModemPreset_LONG_TURBO,
|
|
MODEM_PRESET_END,
|
|
};
|
|
|
|
// A restricted list simulating a hypothetical tight-regulation region
|
|
static const meshtastic_Config_LoRaConfig_ModemPreset TEST_PRESETS_RESTRICTED[] = {
|
|
meshtastic_Config_LoRaConfig_ModemPreset_LONG_SLOW,
|
|
meshtastic_Config_LoRaConfig_ModemPreset_LONG_MODERATE,
|
|
MODEM_PRESET_END,
|
|
};
|
|
|
|
// Mirrors PROFILE_STD but with non-zero spacing/padding for testing
|
|
static const RegionProfile TEST_PROFILE_SPACED = {
|
|
TEST_PRESETS_SINGLE,
|
|
/* spacing */ 0.025f,
|
|
/* padding */ 0.010f,
|
|
/* audioPermitted */ true,
|
|
/* licensedOnly */ false,
|
|
/* textThrottle */ 0,
|
|
/* positionThrottle */ 0,
|
|
/* telemetryThrottle */ 0,
|
|
};
|
|
|
|
// A licensed-only profile for testing access control
|
|
static const RegionProfile TEST_PROFILE_LICENSED = {
|
|
TEST_PRESETS_RESTRICTED,
|
|
/* spacing */ 0.0f,
|
|
/* padding */ 0.0f,
|
|
/* audioPermitted */ false,
|
|
/* licensedOnly */ true,
|
|
/* textThrottle */ 5,
|
|
/* positionThrottle */ 10,
|
|
/* telemetryThrottle */ 10,
|
|
};
|
|
|
|
// Turbo-only profile
|
|
static const RegionProfile TEST_PROFILE_TURBO = {
|
|
TEST_PRESETS_TURBO_ONLY,
|
|
/* spacing */ 0.0f,
|
|
/* padding */ 0.0f,
|
|
/* audioPermitted */ true,
|
|
/* licensedOnly */ false,
|
|
/* textThrottle */ 0,
|
|
/* positionThrottle */ 0,
|
|
/* telemetryThrottle */ 0,
|
|
};
|
|
|
|
// A preset list for the preset-hash override slot test (LONG_FAST + MEDIUM_FAST)
|
|
static const meshtastic_Config_LoRaConfig_ModemPreset TEST_PRESETS_PRESET_HASH[] = {
|
|
meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST,
|
|
meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_FAST,
|
|
MODEM_PRESET_END,
|
|
};
|
|
|
|
// Profile with overrideSlot = OVERRIDE_SLOT_PRESET_HASH (-1):
|
|
// slot selection always uses hash(presetDisplayName), ignoring the primary channel name.
|
|
static const RegionProfile TEST_PROFILE_PRESET_HASH = {
|
|
TEST_PRESETS_PRESET_HASH,
|
|
/* spacing */ 0.0f,
|
|
/* padding */ 0.0f,
|
|
/* audioPermitted */ true,
|
|
/* licensedOnly */ false,
|
|
/* textThrottle */ 0,
|
|
/* positionThrottle */ 0,
|
|
/* telemetryThrottle */ 0,
|
|
};
|
|
|
|
// Standalone test region using US frequencies (26 MHz span → 104 slots at 250 kHz BW)
|
|
// Used to verify OVERRIDE_SLOT_PRESET_HASH slot formula; not inserted into testRegions[].
|
|
static const RegionInfo TEST_REGION_PRESET_HASH = {
|
|
meshtastic_Config_LoRaConfig_RegionCode_US,
|
|
902.0f,
|
|
928.0f,
|
|
100,
|
|
30,
|
|
false,
|
|
false,
|
|
&TEST_PROFILE_PRESET_HASH,
|
|
meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST,
|
|
OVERRIDE_SLOT_PRESET_HASH,
|
|
"TEST_PRESET_HASH",
|
|
};
|
|
|
|
static const RegionInfo testRegions[] = {
|
|
// A wide US-like region with spacing + padding
|
|
{meshtastic_Config_LoRaConfig_RegionCode_US, 902.0f, 928.0f, 100, 30, false, false, &TEST_PROFILE_SPACED,
|
|
meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST, 0, "TEST_US_SPACED"},
|
|
|
|
// A narrow band simulating tight EU regulation
|
|
{meshtastic_Config_LoRaConfig_RegionCode_EU_868, 869.4f, 869.65f, 10, 14, false, false, &TEST_PROFILE_LICENSED,
|
|
meshtastic_Config_LoRaConfig_ModemPreset_LONG_SLOW, 3, "TEST_EU_LICENSED"},
|
|
|
|
// A wide-LoRa region with turbo-only presets
|
|
{meshtastic_Config_LoRaConfig_RegionCode_LORA_24, 2400.0f, 2483.5f, 100, 10, false, true, &TEST_PROFILE_TURBO,
|
|
meshtastic_Config_LoRaConfig_ModemPreset_SHORT_TURBO, 0, "TEST_LORA24_TURBO"},
|
|
|
|
// Sentinel - must be last
|
|
{meshtastic_Config_LoRaConfig_RegionCode_UNSET, 902.0f, 928.0f, 100, 30, false, false, &TEST_PROFILE_SPACED,
|
|
meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST, 0, "TEST_UNSET"},
|
|
};
|
|
|
|
static const RegionInfo *getTestRegion(meshtastic_Config_LoRaConfig_RegionCode code)
|
|
{
|
|
const RegionInfo *r = testRegions;
|
|
while (r->code != meshtastic_Config_LoRaConfig_RegionCode_UNSET) {
|
|
if (r->code == code)
|
|
return r;
|
|
r++;
|
|
}
|
|
return r; // Returns the UNSET sentinel
|
|
}
|
|
|
|
// -----------------------------------------------------------------------
|
|
// Shadow table tests
|
|
// -----------------------------------------------------------------------
|
|
|
|
// Helper: replicate the numFreqSlots formula from RadioInterface so tests can compute expected values.
|
|
static uint32_t testComputeNumFreqSlots(const RegionInfo *r, float bw_kHz)
|
|
{
|
|
float w = r->profile->spacing + (r->profile->padding * 2) + (bw_kHz / 1000.0f);
|
|
return (uint32_t)(((r->freqEnd - r->freqStart + r->profile->spacing) / w) + 0.5f);
|
|
}
|
|
|
|
static void test_shadowTable_spacedProfileHasNonZeroSpacing()
|
|
{
|
|
const RegionInfo *r = getTestRegion(meshtastic_Config_LoRaConfig_RegionCode_US);
|
|
TEST_ASSERT_EQUAL_STRING("TEST_US_SPACED", r->name);
|
|
TEST_ASSERT_FLOAT_WITHIN(0.001f, 0.025f, r->profile->spacing);
|
|
TEST_ASSERT_FLOAT_WITHIN(0.001f, 0.010f, r->profile->padding);
|
|
}
|
|
|
|
static void test_shadowTable_licensedProfileFlagsCorrect()
|
|
{
|
|
const RegionInfo *r = getTestRegion(meshtastic_Config_LoRaConfig_RegionCode_EU_868);
|
|
TEST_ASSERT_TRUE(r->profile->licensedOnly);
|
|
TEST_ASSERT_FALSE(r->profile->audioPermitted);
|
|
TEST_ASSERT_EQUAL(3, r->overrideSlot);
|
|
}
|
|
|
|
static void test_shadowTable_presetCountMatchesExpected()
|
|
{
|
|
const RegionInfo *spaced = getTestRegion(meshtastic_Config_LoRaConfig_RegionCode_US);
|
|
TEST_ASSERT_EQUAL(1, spaced->getNumPresets());
|
|
|
|
const RegionInfo *licensed = getTestRegion(meshtastic_Config_LoRaConfig_RegionCode_EU_868);
|
|
TEST_ASSERT_EQUAL(2, licensed->getNumPresets());
|
|
|
|
const RegionInfo *turbo = getTestRegion(meshtastic_Config_LoRaConfig_RegionCode_LORA_24);
|
|
TEST_ASSERT_EQUAL(2, turbo->getNumPresets());
|
|
}
|
|
|
|
static void test_shadowTable_defaultPresetIsFirstInList()
|
|
{
|
|
const RegionInfo *spaced = getTestRegion(meshtastic_Config_LoRaConfig_RegionCode_US);
|
|
TEST_ASSERT_EQUAL(meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST, spaced->getDefaultPreset());
|
|
|
|
const RegionInfo *licensed = getTestRegion(meshtastic_Config_LoRaConfig_RegionCode_EU_868);
|
|
TEST_ASSERT_EQUAL(meshtastic_Config_LoRaConfig_ModemPreset_LONG_SLOW, licensed->getDefaultPreset());
|
|
|
|
const RegionInfo *turbo = getTestRegion(meshtastic_Config_LoRaConfig_RegionCode_LORA_24);
|
|
TEST_ASSERT_EQUAL(meshtastic_Config_LoRaConfig_ModemPreset_SHORT_TURBO, turbo->getDefaultPreset());
|
|
}
|
|
|
|
static void test_shadowTable_channelSpacingWithPadding()
|
|
{
|
|
// Verify channel count when spacing + padding are non-zero
|
|
const RegionInfo *r = getTestRegion(meshtastic_Config_LoRaConfig_RegionCode_US);
|
|
float bw = modemPresetToBwKHz(r->getDefaultPreset(), r->wideLora);
|
|
float channelSpacing = r->profile->spacing + (r->profile->padding * 2) + (bw / 1000.0f);
|
|
|
|
// spacing=0.025, padding=0.010*2=0.020, bw=250kHz=0.250
|
|
// channelSpacing = 0.025 + 0.020 + 0.250 = 0.295 MHz
|
|
TEST_ASSERT_FLOAT_WITHIN(0.001f, 0.295f, channelSpacing);
|
|
|
|
uint32_t numChannels = (uint32_t)(((r->freqEnd - r->freqStart + r->profile->spacing) / channelSpacing) + 0.5f);
|
|
// (928 - 902 + 0.025) / 0.295 = 88.2 → 88
|
|
TEST_ASSERT_EQUAL_UINT32(88, numChannels);
|
|
}
|
|
|
|
static void test_shadowTable_turboOnlyOnWideLora()
|
|
{
|
|
const RegionInfo *r = getTestRegion(meshtastic_Config_LoRaConfig_RegionCode_LORA_24);
|
|
TEST_ASSERT_TRUE(r->wideLora);
|
|
TEST_ASSERT_EQUAL(meshtastic_Config_LoRaConfig_ModemPreset_SHORT_TURBO, r->getDefaultPreset());
|
|
|
|
// Verify wide-LoRa bandwidth for SHORT_TURBO
|
|
float bw = modemPresetToBwKHz(r->getDefaultPreset(), r->wideLora);
|
|
TEST_ASSERT_FLOAT_WITHIN(0.1f, 1625.0f, bw); // 1625 kHz in wide mode
|
|
}
|
|
|
|
static void test_shadowTable_unknownCodeFallsToSentinel()
|
|
{
|
|
const RegionInfo *r = getTestRegion((meshtastic_Config_LoRaConfig_RegionCode)200);
|
|
TEST_ASSERT_EQUAL(meshtastic_Config_LoRaConfig_RegionCode_UNSET, r->code);
|
|
TEST_ASSERT_EQUAL_STRING("TEST_UNSET", r->name);
|
|
}
|
|
|
|
static void test_shadowTable_presetHashProfileHasCorrectOverrideSlot()
|
|
{
|
|
TEST_ASSERT_EQUAL(OVERRIDE_SLOT_PRESET_HASH, TEST_REGION_PRESET_HASH.overrideSlot);
|
|
TEST_ASSERT_EQUAL(-1, TEST_REGION_PRESET_HASH.overrideSlot);
|
|
TEST_ASSERT_EQUAL(2, TEST_REGION_PRESET_HASH.getNumPresets());
|
|
}
|
|
|
|
// -----------------------------------------------------------------------
|
|
// OVERRIDE_SLOT_PRESET_HASH (-1) slot formula tests
|
|
//
|
|
// Property under test:
|
|
// overrideSlot = -1 → slot = hash(presetDisplayName) % numSlots
|
|
// regardless of what the primary channel is named
|
|
// overrideSlot = 0 → slot = hash(channelName) % numSlots
|
|
// when channel name = preset display name, these two modes give identical slots
|
|
// -----------------------------------------------------------------------
|
|
|
|
static void test_overrideSlotPresetHash_longFast_customChannelMatchesDefaultNameSlot()
|
|
{
|
|
// US + LONG_FAST: spacing=0, padding=0, bw=250 kHz
|
|
// numSlots = round((928-902+0)/0.250) = 104
|
|
const RegionInfo *us = getRegion(meshtastic_Config_LoRaConfig_RegionCode_US);
|
|
float bw = modemPresetToBwKHz(meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST, us->wideLora);
|
|
uint32_t numSlots = testComputeNumFreqSlots(us, bw);
|
|
TEST_ASSERT_EQUAL_UINT32(104, numSlots); // sanity
|
|
|
|
const char *presetName =
|
|
DisplayFormatters::getModemPresetDisplayName(meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST, false, true);
|
|
|
|
// OVERRIDE_SLOT_PRESET_HASH (-1):
|
|
// channel is "MyCustomNetwork" but slot still uses preset name hash
|
|
uint32_t slotPresetHashMode = hash(presetName) % numSlots;
|
|
|
|
// OVERRIDE_SLOT_DEFAULT_CHANNEL_HASH (0) with channel name = preset name (user never renamed it):
|
|
// channelName == presetName → same hash → same slot
|
|
const char *defaultChannelName = presetName;
|
|
uint32_t slotChannelHashModeDefaultName = hash(defaultChannelName) % numSlots;
|
|
|
|
TEST_ASSERT_EQUAL_UINT32(slotPresetHashMode, slotChannelHashModeDefaultName);
|
|
|
|
// Confirm a different custom channel name gives a different hash INPUT
|
|
// (so mode 0 would diverge while mode -1 stays locked)
|
|
TEST_ASSERT_TRUE(strcmp(presetName, "MyCustomNetwork") != 0);
|
|
}
|
|
|
|
static void test_overrideSlotPresetHash_mediumFast_customChannelMatchesDefaultNameSlot()
|
|
{
|
|
// US + MEDIUM_FAST: bw=250 kHz → same 104 slots as LONG_FAST for US
|
|
const RegionInfo *us = getRegion(meshtastic_Config_LoRaConfig_RegionCode_US);
|
|
float bw = modemPresetToBwKHz(meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_FAST, us->wideLora);
|
|
uint32_t numSlots = testComputeNumFreqSlots(us, bw);
|
|
TEST_ASSERT_EQUAL_UINT32(104, numSlots); // sanity
|
|
|
|
const char *presetName =
|
|
DisplayFormatters::getModemPresetDisplayName(meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_FAST, false, true);
|
|
|
|
// Mode -1: slot = hash(presetName) % numSlots (channel name irrelevant)
|
|
uint32_t slotPresetHashMode = hash(presetName) % numSlots;
|
|
|
|
// Mode 0 + default name (channel name = preset display name):
|
|
uint32_t slotChannelHashModeDefaultName = hash(presetName) % numSlots;
|
|
|
|
TEST_ASSERT_EQUAL_UINT32(slotPresetHashMode, slotChannelHashModeDefaultName);
|
|
|
|
TEST_ASSERT_TRUE(strcmp(presetName, "MyCustomNetwork") != 0);
|
|
}
|
|
|
|
static void test_overrideSlotPresetHash_longFast_slotIsStableAcrossCustomNames()
|
|
{
|
|
// Mode -1 must give the same slot for LONG_FAST regardless of which custom name is in use.
|
|
const RegionInfo *us = getRegion(meshtastic_Config_LoRaConfig_RegionCode_US);
|
|
float bw = modemPresetToBwKHz(meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST, us->wideLora);
|
|
uint32_t numSlots = testComputeNumFreqSlots(us, bw);
|
|
|
|
const char *presetName =
|
|
DisplayFormatters::getModemPresetDisplayName(meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST, false, true);
|
|
uint32_t expectedSlot = hash(presetName) % numSlots;
|
|
|
|
// Simulate three different custom channel names; mode -1 ignores all of them
|
|
const char *customNames[] = {"AlphaNet", "BetaMesh", "GammaMesh"};
|
|
for (int i = 0; i < 3; i++) {
|
|
uint32_t slotForCustom = hash(presetName) % numSlots; // mode -1: presetName only
|
|
TEST_ASSERT_EQUAL_UINT32(expectedSlot, slotForCustom);
|
|
// Confirm input would have differed in mode 0
|
|
TEST_ASSERT_TRUE(strcmp(presetName, customNames[i]) != 0);
|
|
}
|
|
}
|
|
|
|
static void test_overrideSlotPresetHash_mediumFast_slotIsStableAcrossCustomNames()
|
|
{
|
|
const RegionInfo *us = getRegion(meshtastic_Config_LoRaConfig_RegionCode_US);
|
|
float bw = modemPresetToBwKHz(meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_FAST, us->wideLora);
|
|
uint32_t numSlots = testComputeNumFreqSlots(us, bw);
|
|
|
|
const char *presetName =
|
|
DisplayFormatters::getModemPresetDisplayName(meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_FAST, false, true);
|
|
uint32_t expectedSlot = hash(presetName) % numSlots;
|
|
|
|
const char *customNames[] = {"AlphaNet", "BetaMesh", "GammaMesh"};
|
|
for (int i = 0; i < 3; i++) {
|
|
uint32_t slotForCustom = hash(presetName) % numSlots; // mode -1: presetName only
|
|
TEST_ASSERT_EQUAL_UINT32(expectedSlot, slotForCustom);
|
|
TEST_ASSERT_TRUE(strcmp(presetName, customNames[i]) != 0);
|
|
}
|
|
}
|
|
|
|
static void test_overrideSlotPresetHash_longFastAndMediumFast_slotsAreDifferentPresets()
|
|
{
|
|
// LONG_FAST and MEDIUM_FAST have different display names → likely different hash slots.
|
|
// This verifies the two presets genuinely occupy distinct positions, so the equivalence
|
|
// tests above are not trivially vacuous.
|
|
const RegionInfo *us = getRegion(meshtastic_Config_LoRaConfig_RegionCode_US);
|
|
float bw_lf = modemPresetToBwKHz(meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST, false);
|
|
float bw_mf = modemPresetToBwKHz(meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_FAST, false);
|
|
uint32_t numSlots_lf = testComputeNumFreqSlots(us, bw_lf);
|
|
uint32_t numSlots_mf = testComputeNumFreqSlots(us, bw_mf);
|
|
|
|
const char *nameLF =
|
|
DisplayFormatters::getModemPresetDisplayName(meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST, false, true);
|
|
const char *nameMF =
|
|
DisplayFormatters::getModemPresetDisplayName(meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_FAST, false, true);
|
|
|
|
TEST_ASSERT_TRUE(strcmp(nameLF, nameMF) != 0);
|
|
|
|
uint32_t slotLF = hash(nameLF) % numSlots_lf;
|
|
uint32_t slotMF = hash(nameMF) % numSlots_mf;
|
|
// They use the same numSlots (both 250 kHz on US), so a difference in display name
|
|
// should produce a different slot.
|
|
TEST_ASSERT_NOT_EQUAL(slotLF, slotMF);
|
|
}
|
|
|
|
// -----------------------------------------------------------------------
|
|
// validateConfigLora() tests
|
|
// -----------------------------------------------------------------------
|
|
|
|
static void test_validateConfigLora_validPresetForUS()
|
|
{
|
|
meshtastic_Config_LoRaConfig cfg = meshtastic_Config_LoRaConfig_init_zero;
|
|
cfg.region = meshtastic_Config_LoRaConfig_RegionCode_US;
|
|
cfg.use_preset = true;
|
|
cfg.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST;
|
|
|
|
TEST_ASSERT_TRUE(RadioInterface::validateConfigLora(cfg));
|
|
}
|
|
|
|
static void test_validateConfigLora_allStdPresetsValidForUS()
|
|
{
|
|
meshtastic_Config_LoRaConfig_ModemPreset stdPresets[] = {
|
|
meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST, meshtastic_Config_LoRaConfig_ModemPreset_LONG_SLOW,
|
|
meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_SLOW, meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_FAST,
|
|
meshtastic_Config_LoRaConfig_ModemPreset_SHORT_SLOW, meshtastic_Config_LoRaConfig_ModemPreset_SHORT_FAST,
|
|
meshtastic_Config_LoRaConfig_ModemPreset_LONG_MODERATE, meshtastic_Config_LoRaConfig_ModemPreset_SHORT_TURBO,
|
|
meshtastic_Config_LoRaConfig_ModemPreset_LONG_TURBO, meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_TURBO,
|
|
};
|
|
|
|
for (size_t i = 0; i < sizeof(stdPresets) / sizeof(stdPresets[0]); i++) {
|
|
meshtastic_Config_LoRaConfig cfg = meshtastic_Config_LoRaConfig_init_zero;
|
|
cfg.region = meshtastic_Config_LoRaConfig_RegionCode_US;
|
|
cfg.use_preset = true;
|
|
cfg.modem_preset = stdPresets[i];
|
|
TEST_ASSERT_TRUE_MESSAGE(RadioInterface::validateConfigLora(cfg), "Expected valid preset for US");
|
|
}
|
|
}
|
|
|
|
static void test_validateConfigLora_turboPresetsInvalidForEU868()
|
|
{
|
|
// EU_868 has PRESETS_EU_868 which excludes the 500 kHz turbo presets
|
|
// (SHORT_TURBO, LONG_TURBO, MEDIUM_TURBO)
|
|
meshtastic_Config_LoRaConfig cfg = meshtastic_Config_LoRaConfig_init_zero;
|
|
cfg.region = meshtastic_Config_LoRaConfig_RegionCode_EU_868;
|
|
cfg.use_preset = true;
|
|
|
|
cfg.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_SHORT_TURBO;
|
|
TEST_ASSERT_FALSE_MESSAGE(RadioInterface::validateConfigLora(cfg), "SHORT_TURBO should be invalid for EU_868");
|
|
|
|
cfg.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_LONG_TURBO;
|
|
TEST_ASSERT_FALSE_MESSAGE(RadioInterface::validateConfigLora(cfg), "LONG_TURBO should be invalid for EU_868");
|
|
|
|
cfg.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_TURBO;
|
|
TEST_ASSERT_FALSE_MESSAGE(RadioInterface::validateConfigLora(cfg), "MEDIUM_TURBO should be invalid for EU_868");
|
|
}
|
|
|
|
static void test_validateConfigLora_validPresetsForEU868()
|
|
{
|
|
meshtastic_Config_LoRaConfig_ModemPreset eu868Presets[] = {
|
|
meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST, meshtastic_Config_LoRaConfig_ModemPreset_LONG_SLOW,
|
|
meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_SLOW, meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_FAST,
|
|
meshtastic_Config_LoRaConfig_ModemPreset_SHORT_SLOW, meshtastic_Config_LoRaConfig_ModemPreset_SHORT_FAST,
|
|
meshtastic_Config_LoRaConfig_ModemPreset_LONG_MODERATE,
|
|
};
|
|
|
|
for (size_t i = 0; i < sizeof(eu868Presets) / sizeof(eu868Presets[0]); i++) {
|
|
meshtastic_Config_LoRaConfig cfg = meshtastic_Config_LoRaConfig_init_zero;
|
|
cfg.region = meshtastic_Config_LoRaConfig_RegionCode_EU_868;
|
|
cfg.use_preset = true;
|
|
cfg.modem_preset = eu868Presets[i];
|
|
TEST_ASSERT_TRUE_MESSAGE(RadioInterface::validateConfigLora(cfg), "Expected valid preset for EU_868");
|
|
}
|
|
}
|
|
|
|
static void test_validateConfigLora_customBandwidthTooWideForEU868()
|
|
{
|
|
// EU_868 spans 869.4 - 869.65 = 0.25 MHz = 250 kHz
|
|
// A 500 kHz custom BW should be rejected
|
|
meshtastic_Config_LoRaConfig cfg = meshtastic_Config_LoRaConfig_init_zero;
|
|
cfg.region = meshtastic_Config_LoRaConfig_RegionCode_EU_868;
|
|
cfg.use_preset = false;
|
|
cfg.bandwidth = 500;
|
|
cfg.spread_factor = 11;
|
|
cfg.coding_rate = 5;
|
|
|
|
TEST_ASSERT_FALSE(RadioInterface::validateConfigLora(cfg));
|
|
}
|
|
|
|
static void test_validateConfigLora_customBandwidthFitsUS()
|
|
{
|
|
// US spans 902 - 928 = 26 MHz, so 250 kHz BW fits easily
|
|
meshtastic_Config_LoRaConfig cfg = meshtastic_Config_LoRaConfig_init_zero;
|
|
cfg.region = meshtastic_Config_LoRaConfig_RegionCode_US;
|
|
cfg.use_preset = false;
|
|
cfg.bandwidth = 250;
|
|
cfg.spread_factor = 11;
|
|
cfg.coding_rate = 5;
|
|
|
|
TEST_ASSERT_TRUE(RadioInterface::validateConfigLora(cfg));
|
|
}
|
|
|
|
static void test_validateConfigLora_customBandwidthFitsEU868()
|
|
{
|
|
// EU_868 spans 250 kHz, 125 kHz BW should fit
|
|
meshtastic_Config_LoRaConfig cfg = meshtastic_Config_LoRaConfig_init_zero;
|
|
cfg.region = meshtastic_Config_LoRaConfig_RegionCode_EU_868;
|
|
cfg.use_preset = false;
|
|
cfg.bandwidth = 125;
|
|
cfg.spread_factor = 12;
|
|
cfg.coding_rate = 8;
|
|
|
|
TEST_ASSERT_TRUE(RadioInterface::validateConfigLora(cfg));
|
|
}
|
|
|
|
static void test_validateConfigLora_bogusPresetRejected()
|
|
{
|
|
// A fabricated preset value not in any list should be rejected
|
|
meshtastic_Config_LoRaConfig cfg = meshtastic_Config_LoRaConfig_init_zero;
|
|
cfg.region = meshtastic_Config_LoRaConfig_RegionCode_US;
|
|
cfg.use_preset = true;
|
|
cfg.modem_preset = (meshtastic_Config_LoRaConfig_ModemPreset)99;
|
|
|
|
TEST_ASSERT_FALSE(RadioInterface::validateConfigLora(cfg));
|
|
}
|
|
|
|
static void test_validateConfigLora_unsetRegionOnlyAcceptsLongFast()
|
|
{
|
|
// UNSET uses PROFILE_UNDEF which has only LONG_FAST
|
|
meshtastic_Config_LoRaConfig cfg = meshtastic_Config_LoRaConfig_init_zero;
|
|
cfg.region = meshtastic_Config_LoRaConfig_RegionCode_UNSET;
|
|
cfg.use_preset = true;
|
|
|
|
cfg.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST;
|
|
TEST_ASSERT_TRUE_MESSAGE(RadioInterface::validateConfigLora(cfg), "LONG_FAST should be valid for UNSET");
|
|
|
|
cfg.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_FAST;
|
|
TEST_ASSERT_FALSE_MESSAGE(RadioInterface::validateConfigLora(cfg), "MEDIUM_FAST should be invalid for UNSET");
|
|
|
|
cfg.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_SHORT_TURBO;
|
|
TEST_ASSERT_FALSE_MESSAGE(RadioInterface::validateConfigLora(cfg), "SHORT_TURBO should be invalid for UNSET");
|
|
}
|
|
|
|
static void test_validateConfigLora_allPresetsValidForLORA24()
|
|
{
|
|
// LORA_24 uses PROFILE_STD (10 presets) with wideLora=true
|
|
meshtastic_Config_LoRaConfig_ModemPreset stdPresets[] = {
|
|
meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST, meshtastic_Config_LoRaConfig_ModemPreset_LONG_SLOW,
|
|
meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_SLOW, meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_FAST,
|
|
meshtastic_Config_LoRaConfig_ModemPreset_SHORT_SLOW, meshtastic_Config_LoRaConfig_ModemPreset_SHORT_FAST,
|
|
meshtastic_Config_LoRaConfig_ModemPreset_LONG_MODERATE, meshtastic_Config_LoRaConfig_ModemPreset_SHORT_TURBO,
|
|
meshtastic_Config_LoRaConfig_ModemPreset_LONG_TURBO, meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_TURBO,
|
|
};
|
|
|
|
for (size_t i = 0; i < sizeof(stdPresets) / sizeof(stdPresets[0]); i++) {
|
|
meshtastic_Config_LoRaConfig cfg = meshtastic_Config_LoRaConfig_init_zero;
|
|
cfg.region = meshtastic_Config_LoRaConfig_RegionCode_LORA_24;
|
|
cfg.use_preset = true;
|
|
cfg.modem_preset = stdPresets[i];
|
|
TEST_ASSERT_TRUE_MESSAGE(RadioInterface::validateConfigLora(cfg), "Expected valid preset for LORA_24");
|
|
}
|
|
}
|
|
|
|
// -----------------------------------------------------------------------
|
|
// clampConfigLora() tests
|
|
// -----------------------------------------------------------------------
|
|
|
|
static void test_clampConfigLora_invalidPresetClampedToDefault()
|
|
{
|
|
meshtastic_Config_LoRaConfig cfg = meshtastic_Config_LoRaConfig_init_zero;
|
|
cfg.region = meshtastic_Config_LoRaConfig_RegionCode_EU_868;
|
|
cfg.use_preset = true;
|
|
cfg.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_SHORT_TURBO; // not in EU_868 preset list
|
|
|
|
RadioInterface::clampConfigLora(cfg);
|
|
|
|
const RegionInfo *eu868 = getRegion(meshtastic_Config_LoRaConfig_RegionCode_EU_868);
|
|
TEST_ASSERT_EQUAL(eu868->getDefaultPreset(), cfg.modem_preset);
|
|
}
|
|
|
|
static void test_clampConfigLora_validPresetUnchanged()
|
|
{
|
|
meshtastic_Config_LoRaConfig cfg = meshtastic_Config_LoRaConfig_init_zero;
|
|
cfg.region = meshtastic_Config_LoRaConfig_RegionCode_US;
|
|
cfg.use_preset = true;
|
|
cfg.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_FAST;
|
|
|
|
RadioInterface::clampConfigLora(cfg);
|
|
|
|
TEST_ASSERT_EQUAL(meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_FAST, cfg.modem_preset);
|
|
}
|
|
|
|
static void test_clampConfigLora_customBwTooWideClampedToDefaultBw()
|
|
{
|
|
// EU_868 span is 250kHz. A 500kHz custom BW should be clamped to default preset BW.
|
|
meshtastic_Config_LoRaConfig cfg = meshtastic_Config_LoRaConfig_init_zero;
|
|
cfg.region = meshtastic_Config_LoRaConfig_RegionCode_EU_868;
|
|
cfg.use_preset = false;
|
|
cfg.bandwidth = 500;
|
|
cfg.spread_factor = 11;
|
|
cfg.coding_rate = 5;
|
|
|
|
RadioInterface::clampConfigLora(cfg);
|
|
|
|
const RegionInfo *eu868 = getRegion(meshtastic_Config_LoRaConfig_RegionCode_EU_868);
|
|
float expectedBw = modemPresetToBwKHz(eu868->getDefaultPreset(), eu868->wideLora);
|
|
TEST_ASSERT_FLOAT_WITHIN(0.01f, expectedBw, (float)cfg.bandwidth);
|
|
}
|
|
|
|
static void test_clampConfigLora_customBwValidLeftUnchanged()
|
|
{
|
|
meshtastic_Config_LoRaConfig cfg = meshtastic_Config_LoRaConfig_init_zero;
|
|
cfg.region = meshtastic_Config_LoRaConfig_RegionCode_US;
|
|
cfg.use_preset = false;
|
|
cfg.bandwidth = 125;
|
|
cfg.spread_factor = 12;
|
|
cfg.coding_rate = 8;
|
|
|
|
RadioInterface::clampConfigLora(cfg);
|
|
|
|
TEST_ASSERT_EQUAL_UINT16(125, cfg.bandwidth);
|
|
}
|
|
|
|
static void test_clampConfigLora_bogusPresetOnUnsetClampedToLongFast()
|
|
{
|
|
// UNSET uses PROFILE_UNDEF with only LONG_FAST; any other preset should clamp to it
|
|
meshtastic_Config_LoRaConfig cfg = meshtastic_Config_LoRaConfig_init_zero;
|
|
cfg.region = meshtastic_Config_LoRaConfig_RegionCode_UNSET;
|
|
cfg.use_preset = true;
|
|
cfg.modem_preset = (meshtastic_Config_LoRaConfig_ModemPreset)99;
|
|
|
|
RadioInterface::clampConfigLora(cfg);
|
|
|
|
TEST_ASSERT_EQUAL(meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST, cfg.modem_preset);
|
|
}
|
|
|
|
static void test_clampConfigLora_invalidPresetOnLORA24ClampedToDefault()
|
|
{
|
|
// LORA_24 uses PROFILE_STD; a bogus preset should clamp to LONG_FAST (first in PRESETS_STD)
|
|
meshtastic_Config_LoRaConfig cfg = meshtastic_Config_LoRaConfig_init_zero;
|
|
cfg.region = meshtastic_Config_LoRaConfig_RegionCode_LORA_24;
|
|
cfg.use_preset = true;
|
|
cfg.modem_preset = (meshtastic_Config_LoRaConfig_ModemPreset)99;
|
|
|
|
RadioInterface::clampConfigLora(cfg);
|
|
|
|
const RegionInfo *lora24 = getRegion(meshtastic_Config_LoRaConfig_RegionCode_LORA_24);
|
|
TEST_ASSERT_EQUAL(lora24->getDefaultPreset(), cfg.modem_preset);
|
|
}
|
|
|
|
// -----------------------------------------------------------------------
|
|
// Region-locked preset swap tests (EU_868 / EU_866 / EU_N_868 trio)
|
|
// -----------------------------------------------------------------------
|
|
|
|
static void test_clampConfigLora_narrowPresetOnEU866SwapsToEUN868()
|
|
{
|
|
meshtastic_Config_LoRaConfig cfg = meshtastic_Config_LoRaConfig_init_zero;
|
|
cfg.region = meshtastic_Config_LoRaConfig_RegionCode_EU_866;
|
|
cfg.use_preset = true;
|
|
cfg.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_NARROW_FAST;
|
|
|
|
RadioInterface::clampConfigLora(cfg);
|
|
|
|
TEST_ASSERT_EQUAL(meshtastic_Config_LoRaConfig_RegionCode_EU_N_868, cfg.region);
|
|
TEST_ASSERT_EQUAL(meshtastic_Config_LoRaConfig_ModemPreset_NARROW_FAST, cfg.modem_preset);
|
|
}
|
|
|
|
static void test_clampConfigLora_litePresetOnEU868SwapsToEU866()
|
|
{
|
|
meshtastic_Config_LoRaConfig cfg = meshtastic_Config_LoRaConfig_init_zero;
|
|
cfg.region = meshtastic_Config_LoRaConfig_RegionCode_EU_868;
|
|
cfg.use_preset = true;
|
|
cfg.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_LITE_SLOW;
|
|
|
|
RadioInterface::clampConfigLora(cfg);
|
|
|
|
TEST_ASSERT_EQUAL(meshtastic_Config_LoRaConfig_RegionCode_EU_866, cfg.region);
|
|
TEST_ASSERT_EQUAL(meshtastic_Config_LoRaConfig_ModemPreset_LITE_SLOW, cfg.modem_preset);
|
|
}
|
|
|
|
static void test_clampConfigLora_eu868PresetOnEUN868SwapsToEU868()
|
|
{
|
|
meshtastic_Config_LoRaConfig cfg = meshtastic_Config_LoRaConfig_init_zero;
|
|
cfg.region = meshtastic_Config_LoRaConfig_RegionCode_EU_N_868;
|
|
cfg.use_preset = true;
|
|
cfg.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST;
|
|
|
|
RadioInterface::clampConfigLora(cfg);
|
|
|
|
TEST_ASSERT_EQUAL(meshtastic_Config_LoRaConfig_RegionCode_EU_868, cfg.region);
|
|
TEST_ASSERT_EQUAL(meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST, cfg.modem_preset);
|
|
}
|
|
|
|
static void test_clampConfigLora_litePresetOnUSDoesNotSwap()
|
|
{
|
|
// Previous region is not one of the swappable trio, so the preset clamps to the
|
|
// region default instead of swapping regions.
|
|
meshtastic_Config_LoRaConfig cfg = meshtastic_Config_LoRaConfig_init_zero;
|
|
cfg.region = meshtastic_Config_LoRaConfig_RegionCode_US;
|
|
cfg.use_preset = true;
|
|
cfg.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_LITE_FAST;
|
|
|
|
RadioInterface::clampConfigLora(cfg);
|
|
|
|
const RegionInfo *us = getRegion(meshtastic_Config_LoRaConfig_RegionCode_US);
|
|
TEST_ASSERT_EQUAL(meshtastic_Config_LoRaConfig_RegionCode_US, cfg.region);
|
|
TEST_ASSERT_EQUAL(us->getDefaultPreset(), cfg.modem_preset);
|
|
}
|
|
|
|
static void test_clampConfigLora_narrowPresetOnHam125cmDoesNotSwap()
|
|
{
|
|
// ITU2_125CM shares the NARROW presets, so they are valid there and nothing changes
|
|
meshtastic_Config_LoRaConfig cfg = meshtastic_Config_LoRaConfig_init_zero;
|
|
cfg.region = meshtastic_Config_LoRaConfig_RegionCode_ITU2_125CM;
|
|
cfg.use_preset = true;
|
|
cfg.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_NARROW_SLOW;
|
|
|
|
RadioInterface::clampConfigLora(cfg);
|
|
|
|
TEST_ASSERT_EQUAL(meshtastic_Config_LoRaConfig_RegionCode_ITU2_125CM, cfg.region);
|
|
TEST_ASSERT_EQUAL(meshtastic_Config_LoRaConfig_ModemPreset_NARROW_SLOW, cfg.modem_preset);
|
|
}
|
|
|
|
static void test_validateConfigLora_siblingLockedPresetStillFailsValidation()
|
|
{
|
|
// Validation (no clamp) must keep failing so callers route into clampConfigLora,
|
|
// which performs the region swap.
|
|
meshtastic_Config_LoRaConfig cfg = meshtastic_Config_LoRaConfig_init_zero;
|
|
cfg.region = meshtastic_Config_LoRaConfig_RegionCode_EU_866;
|
|
cfg.use_preset = true;
|
|
cfg.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_NARROW_FAST;
|
|
|
|
TEST_ASSERT_FALSE(RadioInterface::validateConfigLora(cfg));
|
|
}
|
|
|
|
// -----------------------------------------------------------------------
|
|
// RegionInfo preset list integrity tests
|
|
// -----------------------------------------------------------------------
|
|
|
|
static void test_presetsStd_hasTenEntries()
|
|
{
|
|
// PROFILE_STD should have exactly 10 presets (adds MEDIUM_TURBO to the turbo cluster)
|
|
const RegionInfo *us = getRegion(meshtastic_Config_LoRaConfig_RegionCode_US);
|
|
TEST_ASSERT_EQUAL(10, us->getNumPresets());
|
|
TEST_ASSERT_EQUAL_PTR(PROFILE_STD.presets, us->getAvailablePresets());
|
|
}
|
|
|
|
static void test_presetsEU868_hasSevenEntries()
|
|
{
|
|
const RegionInfo *eu = getRegion(meshtastic_Config_LoRaConfig_RegionCode_EU_868);
|
|
TEST_ASSERT_EQUAL(7, eu->getNumPresets());
|
|
TEST_ASSERT_EQUAL_PTR(PROFILE_EU868.presets, eu->getAvailablePresets());
|
|
}
|
|
|
|
static void test_presetsUndef_hasOneEntry()
|
|
{
|
|
const RegionInfo *unset = getRegion(meshtastic_Config_LoRaConfig_RegionCode_UNSET);
|
|
TEST_ASSERT_EQUAL(1, unset->getNumPresets());
|
|
TEST_ASSERT_EQUAL_PTR(PROFILE_UNDEF.presets, unset->getAvailablePresets());
|
|
}
|
|
|
|
static void test_defaultPresetIsInAvailablePresets()
|
|
{
|
|
// For every region, the defaultPreset must appear in its own availablePresets list
|
|
const RegionInfo *r = regions;
|
|
while (true) {
|
|
bool found = false;
|
|
for (size_t i = 0; i < r->getNumPresets(); i++) {
|
|
if (r->getAvailablePresets()[i] == r->getDefaultPreset()) {
|
|
found = true;
|
|
break;
|
|
}
|
|
}
|
|
char msg[80];
|
|
snprintf(msg, sizeof(msg), "Region %s defaultPreset not in availablePresets", r->name);
|
|
TEST_ASSERT_TRUE_MESSAGE(found, msg);
|
|
|
|
if (r->code == meshtastic_Config_LoRaConfig_RegionCode_UNSET)
|
|
break; // UNSET is the sentinel, stop after it
|
|
r++;
|
|
}
|
|
}
|
|
|
|
static void test_regionFieldsAreSane()
|
|
{
|
|
// Basic sanity check: all regions have freqEnd > freqStart and a non-null name
|
|
const RegionInfo *r = regions;
|
|
while (true) {
|
|
char msg[80];
|
|
snprintf(msg, sizeof(msg), "Region %s: freqEnd must be > freqStart", r->name);
|
|
TEST_ASSERT_TRUE_MESSAGE(r->freqEnd > r->freqStart, msg);
|
|
TEST_ASSERT_NOT_NULL(r->name);
|
|
TEST_ASSERT_TRUE_MESSAGE(r->getNumPresets() > 0, "numPresets must be > 0");
|
|
TEST_ASSERT_NOT_NULL(r->getAvailablePresets());
|
|
|
|
if (r->code == meshtastic_Config_LoRaConfig_RegionCode_UNSET)
|
|
break;
|
|
r++;
|
|
}
|
|
}
|
|
|
|
static void test_onlyLORA24HasWideLora()
|
|
{
|
|
// Verify that LORA_24 is the only region with wideLora=true
|
|
const RegionInfo *r = regions;
|
|
while (true) {
|
|
char msg[80];
|
|
if (r->code == meshtastic_Config_LoRaConfig_RegionCode_LORA_24) {
|
|
snprintf(msg, sizeof(msg), "Region %s should have wideLora=true", r->name);
|
|
TEST_ASSERT_TRUE_MESSAGE(r->wideLora, msg);
|
|
} else {
|
|
snprintf(msg, sizeof(msg), "Region %s should have wideLora=false", r->name);
|
|
TEST_ASSERT_FALSE_MESSAGE(r->wideLora, msg);
|
|
}
|
|
|
|
if (r->code == meshtastic_Config_LoRaConfig_RegionCode_UNSET)
|
|
break;
|
|
r++;
|
|
}
|
|
}
|
|
|
|
// -----------------------------------------------------------------------
|
|
// Channel spacing calculation (placeholder for future protobuf updates)
|
|
// -----------------------------------------------------------------------
|
|
|
|
static void test_channelSpacingCalculation_US_LONG_FAST()
|
|
{
|
|
// Current formula: channelSpacing = spacing + (padding * 2) + (bw / 1000)
|
|
// US: spacing=0, padding=0
|
|
// LONG_FAST on non-wide region: bw=250 kHz
|
|
// channelSpacing = 0 + 0 + 0.250 = 0.250 MHz
|
|
// numChannels = round((928 - 902 + 0) / 0.250) = round(104) = 104
|
|
const RegionInfo *us = getRegion(meshtastic_Config_LoRaConfig_RegionCode_US);
|
|
float bw = modemPresetToBwKHz(meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST, us->wideLora);
|
|
float channelSpacing = us->profile->spacing + (us->profile->padding * 2) + (bw / 1000.0f);
|
|
uint32_t numChannels = (uint32_t)(((us->freqEnd - us->freqStart + us->profile->spacing) / channelSpacing) + 0.5f);
|
|
|
|
TEST_ASSERT_FLOAT_WITHIN(0.001f, 0.250f, channelSpacing);
|
|
TEST_ASSERT_EQUAL_UINT32(104, numChannels);
|
|
}
|
|
|
|
static void test_channelSpacingCalculation_EU868_LONG_FAST()
|
|
{
|
|
// EU_868: freqStart=869.4, freqEnd=869.65, spacing=0, padding=0
|
|
// LONG_FAST: bw=250 kHz => channelSpacing = 0.250 MHz
|
|
// numChannels = round((0.25 + 0) / 0.250) = 1
|
|
const RegionInfo *eu = getRegion(meshtastic_Config_LoRaConfig_RegionCode_EU_868);
|
|
float bw = modemPresetToBwKHz(meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST, eu->wideLora);
|
|
float channelSpacing = eu->profile->spacing + (eu->profile->padding * 2) + (bw / 1000.0f);
|
|
uint32_t numChannels = (uint32_t)(((eu->freqEnd - eu->freqStart + eu->profile->spacing) / channelSpacing) + 0.5f);
|
|
|
|
TEST_ASSERT_FLOAT_WITHIN(0.001f, 0.250f, channelSpacing);
|
|
TEST_ASSERT_EQUAL_UINT32(1, numChannels);
|
|
}
|
|
|
|
// Placeholder: when protobuf region definitions include non-zero padding/spacing,
|
|
// add tests here to verify the channel count and frequency calculations.
|
|
static void test_channelSpacingCalculation_placeholder()
|
|
{
|
|
// TODO: Once protobuf RegionInfo entries have non-zero padding or spacing values,
|
|
// verify:
|
|
// - Channel count matches expected value for each (region, preset) pair
|
|
// - First channel frequency = freqStart + (bw/2000) + padding
|
|
// - Nth channel frequency = first + (n * channelSpacing)
|
|
// - overrideSlot, when non-zero, forces the channel_num
|
|
TEST_PASS_MESSAGE("Placeholder for future channel spacing tests with updated protobuf region fields");
|
|
}
|
|
|
|
// -----------------------------------------------------------------------
|
|
// handleSetConfig fromOthers dispatch tests
|
|
// -----------------------------------------------------------------------
|
|
|
|
// AdminModuleTestShim comes from test/support - the friend seam AdminModule.h declares.
|
|
static AdminModuleTestShim *testAdmin;
|
|
static bool adminRadioGlobalsActive;
|
|
static NodeDB *savedNodeDB;
|
|
static NodeDB *replacementNodeDB;
|
|
static NodeInfoModule *savedNodeInfoModule;
|
|
static meshtastic_DeviceState savedDeviceState;
|
|
static meshtastic_User savedOwner;
|
|
static meshtastic_LocalConfig savedConfig;
|
|
static meshtastic_ChannelFile savedChannelFile;
|
|
|
|
static void replaceAdminRadioGlobals()
|
|
{
|
|
savedNodeDB = nodeDB;
|
|
savedNodeInfoModule = nodeInfoModule;
|
|
savedDeviceState = devicestate;
|
|
savedOwner = owner;
|
|
savedConfig = config;
|
|
savedChannelFile = channelFile;
|
|
replacementNodeDB = new NodeDB();
|
|
nodeDB = replacementNodeDB;
|
|
adminRadioGlobalsActive = true;
|
|
}
|
|
|
|
static void restoreAdminRadioGlobals()
|
|
{
|
|
if (!adminRadioGlobalsActive)
|
|
return;
|
|
nodeInfoModule = savedNodeInfoModule;
|
|
nodeDB = savedNodeDB;
|
|
delete replacementNodeDB;
|
|
replacementNodeDB = nullptr;
|
|
devicestate = savedDeviceState;
|
|
owner = savedOwner;
|
|
config = savedConfig;
|
|
channelFile = savedChannelFile;
|
|
initRegion();
|
|
adminRadioGlobalsActive = false;
|
|
}
|
|
|
|
static void installEncryptedAndAdminChannels()
|
|
{
|
|
channels.initDefaults();
|
|
meshtastic_Channel admin = meshtastic_Channel_init_zero;
|
|
admin.index = 1;
|
|
admin.role = meshtastic_Channel_Role_SECONDARY;
|
|
admin.has_settings = true;
|
|
strncpy(admin.settings.name, Channels::adminChannel, sizeof(admin.settings.name));
|
|
admin.settings.psk.size = 16;
|
|
memset(admin.settings.psk.bytes, 0xA5, admin.settings.psk.size);
|
|
channels.setChannel(admin);
|
|
|
|
meshtastic_Channel secondary = meshtastic_Channel_init_zero;
|
|
secondary.index = 2;
|
|
secondary.role = meshtastic_Channel_Role_SECONDARY;
|
|
secondary.has_settings = true;
|
|
strncpy(secondary.settings.name, "private", sizeof(secondary.settings.name));
|
|
secondary.settings.psk.size = 32;
|
|
memset(secondary.settings.psk.bytes, 0x5A, secondary.settings.psk.size);
|
|
channels.setChannel(secondary);
|
|
}
|
|
|
|
static void assertLicensedChannelsSanitized()
|
|
{
|
|
TEST_ASSERT_EQUAL(0, channels.getByIndex(0).settings.psk.size);
|
|
TEST_ASSERT_EQUAL(meshtastic_Channel_Role_DISABLED, channels.getByIndex(1).role);
|
|
TEST_ASSERT_EQUAL(0, channels.getByIndex(1).settings.psk.size);
|
|
TEST_ASSERT_EQUAL(0, channels.getByIndex(2).settings.psk.size);
|
|
}
|
|
|
|
static void test_handleSetOwner_persistsLicensedChannelSanitation()
|
|
{
|
|
replaceAdminRadioGlobals();
|
|
owner = meshtastic_User_init_zero;
|
|
config.lora.region = meshtastic_Config_LoRaConfig_RegionCode_UNSET;
|
|
installEncryptedAndAdminChannels();
|
|
|
|
meshtastic_User licensed = meshtastic_User_init_zero;
|
|
licensed.is_licensed = true;
|
|
testAdmin->deferSaves();
|
|
nodeInfoModule = reinterpret_cast<NodeInfoModule *>(1); // reloadOwner(false) only checks presence
|
|
testAdmin->handleSetOwner(licensed);
|
|
|
|
TEST_ASSERT_TRUE(testAdmin->savedSegments() & SEGMENT_CHANNELS);
|
|
assertLicensedChannelsSanitized();
|
|
|
|
uint8_t encoded[meshtastic_ChannelFile_size];
|
|
const size_t encodedSize = pb_encode_to_bytes(encoded, sizeof(encoded), &meshtastic_ChannelFile_msg, &channelFile);
|
|
TEST_ASSERT_GREATER_THAN(0, encodedSize);
|
|
meshtastic_ChannelFile reloaded = meshtastic_ChannelFile_init_zero;
|
|
TEST_ASSERT_TRUE(pb_decode_from_bytes(encoded, encodedSize, &meshtastic_ChannelFile_msg, &reloaded));
|
|
channelFile = reloaded;
|
|
assertLicensedChannelsSanitized();
|
|
TEST_ASSERT_FALSE_MESSAGE(channels.ensureLicensedOperation(), "sanitized reload must not trigger another persistence write");
|
|
}
|
|
|
|
static void test_bootDefense_sanitizesStaleLicensedChannelsOnce()
|
|
{
|
|
owner = meshtastic_User_init_zero;
|
|
owner.is_licensed = true;
|
|
installEncryptedAndAdminChannels();
|
|
|
|
TEST_ASSERT_TRUE(channels.ensureLicensedOperation());
|
|
assertLicensedChannelsSanitized();
|
|
TEST_ASSERT_FALSE_MESSAGE(channels.ensureLicensedOperation(), "boot sanitation must be idempotent");
|
|
}
|
|
|
|
static void test_restorePreferences_sanitizesLicensedBackupBeforeReturn()
|
|
{
|
|
NodeDB *savedNodeDB = nodeDB;
|
|
nodeDB = new NodeDB();
|
|
const meshtastic_DeviceState savedDeviceState = devicestate;
|
|
const meshtastic_ChannelFile savedChannelFile = channelFile;
|
|
|
|
owner = meshtastic_User_init_zero;
|
|
owner.is_licensed = true;
|
|
installEncryptedAndAdminChannels();
|
|
TEST_ASSERT_TRUE(nodeDB->backupPreferences(meshtastic_AdminMessage_BackupLocation_FLASH));
|
|
|
|
owner.is_licensed = false;
|
|
channels.initDefaults();
|
|
TEST_ASSERT_TRUE(
|
|
nodeDB->restorePreferences(meshtastic_AdminMessage_BackupLocation_FLASH, SEGMENT_DEVICESTATE | SEGMENT_CHANNELS));
|
|
TEST_ASSERT_TRUE(owner.is_licensed);
|
|
assertLicensedChannelsSanitized();
|
|
TEST_ASSERT_FALSE_MESSAGE(channels.ensureLicensedOperation(), "restored licensed channels must remain sanitized");
|
|
|
|
devicestate = savedDeviceState;
|
|
channelFile = savedChannelFile;
|
|
nodeDB->saveToDisk(SEGMENT_DEVICESTATE | SEGMENT_CHANNELS);
|
|
FSCom.remove(backupFileName);
|
|
delete nodeDB;
|
|
nodeDB = savedNodeDB;
|
|
}
|
|
|
|
static meshtastic_Config makeLoraSetConfig(meshtastic_Config_LoRaConfig_RegionCode region, bool usePreset,
|
|
meshtastic_Config_LoRaConfig_ModemPreset preset)
|
|
{
|
|
meshtastic_Config c = meshtastic_Config_init_zero;
|
|
c.which_payload_variant = meshtastic_Config_lora_tag;
|
|
c.payload_variant.lora.region = region;
|
|
c.payload_variant.lora.use_preset = usePreset;
|
|
c.payload_variant.lora.modem_preset = preset;
|
|
return c;
|
|
}
|
|
|
|
static void test_handleSetConfig_persistsLicensedFirstRegionIdentity()
|
|
{
|
|
replaceAdminRadioGlobals();
|
|
owner = meshtastic_User_init_zero;
|
|
owner.is_licensed = true;
|
|
config.security = meshtastic_Config_SecurityConfig_init_zero;
|
|
config.lora = meshtastic_Config_LoRaConfig_init_zero;
|
|
config.lora.region = meshtastic_Config_LoRaConfig_RegionCode_UNSET;
|
|
initRegion();
|
|
|
|
testAdmin->deferSaves();
|
|
const meshtastic_Config c =
|
|
makeLoraSetConfig(meshtastic_Config_LoRaConfig_RegionCode_US, true, meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST);
|
|
testAdmin->handleSetConfig(c, false);
|
|
|
|
const int expectedSegments = SEGMENT_CONFIG | SEGMENT_MODULECONFIG | SEGMENT_DEVICESTATE | SEGMENT_NODEDATABASE;
|
|
TEST_ASSERT_EQUAL_INT(expectedSegments, testAdmin->savedSegments());
|
|
TEST_ASSERT_EQUAL(32, config.security.private_key.size);
|
|
TEST_ASSERT_EQUAL(32, owner.public_key.size);
|
|
}
|
|
|
|
static void test_handleSetConfig_fromOthers_invalidPresetRejected()
|
|
{
|
|
// Set up a known-good baseline in the global config
|
|
config.lora = meshtastic_Config_LoRaConfig_init_zero;
|
|
config.lora.region = meshtastic_Config_LoRaConfig_RegionCode_EU_868;
|
|
config.lora.use_preset = true;
|
|
config.lora.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST;
|
|
initRegion();
|
|
|
|
// Build an admin set_config with an invalid preset for EU_868
|
|
meshtastic_Config c = makeLoraSetConfig(meshtastic_Config_LoRaConfig_RegionCode_EU_868, true,
|
|
meshtastic_Config_LoRaConfig_ModemPreset_SHORT_TURBO);
|
|
|
|
testAdmin->handleSetConfig(c, true); // fromOthers = true
|
|
|
|
// fromOthers=true: invalid preset should be rejected, old preset preserved
|
|
TEST_ASSERT_EQUAL(meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST, config.lora.modem_preset);
|
|
}
|
|
|
|
static void test_handleSetConfig_fromLocal_invalidPresetClamped()
|
|
{
|
|
// Set up a known-good baseline
|
|
config.lora = meshtastic_Config_LoRaConfig_init_zero;
|
|
config.lora.region = meshtastic_Config_LoRaConfig_RegionCode_EU_868;
|
|
config.lora.use_preset = true;
|
|
config.lora.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST;
|
|
initRegion();
|
|
|
|
// Build an admin set_config with an invalid preset for EU_868
|
|
meshtastic_Config c = makeLoraSetConfig(meshtastic_Config_LoRaConfig_RegionCode_EU_868, true,
|
|
meshtastic_Config_LoRaConfig_ModemPreset_SHORT_TURBO);
|
|
|
|
testAdmin->handleSetConfig(c, false); // fromOthers = false (local client)
|
|
|
|
// fromOthers=false: invalid preset should be clamped to the region's default
|
|
const RegionInfo *eu868 = getRegion(meshtastic_Config_LoRaConfig_RegionCode_EU_868);
|
|
TEST_ASSERT_EQUAL(eu868->getDefaultPreset(), config.lora.modem_preset);
|
|
}
|
|
|
|
static void test_handleSetConfig_fromOthers_validPresetAccepted()
|
|
{
|
|
// Set up baseline
|
|
config.lora = meshtastic_Config_LoRaConfig_init_zero;
|
|
config.lora.region = meshtastic_Config_LoRaConfig_RegionCode_EU_868;
|
|
config.lora.use_preset = true;
|
|
config.lora.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST;
|
|
initRegion();
|
|
|
|
// Build an admin set_config with a valid preset for EU_868
|
|
meshtastic_Config c = makeLoraSetConfig(meshtastic_Config_LoRaConfig_RegionCode_EU_868, true,
|
|
meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_FAST);
|
|
|
|
testAdmin->handleSetConfig(c, true); // fromOthers = true
|
|
|
|
// Valid preset should be accepted regardless of fromOthers
|
|
TEST_ASSERT_EQUAL(meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_FAST, config.lora.modem_preset);
|
|
}
|
|
|
|
static void test_handleSetConfig_fromOthers_invalidChannelNumFullyRejected()
|
|
{
|
|
// Rejecting a remote config must reject ALL of it: an invalid channel_num must not
|
|
// leak into config.lora alongside the restored region/preset.
|
|
config.lora = meshtastic_Config_LoRaConfig_init_zero;
|
|
config.lora.region = meshtastic_Config_LoRaConfig_RegionCode_US;
|
|
config.lora.use_preset = true;
|
|
config.lora.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST;
|
|
config.lora.channel_num = 0;
|
|
initRegion();
|
|
|
|
meshtastic_Config c =
|
|
makeLoraSetConfig(meshtastic_Config_LoRaConfig_RegionCode_US, true, meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST);
|
|
c.payload_variant.lora.channel_num = 5000; // far beyond US slot count
|
|
|
|
testAdmin->handleSetConfig(c, true); // fromOthers = true
|
|
|
|
TEST_ASSERT_EQUAL(meshtastic_Config_LoRaConfig_RegionCode_US, config.lora.region);
|
|
TEST_ASSERT_EQUAL(meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST, config.lora.modem_preset);
|
|
TEST_ASSERT_EQUAL_UINT32(0, config.lora.channel_num);
|
|
}
|
|
|
|
// clampBandwidthCode: an unset (0) bandwidth code maps to the default; any other code is left as-is.
|
|
static void test_clampBandwidthCode_zeroMapsToDefaultOthersUnchanged()
|
|
{
|
|
TEST_ASSERT_NOT_EQUAL_UINT16(0, clampBandwidthCode(0)); // the point of the fix: 0 must not stay 0
|
|
TEST_ASSERT_EQUAL_UINT16(bwKHzToCode(LORA_BW_DEFAULT_KHZ), clampBandwidthCode(0));
|
|
TEST_ASSERT_EQUAL_UINT16(250, clampBandwidthCode(250));
|
|
TEST_ASSERT_EQUAL_UINT16(125, clampBandwidthCode(125));
|
|
TEST_ASSERT_EQUAL_UINT16(31, clampBandwidthCode(31));
|
|
}
|
|
|
|
// A custom (non-preset) config that leaves bandwidth at its proto zero-value must not persist as 0.
|
|
// Pre-fix it slipped past validateConfigLora() and the radio silently ran at the default while
|
|
// get_config still reported bandwidth 0. It is now coerced to the default code on ingest.
|
|
static void test_handleSetConfig_fromLocal_customBandwidthZeroClampedToDefault()
|
|
{
|
|
config.lora = meshtastic_Config_LoRaConfig_init_zero;
|
|
config.lora.region = meshtastic_Config_LoRaConfig_RegionCode_US;
|
|
config.lora.use_preset = true;
|
|
config.lora.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST;
|
|
initRegion();
|
|
|
|
meshtastic_Config c =
|
|
makeLoraSetConfig(meshtastic_Config_LoRaConfig_RegionCode_US, false, meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST);
|
|
c.payload_variant.lora.spread_factor = 11;
|
|
c.payload_variant.lora.coding_rate = 5;
|
|
c.payload_variant.lora.bandwidth = 0; // the footgun: unset custom bandwidth
|
|
|
|
testAdmin->handleSetConfig(c, false); // fromOthers = false (local client)
|
|
|
|
TEST_ASSERT_FALSE(config.lora.use_preset);
|
|
TEST_ASSERT_NOT_EQUAL_UINT16(0, config.lora.bandwidth); // must not persist as 0
|
|
TEST_ASSERT_EQUAL_UINT16(bwKHzToCode(LORA_BW_DEFAULT_KHZ), config.lora.bandwidth);
|
|
}
|
|
|
|
// Remote admin (fromOthers) is subject to the same ingest clamp: a custom bandwidth 0 from another
|
|
// node is normalized to the default rather than persisted as 0 (it does not weaken the wholesale
|
|
// rejection of configs that actually fail validation - a 0 bandwidth already passed validation).
|
|
static void test_handleSetConfig_fromOthers_customBandwidthZeroClampedToDefault()
|
|
{
|
|
config.lora = meshtastic_Config_LoRaConfig_init_zero;
|
|
config.lora.region = meshtastic_Config_LoRaConfig_RegionCode_US;
|
|
config.lora.use_preset = true;
|
|
config.lora.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST;
|
|
initRegion();
|
|
|
|
meshtastic_Config c =
|
|
makeLoraSetConfig(meshtastic_Config_LoRaConfig_RegionCode_US, false, meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST);
|
|
c.payload_variant.lora.spread_factor = 11;
|
|
c.payload_variant.lora.coding_rate = 5;
|
|
c.payload_variant.lora.bandwidth = 0;
|
|
|
|
testAdmin->handleSetConfig(c, true); // fromOthers = true
|
|
|
|
TEST_ASSERT_FALSE(config.lora.use_preset);
|
|
TEST_ASSERT_EQUAL_UINT16(bwKHzToCode(LORA_BW_DEFAULT_KHZ), config.lora.bandwidth);
|
|
}
|
|
|
|
// In preset mode bandwidth 0 is the norm (the preset supplies it); the ingest clamp must leave it
|
|
// untouched so preset configs still read back bandwidth 0.
|
|
static void test_handleSetConfig_fromLocal_presetBandwidthZeroLeftUntouched()
|
|
{
|
|
config.lora = meshtastic_Config_LoRaConfig_init_zero;
|
|
config.lora.region = meshtastic_Config_LoRaConfig_RegionCode_US;
|
|
config.lora.use_preset = true;
|
|
config.lora.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST;
|
|
initRegion();
|
|
|
|
meshtastic_Config c =
|
|
makeLoraSetConfig(meshtastic_Config_LoRaConfig_RegionCode_US, true, meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST);
|
|
c.payload_variant.lora.bandwidth = 0;
|
|
|
|
testAdmin->handleSetConfig(c, false);
|
|
|
|
TEST_ASSERT_TRUE(config.lora.use_preset);
|
|
TEST_ASSERT_EQUAL_UINT16(0, config.lora.bandwidth);
|
|
}
|
|
|
|
// A custom (non-preset) config with an already-valid bandwidth must be preserved verbatim.
|
|
static void test_handleSetConfig_fromLocal_customBandwidthNonZeroPreserved()
|
|
{
|
|
config.lora = meshtastic_Config_LoRaConfig_init_zero;
|
|
config.lora.region = meshtastic_Config_LoRaConfig_RegionCode_US;
|
|
config.lora.use_preset = true;
|
|
config.lora.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST;
|
|
initRegion();
|
|
|
|
meshtastic_Config c =
|
|
makeLoraSetConfig(meshtastic_Config_LoRaConfig_RegionCode_US, false, meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST);
|
|
c.payload_variant.lora.spread_factor = 11;
|
|
c.payload_variant.lora.coding_rate = 5;
|
|
c.payload_variant.lora.bandwidth = 125;
|
|
|
|
testAdmin->handleSetConfig(c, false);
|
|
|
|
TEST_ASSERT_FALSE(config.lora.use_preset);
|
|
TEST_ASSERT_EQUAL_UINT16(125, config.lora.bandwidth);
|
|
}
|
|
|
|
// A security-config SET that omits the private key (partial/legacy client editing some other security field)
|
|
// must NOT regenerate our keypair: our NodeNum is crc32(public_key), so a new keypair would silently change
|
|
// our identity. The existing keypair has to be preserved.
|
|
static void test_handleSetConfig_security_preservesKeypairWhenPrivateOmitted()
|
|
{
|
|
config.security = meshtastic_Config_SecurityConfig_init_zero;
|
|
config.security.private_key.size = 32;
|
|
memset(config.security.private_key.bytes, 0x11, 32);
|
|
config.security.public_key.size = 32;
|
|
memset(config.security.public_key.bytes, 0x22, 32);
|
|
|
|
// Incoming SET carries no private/public key, just another security field.
|
|
meshtastic_Config c = meshtastic_Config_init_zero;
|
|
c.which_payload_variant = meshtastic_Config_security_tag;
|
|
c.payload_variant.security.serial_enabled = true;
|
|
|
|
testAdmin->deferSaves();
|
|
testAdmin->handleSetConfig(c, false);
|
|
|
|
uint8_t expectedPriv[32];
|
|
memset(expectedPriv, 0x11, 32);
|
|
uint8_t expectedPub[32];
|
|
memset(expectedPub, 0x22, 32);
|
|
TEST_ASSERT_EQUAL_UINT(32, config.security.private_key.size);
|
|
TEST_ASSERT_EQUAL_MEMORY(expectedPriv, config.security.private_key.bytes, 32);
|
|
TEST_ASSERT_EQUAL_UINT(32, config.security.public_key.size);
|
|
TEST_ASSERT_EQUAL_MEMORY(expectedPub, config.security.public_key.bytes, 32);
|
|
// The non-key field still applies.
|
|
TEST_ASSERT_TRUE(config.security.serial_enabled);
|
|
}
|
|
|
|
// A SET that DOES supply a full 32-byte keypair (legitimate key import) must apply it, not preserve the old one.
|
|
static void test_handleSetConfig_security_acceptsSuppliedKeypair()
|
|
{
|
|
config.security = meshtastic_Config_SecurityConfig_init_zero;
|
|
config.security.private_key.size = 32;
|
|
memset(config.security.private_key.bytes, 0x11, 32);
|
|
config.security.public_key.size = 32;
|
|
memset(config.security.public_key.bytes, 0x22, 32);
|
|
|
|
meshtastic_Config c = meshtastic_Config_init_zero;
|
|
c.which_payload_variant = meshtastic_Config_security_tag;
|
|
c.payload_variant.security.private_key.size = 32;
|
|
memset(c.payload_variant.security.private_key.bytes, 0x33, 32);
|
|
c.payload_variant.security.public_key.size = 32;
|
|
memset(c.payload_variant.security.public_key.bytes, 0x44, 32);
|
|
|
|
testAdmin->deferSaves();
|
|
testAdmin->handleSetConfig(c, false);
|
|
|
|
uint8_t expectedPriv[32];
|
|
memset(expectedPriv, 0x33, 32);
|
|
uint8_t expectedPub[32];
|
|
memset(expectedPub, 0x44, 32);
|
|
TEST_ASSERT_EQUAL_MEMORY(expectedPriv, config.security.private_key.bytes, 32);
|
|
TEST_ASSERT_EQUAL_MEMORY(expectedPub, config.security.public_key.bytes, 32);
|
|
}
|
|
|
|
// Issue #11073: "regenerate keys" sends a blank SecurityConfig holding only the new private key. Replacing
|
|
// the whole struct with it wiped the admin keys, locking the owner out of remote admin.
|
|
static void test_handleSetConfig_security_rotationPreservesAdminKeys()
|
|
{
|
|
config.security = meshtastic_Config_SecurityConfig_init_zero;
|
|
config.security.private_key.size = 32;
|
|
memset(config.security.private_key.bytes, 0x11, 32);
|
|
config.security.public_key.size = 32;
|
|
memset(config.security.public_key.bytes, 0x22, 32);
|
|
config.security.admin_key_count = 2;
|
|
config.security.admin_key[0].size = 32;
|
|
memset(config.security.admin_key[0].bytes, 0xAA, 32);
|
|
config.security.admin_key[1].size = 32;
|
|
memset(config.security.admin_key[1].bytes, 0xBB, 32);
|
|
config.security.is_managed = true;
|
|
config.security.serial_enabled = true;
|
|
config.security.packet_signature_policy =
|
|
meshtastic_Config_SecurityConfig_PacketSignaturePolicy_PACKET_SIGNATURE_POLICY_STRICT;
|
|
|
|
// Exactly what the regenerate dialog emits.
|
|
meshtastic_Config c = meshtastic_Config_init_zero;
|
|
c.which_payload_variant = meshtastic_Config_security_tag;
|
|
c.payload_variant.security.private_key.size = 32;
|
|
memset(c.payload_variant.security.private_key.bytes, 0x33, 32);
|
|
|
|
testAdmin->deferSaves();
|
|
testAdmin->handleSetConfig(c, false);
|
|
|
|
uint8_t expectedPriv[32];
|
|
memset(expectedPriv, 0x33, 32);
|
|
TEST_ASSERT_EQUAL_UINT(32, config.security.private_key.size);
|
|
TEST_ASSERT_EQUAL_MEMORY(expectedPriv, config.security.private_key.bytes, 32);
|
|
|
|
uint8_t expectedAdmin0[32], expectedAdmin1[32];
|
|
memset(expectedAdmin0, 0xAA, 32);
|
|
memset(expectedAdmin1, 0xBB, 32);
|
|
TEST_ASSERT_EQUAL_UINT(2, config.security.admin_key_count);
|
|
TEST_ASSERT_EQUAL_UINT(32, config.security.admin_key[0].size);
|
|
TEST_ASSERT_EQUAL_MEMORY(expectedAdmin0, config.security.admin_key[0].bytes, 32);
|
|
TEST_ASSERT_EQUAL_UINT(32, config.security.admin_key[1].size);
|
|
TEST_ASSERT_EQUAL_MEMORY(expectedAdmin1, config.security.admin_key[1].bytes, 32);
|
|
TEST_ASSERT_TRUE(config.security.is_managed);
|
|
TEST_ASSERT_TRUE(config.security.serial_enabled);
|
|
TEST_ASSERT_EQUAL(meshtastic_Config_SecurityConfig_PacketSignaturePolicy_PACKET_SIGNATURE_POLICY_STRICT,
|
|
config.security.packet_signature_policy);
|
|
}
|
|
|
|
// The escape hatch: a SET that leaves the private key alone still clears admin keys.
|
|
static void test_handleSetConfig_security_clearsAdminKeysWhenKeypairUnchanged()
|
|
{
|
|
config.security = meshtastic_Config_SecurityConfig_init_zero;
|
|
config.security.private_key.size = 32;
|
|
memset(config.security.private_key.bytes, 0x11, 32);
|
|
config.security.public_key.size = 32;
|
|
memset(config.security.public_key.bytes, 0x22, 32);
|
|
config.security.admin_key_count = 1;
|
|
config.security.admin_key[0].size = 32;
|
|
memset(config.security.admin_key[0].bytes, 0xAA, 32);
|
|
|
|
// Same private key we already hold, empty admin key list.
|
|
meshtastic_Config c = meshtastic_Config_init_zero;
|
|
c.which_payload_variant = meshtastic_Config_security_tag;
|
|
c.payload_variant.security.private_key.size = 32;
|
|
memset(c.payload_variant.security.private_key.bytes, 0x11, 32);
|
|
c.payload_variant.security.public_key.size = 32;
|
|
memset(c.payload_variant.security.public_key.bytes, 0x22, 32);
|
|
|
|
testAdmin->deferSaves();
|
|
testAdmin->handleSetConfig(c, false);
|
|
|
|
TEST_ASSERT_EQUAL_UINT(0, config.security.admin_key_count);
|
|
TEST_ASSERT_EQUAL_UINT(0, config.security.admin_key[0].size);
|
|
}
|
|
|
|
static void test_regionInfo_supportsPreset()
|
|
{
|
|
const RegionInfo *eu868 = getRegion(meshtastic_Config_LoRaConfig_RegionCode_EU_868);
|
|
TEST_ASSERT_TRUE(eu868->supportsPreset(meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST));
|
|
TEST_ASSERT_FALSE(eu868->supportsPreset(meshtastic_Config_LoRaConfig_ModemPreset_SHORT_TURBO));
|
|
TEST_ASSERT_FALSE(eu868->supportsPreset(meshtastic_Config_LoRaConfig_ModemPreset_NARROW_FAST));
|
|
|
|
const RegionInfo *eu866 = getRegion(meshtastic_Config_LoRaConfig_RegionCode_EU_866);
|
|
TEST_ASSERT_TRUE(eu866->supportsPreset(meshtastic_Config_LoRaConfig_ModemPreset_LITE_SLOW));
|
|
TEST_ASSERT_FALSE(eu866->supportsPreset(meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST));
|
|
}
|
|
|
|
static void test_checkConfigRegion_quietCheckReportsReason()
|
|
{
|
|
meshtastic_Config_LoRaConfig cfg = meshtastic_Config_LoRaConfig_init_zero;
|
|
cfg.region = meshtastic_Config_LoRaConfig_RegionCode_US;
|
|
TEST_ASSERT_TRUE(RadioInterface::checkConfigRegion(cfg));
|
|
|
|
cfg.region = (meshtastic_Config_LoRaConfig_RegionCode)254;
|
|
char err[160] = {0};
|
|
TEST_ASSERT_FALSE(RadioInterface::checkConfigRegion(cfg, err, sizeof(err)));
|
|
TEST_ASSERT_TRUE_MESSAGE(strlen(err) > 0, "Expected a failure reason in errBuf");
|
|
}
|
|
|
|
static void test_checkConfigRegion_allowsProspectiveLicensedOwner()
|
|
{
|
|
meshtastic_Config_LoRaConfig cfg = meshtastic_Config_LoRaConfig_init_zero;
|
|
cfg.region = meshtastic_Config_LoRaConfig_RegionCode_ITU1_2M;
|
|
devicestate.owner.is_licensed = false;
|
|
|
|
TEST_ASSERT_FALSE(RadioInterface::checkConfigRegion(cfg));
|
|
TEST_ASSERT_TRUE(RadioInterface::checkConfigRegion(cfg, nullptr, 0, true));
|
|
}
|
|
|
|
static void test_handleSetConfig_fromOthers_siblingLockedPresetSwapsRegion()
|
|
{
|
|
// Baseline: EU_866 (LITE profile)
|
|
config.lora = meshtastic_Config_LoRaConfig_init_zero;
|
|
config.lora.region = meshtastic_Config_LoRaConfig_RegionCode_EU_866;
|
|
config.lora.use_preset = true;
|
|
config.lora.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_LITE_FAST;
|
|
initRegion();
|
|
|
|
// Remote admin keeps the region but selects a NARROW preset (locked to EU_N_868)
|
|
meshtastic_Config c = makeLoraSetConfig(meshtastic_Config_LoRaConfig_RegionCode_EU_866, true,
|
|
meshtastic_Config_LoRaConfig_ModemPreset_NARROW_FAST);
|
|
|
|
testAdmin->handleSetConfig(c, true); // fromOthers = true
|
|
|
|
TEST_ASSERT_EQUAL(meshtastic_Config_LoRaConfig_RegionCode_EU_N_868, config.lora.region);
|
|
TEST_ASSERT_EQUAL(meshtastic_Config_LoRaConfig_ModemPreset_NARROW_FAST, config.lora.modem_preset);
|
|
|
|
// Restore the region table pointer for subsequent tests
|
|
config.lora.region = meshtastic_Config_LoRaConfig_RegionCode_UNSET;
|
|
initRegion();
|
|
}
|
|
|
|
static void test_handleSetConfig_fromOthers_lockedPresetFromNonTrioRegionRejected()
|
|
{
|
|
// Baseline: US is not one of the swappable trio, so a LITE preset must be rejected
|
|
config.lora = meshtastic_Config_LoRaConfig_init_zero;
|
|
config.lora.region = meshtastic_Config_LoRaConfig_RegionCode_US;
|
|
config.lora.use_preset = true;
|
|
config.lora.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST;
|
|
initRegion();
|
|
|
|
meshtastic_Config c =
|
|
makeLoraSetConfig(meshtastic_Config_LoRaConfig_RegionCode_US, true, meshtastic_Config_LoRaConfig_ModemPreset_LITE_FAST);
|
|
|
|
testAdmin->handleSetConfig(c, true); // fromOthers = true
|
|
|
|
TEST_ASSERT_EQUAL(meshtastic_Config_LoRaConfig_RegionCode_US, config.lora.region);
|
|
TEST_ASSERT_EQUAL(meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST, config.lora.modem_preset);
|
|
}
|
|
|
|
// -----------------------------------------------------------------------
|
|
// Channel-configuration warning + coalescing tests
|
|
//
|
|
// These exercise the real incoming-admin-message path (handleReceivedProtobuf):
|
|
// begin_edit_settings / set_channel / commit_edit_settings. Warnings raised while a
|
|
// transaction is open must be deferred and collapsed into a single notification at
|
|
// commit; outside a transaction each save emits its own single message immediately.
|
|
// -----------------------------------------------------------------------
|
|
|
|
static const uint8_t DEFAULT_KEY[] = {0x01}; // the well-known "default" PSK (AQ==)
|
|
static const uint8_t CUSTOM_KEY[] = {0x42, 0x17}; // any non-default key
|
|
|
|
// Count captured warnings whose text contains substr.
|
|
static int warningsContaining(const char *substr)
|
|
{
|
|
int n = 0;
|
|
for (const auto &w : capturedWarnings)
|
|
if (w.find(substr) != std::string::npos)
|
|
n++;
|
|
return n;
|
|
}
|
|
|
|
static meshtastic_Channel makeChannel(int8_t index, meshtastic_Channel_Role role, const char *name, const uint8_t *psk,
|
|
size_t pskLen)
|
|
{
|
|
meshtastic_Channel ch = meshtastic_Channel_init_zero;
|
|
ch.index = index;
|
|
ch.role = role;
|
|
ch.has_settings = true;
|
|
strncpy(ch.settings.name, name, sizeof(ch.settings.name) - 1);
|
|
ch.settings.psk.size = pskLen;
|
|
for (size_t i = 0; i < pskLen; i++)
|
|
ch.settings.psk.bytes[i] = psk[i];
|
|
return ch;
|
|
}
|
|
|
|
// Dispatch one admin message as if it arrived from a local (from==0) client, which bypasses
|
|
// the passkey/authorization gates so the switch body runs.
|
|
static void sendAdmin(meshtastic_AdminMessage &m)
|
|
{
|
|
meshtastic_MeshPacket mp = meshtastic_MeshPacket_init_zero;
|
|
mp.from = 0;
|
|
mp.which_payload_variant = meshtastic_MeshPacket_decoded_tag; // required: handler drops non-decoded packets
|
|
testAdmin->handleReceivedProtobuf(mp, &m);
|
|
}
|
|
|
|
static void sendSetChannel(const meshtastic_Channel &ch)
|
|
{
|
|
meshtastic_AdminMessage m = meshtastic_AdminMessage_init_zero;
|
|
m.which_payload_variant = meshtastic_AdminMessage_set_channel_tag;
|
|
m.set_channel = ch;
|
|
sendAdmin(m);
|
|
}
|
|
|
|
static void sendBeginEdit()
|
|
{
|
|
meshtastic_AdminMessage m = meshtastic_AdminMessage_init_zero;
|
|
m.which_payload_variant = meshtastic_AdminMessage_begin_edit_settings_tag;
|
|
m.begin_edit_settings = true;
|
|
sendAdmin(m);
|
|
}
|
|
|
|
static void sendCommitEdit()
|
|
{
|
|
meshtastic_AdminMessage m = meshtastic_AdminMessage_init_zero;
|
|
m.which_payload_variant = meshtastic_AdminMessage_commit_edit_settings_tag;
|
|
m.commit_edit_settings = true;
|
|
sendAdmin(m);
|
|
}
|
|
|
|
// Preset = LongFast on US, unlicensed owner. "LongFast" is the display name we compare against.
|
|
static void usePresetLongFast()
|
|
{
|
|
config.lora = meshtastic_Config_LoRaConfig_init_zero;
|
|
config.lora.region = meshtastic_Config_LoRaConfig_RegionCode_US;
|
|
config.lora.use_preset = true;
|
|
config.lora.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST;
|
|
initRegion();
|
|
owner.is_licensed = false;
|
|
}
|
|
|
|
static void test_warn_singleChannel_variantName_oneSpecificMessage()
|
|
{
|
|
usePresetLongFast();
|
|
// Name is a case/space variant of the preset with the default key: a single name issue.
|
|
sendSetChannel(makeChannel(0, meshtastic_Channel_Role_PRIMARY, "long fast", DEFAULT_KEY, 1));
|
|
TEST_ASSERT_EQUAL_INT(1, (int)capturedWarnings.size());
|
|
TEST_ASSERT_EQUAL_INT(1, warningsContaining("looks like a mistype of 'LongFast'"));
|
|
}
|
|
|
|
static void test_warn_singleChannel_nameAndPsk_collapsedToCatchAll()
|
|
{
|
|
usePresetLongFast();
|
|
// Variant name AND a non-default key: two issues on one channel collapse to one catch-all.
|
|
sendSetChannel(makeChannel(0, meshtastic_Channel_Role_PRIMARY, "long fast", CUSTOM_KEY, 2));
|
|
TEST_ASSERT_EQUAL_INT(1, (int)capturedWarnings.size());
|
|
TEST_ASSERT_EQUAL_INT(1, warningsContaining("There may be name and PSK issues on channel 0"));
|
|
}
|
|
|
|
static void test_warn_cleanChannel_noMessage()
|
|
{
|
|
usePresetLongFast();
|
|
// Exact preset name + default key: nothing to warn about.
|
|
sendSetChannel(makeChannel(0, meshtastic_Channel_Role_PRIMARY, "LongFast", DEFAULT_KEY, 1));
|
|
TEST_ASSERT_EQUAL_INT(0, (int)capturedWarnings.size());
|
|
}
|
|
|
|
static void test_warn_transaction_multipleChannels_singleCoalescedMessage()
|
|
{
|
|
usePresetLongFast();
|
|
sendBeginEdit();
|
|
sendSetChannel(makeChannel(0, meshtastic_Channel_Role_PRIMARY, "long fast", DEFAULT_KEY, 1));
|
|
sendSetChannel(makeChannel(1, meshtastic_Channel_Role_SECONDARY, "long fast", DEFAULT_KEY, 1));
|
|
// Nothing emitted yet - warnings are deferred until commit.
|
|
TEST_ASSERT_EQUAL_INT(0, (int)capturedWarnings.size());
|
|
|
|
sendCommitEdit();
|
|
// Exactly one message, naming both channels.
|
|
TEST_ASSERT_EQUAL_INT(1, (int)capturedWarnings.size());
|
|
TEST_ASSERT_EQUAL_INT(1, warningsContaining("There may be name issues on channels 0, 1"));
|
|
}
|
|
|
|
static void test_warn_transaction_singleChannel_keepsSpecificMessage()
|
|
{
|
|
usePresetLongFast();
|
|
sendBeginEdit();
|
|
sendSetChannel(makeChannel(0, meshtastic_Channel_Role_PRIMARY, "long fast", DEFAULT_KEY, 1));
|
|
TEST_ASSERT_EQUAL_INT(0, (int)capturedWarnings.size());
|
|
|
|
sendCommitEdit();
|
|
// One flagged channel: the specific message verbatim, not the plural catch-all.
|
|
TEST_ASSERT_EQUAL_INT(1, (int)capturedWarnings.size());
|
|
TEST_ASSERT_EQUAL_INT(1, warningsContaining("looks like a mistype of 'LongFast'"));
|
|
TEST_ASSERT_EQUAL_INT(0, warningsContaining("on channels"));
|
|
}
|
|
|
|
static void test_warn_license_noTransaction_emittedImmediately()
|
|
{
|
|
usePresetLongFast();
|
|
owner.is_licensed = true;
|
|
// Setting a channel that still carries a key triggers ensureLicensedOperation() to strip it.
|
|
sendSetChannel(makeChannel(0, meshtastic_Channel_Role_PRIMARY, "", CUSTOM_KEY, 2));
|
|
TEST_ASSERT_EQUAL_INT(1, warningsContaining("Licensed mode activated"));
|
|
}
|
|
|
|
static void test_warn_license_transaction_coalescedToSingleMessage()
|
|
{
|
|
usePresetLongFast();
|
|
owner.is_licensed = true;
|
|
sendBeginEdit();
|
|
// Two separate triggers within one transaction (two channels with keys to strip).
|
|
sendSetChannel(makeChannel(0, meshtastic_Channel_Role_PRIMARY, "", CUSTOM_KEY, 2));
|
|
sendSetChannel(makeChannel(1, meshtastic_Channel_Role_SECONDARY, "", CUSTOM_KEY, 2));
|
|
TEST_ASSERT_EQUAL_INT(0, (int)capturedWarnings.size());
|
|
|
|
sendCommitEdit();
|
|
// Collapsed to a single licensed-mode notice (and no channel warning, since names are blank).
|
|
TEST_ASSERT_EQUAL_INT(1, warningsContaining("Licensed mode activated"));
|
|
TEST_ASSERT_EQUAL_INT(1, (int)capturedWarnings.size());
|
|
}
|
|
|
|
// -----------------------------------------------------------------------
|
|
// Test runner
|
|
// -----------------------------------------------------------------------
|
|
|
|
void setUp(void)
|
|
{
|
|
mockMeshService = new MockMeshService();
|
|
service = mockMeshService;
|
|
testAdmin = new AdminModuleTestShim();
|
|
capturedWarnings.clear();
|
|
// Committing an edit transaction triggers a full saveToDisk(), which dereferences nodeDB.
|
|
// Create it once (kept reachable via the global, so no leak) for the warning tests; the
|
|
// other tests in this suite set their own config/region state and are unaffected.
|
|
if (!nodeDB)
|
|
nodeDB = new NodeDB();
|
|
}
|
|
void tearDown(void)
|
|
{
|
|
restoreAdminRadioGlobals();
|
|
service = nullptr;
|
|
delete mockMeshService;
|
|
mockMeshService = nullptr;
|
|
delete testAdmin;
|
|
testAdmin = nullptr;
|
|
}
|
|
|
|
void setup()
|
|
{
|
|
delay(10);
|
|
delay(2000);
|
|
|
|
initializeTestEnvironment();
|
|
|
|
UNITY_BEGIN();
|
|
|
|
// getRegion()
|
|
RUN_TEST(test_handleSetOwner_persistsLicensedChannelSanitation);
|
|
RUN_TEST(test_handleSetConfig_persistsLicensedFirstRegionIdentity);
|
|
RUN_TEST(test_bootDefense_sanitizesStaleLicensedChannelsOnce);
|
|
RUN_TEST(test_restorePreferences_sanitizesLicensedBackupBeforeReturn);
|
|
RUN_TEST(test_getRegion_returnsCorrectRegion_US);
|
|
RUN_TEST(test_getRegion_returnsCorrectRegion_EU868);
|
|
RUN_TEST(test_getRegion_returnsCorrectRegion_LORA24);
|
|
RUN_TEST(test_getRegion_unsetCodeReturnsUnsetEntry);
|
|
RUN_TEST(test_getRegion_unknownCodeFallsToUnset);
|
|
|
|
// validateConfigRegion()
|
|
RUN_TEST(test_validateConfigRegion_validRegionReturnsTrue);
|
|
RUN_TEST(test_validateConfigRegion_unsetRegionReturnsTrue);
|
|
RUN_TEST(test_validateConfigRegion_unknownCodeReturnsFalse);
|
|
RUN_TEST(test_validateConfigRegion_anotherUnknownCodeReturnsFalse);
|
|
|
|
// Shadow table tests
|
|
RUN_TEST(test_shadowTable_spacedProfileHasNonZeroSpacing);
|
|
RUN_TEST(test_shadowTable_licensedProfileFlagsCorrect);
|
|
RUN_TEST(test_shadowTable_presetCountMatchesExpected);
|
|
RUN_TEST(test_shadowTable_defaultPresetIsFirstInList);
|
|
RUN_TEST(test_shadowTable_channelSpacingWithPadding);
|
|
RUN_TEST(test_shadowTable_turboOnlyOnWideLora);
|
|
RUN_TEST(test_shadowTable_unknownCodeFallsToSentinel);
|
|
RUN_TEST(test_shadowTable_presetHashProfileHasCorrectOverrideSlot);
|
|
|
|
// validateConfigLora()
|
|
RUN_TEST(test_validateConfigLora_validPresetForUS);
|
|
RUN_TEST(test_validateConfigLora_allStdPresetsValidForUS);
|
|
RUN_TEST(test_validateConfigLora_turboPresetsInvalidForEU868);
|
|
RUN_TEST(test_validateConfigLora_validPresetsForEU868);
|
|
RUN_TEST(test_validateConfigLora_customBandwidthTooWideForEU868);
|
|
RUN_TEST(test_validateConfigLora_customBandwidthFitsUS);
|
|
RUN_TEST(test_validateConfigLora_customBandwidthFitsEU868);
|
|
RUN_TEST(test_validateConfigLora_bogusPresetRejected);
|
|
RUN_TEST(test_validateConfigLora_unsetRegionOnlyAcceptsLongFast);
|
|
RUN_TEST(test_validateConfigLora_allPresetsValidForLORA24);
|
|
|
|
// clampConfigLora()
|
|
RUN_TEST(test_clampConfigLora_invalidPresetClampedToDefault);
|
|
RUN_TEST(test_clampConfigLora_validPresetUnchanged);
|
|
RUN_TEST(test_clampConfigLora_customBwTooWideClampedToDefaultBw);
|
|
RUN_TEST(test_clampConfigLora_customBwValidLeftUnchanged);
|
|
RUN_TEST(test_clampConfigLora_bogusPresetOnUnsetClampedToLongFast);
|
|
RUN_TEST(test_clampConfigLora_invalidPresetOnLORA24ClampedToDefault);
|
|
|
|
// Region-locked preset swap
|
|
RUN_TEST(test_clampConfigLora_narrowPresetOnEU866SwapsToEUN868);
|
|
RUN_TEST(test_clampConfigLora_litePresetOnEU868SwapsToEU866);
|
|
RUN_TEST(test_clampConfigLora_eu868PresetOnEUN868SwapsToEU868);
|
|
RUN_TEST(test_clampConfigLora_litePresetOnUSDoesNotSwap);
|
|
RUN_TEST(test_clampConfigLora_narrowPresetOnHam125cmDoesNotSwap);
|
|
RUN_TEST(test_validateConfigLora_siblingLockedPresetStillFailsValidation);
|
|
|
|
// RegionInfo preset list integrity
|
|
RUN_TEST(test_presetsStd_hasTenEntries);
|
|
RUN_TEST(test_presetsEU868_hasSevenEntries);
|
|
RUN_TEST(test_presetsUndef_hasOneEntry);
|
|
RUN_TEST(test_defaultPresetIsInAvailablePresets);
|
|
RUN_TEST(test_regionFieldsAreSane);
|
|
RUN_TEST(test_onlyLORA24HasWideLora);
|
|
|
|
// OVERRIDE_SLOT_PRESET_HASH (-1) slot formula tests
|
|
RUN_TEST(test_overrideSlotPresetHash_longFast_customChannelMatchesDefaultNameSlot);
|
|
RUN_TEST(test_overrideSlotPresetHash_mediumFast_customChannelMatchesDefaultNameSlot);
|
|
RUN_TEST(test_overrideSlotPresetHash_longFast_slotIsStableAcrossCustomNames);
|
|
RUN_TEST(test_overrideSlotPresetHash_mediumFast_slotIsStableAcrossCustomNames);
|
|
RUN_TEST(test_overrideSlotPresetHash_longFastAndMediumFast_slotsAreDifferentPresets);
|
|
|
|
// Channel spacing (current + placeholder)
|
|
RUN_TEST(test_channelSpacingCalculation_US_LONG_FAST);
|
|
RUN_TEST(test_channelSpacingCalculation_EU868_LONG_FAST);
|
|
RUN_TEST(test_channelSpacingCalculation_placeholder);
|
|
|
|
// handleSetConfig fromOthers dispatch
|
|
RUN_TEST(test_handleSetConfig_fromOthers_invalidPresetRejected);
|
|
RUN_TEST(test_handleSetConfig_fromLocal_invalidPresetClamped);
|
|
RUN_TEST(test_handleSetConfig_fromOthers_validPresetAccepted);
|
|
RUN_TEST(test_handleSetConfig_fromOthers_invalidChannelNumFullyRejected);
|
|
RUN_TEST(test_clampBandwidthCode_zeroMapsToDefaultOthersUnchanged);
|
|
RUN_TEST(test_handleSetConfig_fromLocal_customBandwidthZeroClampedToDefault);
|
|
RUN_TEST(test_handleSetConfig_fromOthers_customBandwidthZeroClampedToDefault);
|
|
RUN_TEST(test_handleSetConfig_fromLocal_presetBandwidthZeroLeftUntouched);
|
|
RUN_TEST(test_handleSetConfig_fromLocal_customBandwidthNonZeroPreserved);
|
|
RUN_TEST(test_handleSetConfig_security_preservesKeypairWhenPrivateOmitted);
|
|
RUN_TEST(test_handleSetConfig_security_acceptsSuppliedKeypair);
|
|
RUN_TEST(test_handleSetConfig_security_rotationPreservesAdminKeys);
|
|
RUN_TEST(test_handleSetConfig_security_clearsAdminKeysWhenKeypairUnchanged);
|
|
RUN_TEST(test_regionInfo_supportsPreset);
|
|
RUN_TEST(test_checkConfigRegion_quietCheckReportsReason);
|
|
RUN_TEST(test_checkConfigRegion_allowsProspectiveLicensedOwner);
|
|
RUN_TEST(test_handleSetConfig_fromOthers_siblingLockedPresetSwapsRegion);
|
|
RUN_TEST(test_handleSetConfig_fromOthers_lockedPresetFromNonTrioRegionRejected);
|
|
|
|
// Channel-configuration warning + coalescing
|
|
RUN_TEST(test_warn_singleChannel_variantName_oneSpecificMessage);
|
|
RUN_TEST(test_warn_singleChannel_nameAndPsk_collapsedToCatchAll);
|
|
RUN_TEST(test_warn_cleanChannel_noMessage);
|
|
RUN_TEST(test_warn_transaction_multipleChannels_singleCoalescedMessage);
|
|
RUN_TEST(test_warn_transaction_singleChannel_keepsSpecificMessage);
|
|
RUN_TEST(test_warn_license_noTransaction_emittedImmediately);
|
|
RUN_TEST(test_warn_license_transaction_coalescedToSingleMessage);
|
|
|
|
exit(UNITY_END());
|
|
}
|
|
|
|
void loop() {}
|