Public Access
The MeshCore preset is the default for a new device, in the Settings and in the radio before the Scanner is opened. The setting now accepts it: it only took the 7 Meshtastic presets, so MeshCore picked in the App was not saved. Messages on MeshCore's public channel are decrypted with the channel's published key (AES-128, checked with 2 bytes of HMAC-SHA256): the row shows who says they sent it and the start of the text, the details the sender, the time and the whole text. Other channels and private messages stay encrypted. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0162FokPdvY2KsS4NBfwyWPk
243 lines
8.8 KiB
C++
243 lines
8.8 KiB
C++
#include <unity.h>
|
|
|
|
#include <cstring>
|
|
#include <map>
|
|
#include <string>
|
|
|
|
#include "../memory_store.h"
|
|
#include "event_bus.h"
|
|
#include "settings.h"
|
|
|
|
using namespace roro;
|
|
|
|
|
|
void setUp() {}
|
|
void tearDown() {}
|
|
|
|
void test_defaults_when_store_is_empty() {
|
|
MemoryStore store;
|
|
EventBus bus;
|
|
Settings s(store, bus);
|
|
s.load();
|
|
TEST_ASSERT_FALSE(s.getBool(Setting::SetupDone));
|
|
TEST_ASSERT_EQUAL(60, s.getInt(Setting::Brightness));
|
|
TEST_ASSERT_EQUAL(30, s.getInt(Setting::DimTimeoutS));
|
|
TEST_ASSERT_EQUAL(60, s.getInt(Setting::OffTimeoutS));
|
|
TEST_ASSERT_TRUE(s.getBool(Setting::Sound));
|
|
TEST_ASSERT_TRUE(s.getBool(Setting::ProbeMacRaw));
|
|
TEST_ASSERT_TRUE(s.getBool(Setting::WifiEnabled));
|
|
TEST_ASSERT_TRUE(s.getBool(Setting::GnssEnabled)); // Q58: on by default
|
|
TEST_ASSERT_FALSE(s.getBool(Setting::CoordinatesDms)); // Q64: decimal degrees
|
|
TEST_ASSERT_EQUAL_INT32(7, s.getInt(Setting::LoraPreset)); // MeshCore, after the seven EU868 presets
|
|
TEST_ASSERT_FALSE(s.setInt(Setting::LoraPreset, 8));
|
|
TEST_ASSERT_EQUAL_STRING("EU868", s.getString(Setting::Region).c_str());
|
|
TEST_ASSERT_EQUAL_STRING("CET-1CEST,M3.5.0,M10.5.0/3", s.getString(Setting::Timezone).c_str());
|
|
}
|
|
|
|
void test_set_persists_and_survives_reload() {
|
|
MemoryStore store;
|
|
EventBus bus;
|
|
{
|
|
Settings s(store, bus);
|
|
s.load();
|
|
TEST_ASSERT_TRUE(s.setInt(Setting::Brightness, 80));
|
|
TEST_ASSERT_TRUE(s.setString(Setting::LongName, "Clément"));
|
|
}
|
|
Settings again(store, bus);
|
|
again.load();
|
|
TEST_ASSERT_EQUAL(80, again.getInt(Setting::Brightness));
|
|
TEST_ASSERT_EQUAL_STRING("Clément", again.getString(Setting::LongName).c_str());
|
|
}
|
|
|
|
void test_out_of_range_values_are_rejected_and_not_stored() {
|
|
MemoryStore store;
|
|
EventBus bus;
|
|
Settings s(store, bus);
|
|
s.load();
|
|
TEST_ASSERT_FALSE(s.setInt(Setting::Brightness, 5)); // would make the screen unreadable
|
|
TEST_ASSERT_FALSE(s.setInt(Setting::Brightness, 101));
|
|
TEST_ASSERT_EQUAL(60, s.getInt(Setting::Brightness));
|
|
TEST_ASSERT_EQUAL(0, store.ints.count(Settings::key(Setting::Brightness)));
|
|
}
|
|
|
|
void test_corrupt_stored_value_falls_back_to_default() {
|
|
MemoryStore store;
|
|
EventBus bus;
|
|
store.ints[Settings::key(Setting::Brightness)] = 9999;
|
|
Settings s(store, bus);
|
|
s.load();
|
|
TEST_ASSERT_EQUAL(60, s.getInt(Setting::Brightness));
|
|
}
|
|
|
|
void test_dim_timeout_must_stay_below_off_timeout() {
|
|
MemoryStore store;
|
|
EventBus bus;
|
|
Settings s(store, bus);
|
|
s.load();
|
|
TEST_ASSERT_FALSE(s.setInt(Setting::DimTimeoutS, 60)); // off is 60
|
|
TEST_ASSERT_FALSE(s.setInt(Setting::OffTimeoutS, 30)); // dim is 30
|
|
TEST_ASSERT_TRUE(s.setInt(Setting::OffTimeoutS, 120));
|
|
TEST_ASSERT_TRUE(s.setInt(Setting::DimTimeoutS, 90));
|
|
}
|
|
|
|
void test_short_name_is_one_to_four_bytes() {
|
|
MemoryStore store;
|
|
EventBus bus;
|
|
Settings s(store, bus);
|
|
s.load();
|
|
TEST_ASSERT_FALSE(s.setString(Setting::ShortName, ""));
|
|
TEST_ASSERT_FALSE(s.setString(Setting::ShortName, "ROROX"));
|
|
TEST_ASSERT_TRUE(s.setString(Setting::ShortName, "RORO"));
|
|
}
|
|
|
|
void test_long_name_is_limited_to_39_bytes() {
|
|
MemoryStore store;
|
|
EventBus bus;
|
|
Settings s(store, bus);
|
|
s.load();
|
|
TEST_ASSERT_TRUE(s.setString(Setting::LongName, std::string(39, 'a')));
|
|
TEST_ASSERT_FALSE(s.setString(Setting::LongName, std::string(40, 'a')));
|
|
TEST_ASSERT_FALSE(s.setString(Setting::LongName, ""));
|
|
}
|
|
|
|
void test_only_known_regions_are_accepted() {
|
|
MemoryStore store;
|
|
EventBus bus;
|
|
Settings s(store, bus);
|
|
s.load();
|
|
TEST_ASSERT_FALSE(s.setString(Setting::Region, "US915"));
|
|
TEST_ASSERT_TRUE(s.setString(Setting::Region, "EU868"));
|
|
}
|
|
|
|
void test_type_mismatch_is_rejected() {
|
|
MemoryStore store;
|
|
EventBus bus;
|
|
Settings s(store, bus);
|
|
s.load();
|
|
TEST_ASSERT_FALSE(s.setString(Setting::Brightness, "80"));
|
|
TEST_ASSERT_FALSE(s.setInt(Setting::LongName, 3));
|
|
}
|
|
|
|
void test_change_publishes_setting_changed_once() {
|
|
MemoryStore store;
|
|
EventBus bus;
|
|
Settings s(store, bus);
|
|
s.load();
|
|
int changes = 0;
|
|
int32_t which = -1;
|
|
bus.subscribe(EventType::SettingChanged, [&](const Event& e) {
|
|
changes++;
|
|
which = e.a;
|
|
});
|
|
|
|
s.setInt(Setting::Brightness, 70);
|
|
s.setInt(Setting::Brightness, 70); // same value: no event
|
|
s.setInt(Setting::Brightness, 1); // invalid: no event
|
|
bus.dispatch();
|
|
|
|
TEST_ASSERT_EQUAL(1, changes);
|
|
TEST_ASSERT_EQUAL(static_cast<int32_t>(Setting::Brightness), which);
|
|
}
|
|
|
|
void test_transmit_requires_confirmed_region() {
|
|
MemoryStore store;
|
|
EventBus bus;
|
|
Settings s(store, bus);
|
|
s.load();
|
|
TEST_ASSERT_FALSE(s.canTransmit());
|
|
s.setBool(Setting::RegionConfirmed, true);
|
|
TEST_ASSERT_TRUE(s.canTransmit());
|
|
}
|
|
|
|
void test_storage_keys_fit_nvs_limit() {
|
|
for (int i = 0; i < static_cast<int>(Setting::Count); i++)
|
|
TEST_ASSERT_TRUE(std::strlen(Settings::key(static_cast<Setting>(i))) <= 15);
|
|
}
|
|
|
|
// S1, Q108 to Q110: DNS and NTP servers, with public defaults.
|
|
void test_dns_and_ntp_defaults() {
|
|
MemoryStore store;
|
|
EventBus bus;
|
|
Settings s(store, bus);
|
|
s.load();
|
|
TEST_ASSERT_EQUAL_STRING("9.9.9.9", s.getString(Setting::Dns1).c_str());
|
|
TEST_ASSERT_EQUAL_STRING("1.1.1.1", s.getString(Setting::Dns2).c_str());
|
|
TEST_ASSERT_FALSE(s.getBool(Setting::DnsAlways));
|
|
TEST_ASSERT_EQUAL_STRING("pool.ntp.org", s.getString(Setting::Ntp1).c_str());
|
|
TEST_ASSERT_EQUAL_STRING("time.cloudflare.com", s.getString(Setting::Ntp2).c_str());
|
|
}
|
|
|
|
void test_dns_and_ntp_are_checked() {
|
|
MemoryStore store;
|
|
EventBus bus;
|
|
Settings s(store, bus);
|
|
s.load();
|
|
TEST_ASSERT_TRUE(s.setString(Setting::Dns1, "10.39.39.1"));
|
|
TEST_ASSERT_FALSE(s.setString(Setting::Dns1, "dns.example")); // an address, not a name
|
|
TEST_ASSERT_FALSE(s.setString(Setting::Dns1, "")); // the first one is required
|
|
TEST_ASSERT_EQUAL_STRING("10.39.39.1", s.getString(Setting::Dns1).c_str());
|
|
TEST_ASSERT_TRUE(s.setString(Setting::Dns2, "")); // the second is optional
|
|
TEST_ASSERT_FALSE(s.setString(Setting::Dns2, "1.1.1"));
|
|
TEST_ASSERT_TRUE(s.setString(Setting::Ntp1, "10.39.39.1"));
|
|
TEST_ASSERT_TRUE(s.setString(Setting::Ntp1, "0.be.pool.ntp.org"));
|
|
TEST_ASSERT_FALSE(s.setString(Setting::Ntp1, "time server"));
|
|
TEST_ASSERT_FALSE(s.setString(Setting::Ntp1, ""));
|
|
TEST_ASSERT_TRUE(s.setString(Setting::Ntp2, ""));
|
|
}
|
|
|
|
// ADR 0010: off, and no token, until the owner switches the console on. A stored value that isn't
|
|
// valid counts as missing, so a damaged store can't switch it on or leave a weak token.
|
|
void test_the_debug_console_is_off_and_has_no_token_by_default() {
|
|
MemoryStore store;
|
|
EventBus bus;
|
|
Settings s(store, bus);
|
|
s.load();
|
|
TEST_ASSERT_FALSE(s.getBool(Setting::DebugConsole));
|
|
TEST_ASSERT_EQUAL_STRING("", s.getString(Setting::DebugToken).c_str());
|
|
|
|
MemoryStore damaged;
|
|
damaged.ints["debug_on"] = 7; // not a bool
|
|
damaged.strings["debug_token"] = "1234"; // too short
|
|
Settings d(damaged, bus);
|
|
d.load();
|
|
TEST_ASSERT_FALSE(d.getBool(Setting::DebugConsole));
|
|
TEST_ASSERT_EQUAL_STRING("", d.getString(Setting::DebugToken).c_str());
|
|
}
|
|
|
|
void test_a_debug_token_must_be_valid_or_empty() {
|
|
MemoryStore store;
|
|
EventBus bus;
|
|
Settings s(store, bus);
|
|
s.load();
|
|
TEST_ASSERT_FALSE(s.setString(Setting::DebugToken, "1234"));
|
|
TEST_ASSERT_FALSE(s.setString(Setting::DebugToken, "k7qf-3m2x-9wbd-ht4p-6rnc")); // not tidied
|
|
TEST_ASSERT_TRUE(s.setString(Setting::DebugToken, "K7QF3M2X9WBDHT4P6RNC"));
|
|
TEST_ASSERT_TRUE(s.setBool(Setting::DebugConsole, true));
|
|
Settings again(store, bus);
|
|
again.load();
|
|
TEST_ASSERT_TRUE(again.getBool(Setting::DebugConsole));
|
|
TEST_ASSERT_EQUAL_STRING("K7QF3M2X9WBDHT4P6RNC", again.getString(Setting::DebugToken).c_str());
|
|
TEST_ASSERT_TRUE(again.setString(Setting::DebugToken, "")); // forgotten
|
|
}
|
|
|
|
int main() {
|
|
UNITY_BEGIN();
|
|
RUN_TEST(test_defaults_when_store_is_empty);
|
|
RUN_TEST(test_set_persists_and_survives_reload);
|
|
RUN_TEST(test_out_of_range_values_are_rejected_and_not_stored);
|
|
RUN_TEST(test_corrupt_stored_value_falls_back_to_default);
|
|
RUN_TEST(test_dim_timeout_must_stay_below_off_timeout);
|
|
RUN_TEST(test_short_name_is_one_to_four_bytes);
|
|
RUN_TEST(test_long_name_is_limited_to_39_bytes);
|
|
RUN_TEST(test_only_known_regions_are_accepted);
|
|
RUN_TEST(test_type_mismatch_is_rejected);
|
|
RUN_TEST(test_change_publishes_setting_changed_once);
|
|
RUN_TEST(test_transmit_requires_confirmed_region);
|
|
RUN_TEST(test_storage_keys_fit_nvs_limit);
|
|
RUN_TEST(test_dns_and_ntp_defaults);
|
|
RUN_TEST(test_dns_and_ntp_are_checked);
|
|
RUN_TEST(test_the_debug_console_is_off_and_has_no_token_by_default);
|
|
RUN_TEST(test_a_debug_token_must_be_valid_or_empty);
|
|
return UNITY_END();
|
|
}
|