Files
roro9stack/test/test_settings/test_settings.cpp
twislaandClaude Opus 5.5 305818466f
CI / build (pull_request) Successful in 2m19s
Site / build (pull_request) Successful in 9s
LoRa Scanner: MeshCore by default, and its public channel read
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
2026-10-09 22:19:14 +02:00

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();
}