Public Access
S1 #7 steps 1-2: IPv4 checks, the IP setting per Saved Network, DNS and NTP settings
The plan and decisions Q105 to Q116 (docs/milestones/S1.md). lib/net: strict IPv4 parsing, prefix and mask, and the checks a Fixed setting must pass, each refusal with its reason. A Saved Network is Automatic or Fixed (address/prefix and an optional gateway), kept with it in flash. Settings: two DNS servers (9.9.9.9, 1.1.1.1), "Always use my DNS", two NTP servers (pool.ntp.org, time.cloudflare.com). Host-tested: 383 tests. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EhqxQ49eCju4CzKYNjZzwT
This commit is contained in:
@@ -0,0 +1,101 @@
|
||||
#include <unity.h>
|
||||
|
||||
#include <string>
|
||||
|
||||
#include "ipv4.h"
|
||||
|
||||
using namespace roro::net;
|
||||
|
||||
void setUp() {}
|
||||
void tearDown() {}
|
||||
|
||||
static uint32_t ip(const char* s) {
|
||||
uint32_t a = 0;
|
||||
TEST_ASSERT_TRUE_MESSAGE(parseIpv4(s, a), s);
|
||||
return a;
|
||||
}
|
||||
|
||||
void test_parse_and_format() {
|
||||
TEST_ASSERT_EQUAL_HEX32(0x0A27270C, ip("10.39.39.12"));
|
||||
TEST_ASSERT_EQUAL_HEX32(0x00000000, ip("0.0.0.0"));
|
||||
TEST_ASSERT_EQUAL_HEX32(0xFFFFFFFF, ip("255.255.255.255"));
|
||||
TEST_ASSERT_EQUAL_STRING("10.39.39.12", formatIpv4(0x0A27270C).c_str());
|
||||
TEST_ASSERT_EQUAL_STRING("192.168.1.254", formatIpv4(ip("192.168.1.254")).c_str());
|
||||
}
|
||||
|
||||
void test_parse_refuses() {
|
||||
uint32_t a;
|
||||
for (const char* bad : {"", "10.39.39", "10.39.39.12.1", "10.39.39.256", "10..39.12", "10.39.39.", ".10.39.39",
|
||||
"10.39.39.1a", " 10.39.39.12", "10.39.39.12 ", "1.2.3.-4", "01234.1.1.1", "1.2.3.4/24"})
|
||||
TEST_ASSERT_FALSE_MESSAGE(parseIpv4(bad, a), bad);
|
||||
}
|
||||
|
||||
void test_prefix_and_mask() {
|
||||
TEST_ASSERT_EQUAL_HEX32(0xFFFFFF00, maskOf(24));
|
||||
TEST_ASSERT_EQUAL_HEX32(0xFFFFFFFC, maskOf(30));
|
||||
TEST_ASSERT_EQUAL_HEX32(0x80000000, maskOf(1));
|
||||
TEST_ASSERT_EQUAL_HEX32(0xFFFF0000, maskOf(16));
|
||||
TEST_ASSERT_EQUAL_STRING("255.255.255.0", formatIpv4(maskOf(24)).c_str());
|
||||
}
|
||||
|
||||
// Q111: what a Fixed setting must satisfy, and the reason given when it doesn't.
|
||||
void test_fixed_setting_accepted() {
|
||||
TEST_ASSERT_EQUAL_STRING("", checkFixed({ip("10.39.39.13"), 24, ip("10.39.39.1")}).c_str());
|
||||
TEST_ASSERT_EQUAL_STRING("", checkFixed({ip("192.168.4.2"), 30, ip("192.168.4.1")}).c_str());
|
||||
TEST_ASSERT_EQUAL_STRING("", checkFixed({ip("10.0.0.5"), 8, 0}).c_str()); // Q107: no gateway is fine
|
||||
}
|
||||
|
||||
void test_fixed_setting_refused() {
|
||||
TEST_ASSERT_EQUAL_STRING("The prefix must be 1 to 30", checkFixed({ip("10.39.39.13"), 0, 0}).c_str());
|
||||
TEST_ASSERT_EQUAL_STRING("The prefix must be 1 to 30", checkFixed({ip("10.39.39.13"), 31, 0}).c_str());
|
||||
TEST_ASSERT_EQUAL_STRING("10.39.39.0 is the network's own address", checkFixed({ip("10.39.39.0"), 24, 0}).c_str());
|
||||
TEST_ASSERT_EQUAL_STRING("10.39.39.255 is the broadcast address", checkFixed({ip("10.39.39.255"), 24, 0}).c_str());
|
||||
TEST_ASSERT_EQUAL_STRING("The gateway 10.39.40.1 isn't in 10.39.39.0/24",
|
||||
checkFixed({ip("10.39.39.13"), 24, ip("10.39.40.1")}).c_str());
|
||||
TEST_ASSERT_EQUAL_STRING("The gateway can't be this device's address",
|
||||
checkFixed({ip("10.39.39.13"), 24, ip("10.39.39.13")}).c_str());
|
||||
TEST_ASSERT_EQUAL_STRING("The gateway 10.39.39.255 is the broadcast address",
|
||||
checkFixed({ip("10.39.39.13"), 24, ip("10.39.39.255")}).c_str());
|
||||
TEST_ASSERT_EQUAL_STRING("0.0.0.0 isn't an address a device can have", checkFixed({0, 24, 0}).c_str());
|
||||
}
|
||||
|
||||
// How a Fixed setting is typed on the console and kept in flash: "address/prefix [gateway]".
|
||||
void test_fixed_setting_as_text() {
|
||||
FixedIp f;
|
||||
TEST_ASSERT_EQUAL_STRING("", parseFixed("10.39.39.13/24 10.39.39.1", f).c_str());
|
||||
TEST_ASSERT_EQUAL_HEX32(ip("10.39.39.13"), f.address);
|
||||
TEST_ASSERT_EQUAL_UINT8(24, f.prefix);
|
||||
TEST_ASSERT_EQUAL_HEX32(ip("10.39.39.1"), f.gateway);
|
||||
TEST_ASSERT_EQUAL_STRING("10.39.39.13/24 10.39.39.1", formatFixed(f).c_str());
|
||||
|
||||
TEST_ASSERT_EQUAL_STRING("", parseFixed("192.168.4.2/30", f).c_str());
|
||||
TEST_ASSERT_EQUAL_HEX32(0, f.gateway);
|
||||
TEST_ASSERT_EQUAL_STRING("192.168.4.2/30", formatFixed(f).c_str());
|
||||
|
||||
TEST_ASSERT_EQUAL_STRING("Write it as address/prefix, then the gateway if there is one", parseFixed("10.39.39.13", f).c_str());
|
||||
TEST_ASSERT_EQUAL_STRING("10.39.39.300 isn't an IPv4 address", parseFixed("10.39.39.300/24", f).c_str());
|
||||
TEST_ASSERT_EQUAL_STRING("The prefix must be 1 to 30", parseFixed("10.39.39.13/abc", f).c_str());
|
||||
TEST_ASSERT_EQUAL_STRING("gw isn't an IPv4 address", parseFixed("10.39.39.13/24 gw", f).c_str());
|
||||
TEST_ASSERT_EQUAL_STRING("The gateway 10.39.40.1 isn't in 10.39.39.0/24", parseFixed("10.39.39.13/24 10.39.40.1", f).c_str());
|
||||
}
|
||||
|
||||
// An NTP server is an address or a host name (Q110).
|
||||
void test_host_names() {
|
||||
for (const char* good : {"pool.ntp.org", "time.cloudflare.com", "10.39.39.1", "ntp", "a-b.example", "0.be.pool.ntp.org"})
|
||||
TEST_ASSERT_TRUE_MESSAGE(validHost(good), good);
|
||||
for (const char* bad : {"", "pool .ntp.org", "-ntp.org", "ntp-.org", "ntp..org", ".ntp.org", "ntp.org.", "n_tp.org", "häst.se",
|
||||
"999.1.1.1", "1.2.3"})
|
||||
TEST_ASSERT_FALSE_MESSAGE(validHost(bad), bad);
|
||||
}
|
||||
|
||||
int main() {
|
||||
UNITY_BEGIN();
|
||||
RUN_TEST(test_parse_and_format);
|
||||
RUN_TEST(test_parse_refuses);
|
||||
RUN_TEST(test_prefix_and_mask);
|
||||
RUN_TEST(test_fixed_setting_accepted);
|
||||
RUN_TEST(test_fixed_setting_refused);
|
||||
RUN_TEST(test_fixed_setting_as_text);
|
||||
RUN_TEST(test_host_names);
|
||||
return UNITY_END();
|
||||
}
|
||||
@@ -97,6 +97,76 @@ void test_find() {
|
||||
TEST_ASSERT_NULL(n.find("other"));
|
||||
}
|
||||
|
||||
// S1, Q105: each Saved Network is Automatic (DHCP) or Fixed, and remembers which.
|
||||
void test_new_network_is_automatic() {
|
||||
MemoryStore store;
|
||||
SavedNetworks nets(store);
|
||||
nets.add("home", "password1");
|
||||
TEST_ASSERT_FALSE(nets.find("home")->fixed);
|
||||
}
|
||||
|
||||
void test_ip_setting_survives_reload() {
|
||||
MemoryStore store;
|
||||
SavedNetworks nets(store);
|
||||
nets.add("home", "password1");
|
||||
nets.add("bench", "");
|
||||
roro::net::FixedIp f;
|
||||
TEST_ASSERT_EQUAL_STRING("", roro::net::parseFixed("10.39.39.13/24 10.39.39.1", f).c_str());
|
||||
TEST_ASSERT_EQUAL_STRING("", nets.setIp("bench", &f).c_str());
|
||||
|
||||
SavedNetworks again(store);
|
||||
again.load();
|
||||
TEST_ASSERT_FALSE(again.find("home")->fixed);
|
||||
TEST_ASSERT_TRUE(again.find("bench")->fixed);
|
||||
TEST_ASSERT_EQUAL_STRING("10.39.39.13/24 10.39.39.1", roro::net::formatFixed(again.find("bench")->ip).c_str());
|
||||
|
||||
TEST_ASSERT_EQUAL_STRING("", again.setIp("bench", nullptr).c_str()); // back to Automatic
|
||||
SavedNetworks third(store);
|
||||
third.load();
|
||||
TEST_ASSERT_FALSE(third.find("bench")->fixed);
|
||||
}
|
||||
|
||||
void test_ip_setting_is_checked() {
|
||||
MemoryStore store;
|
||||
SavedNetworks nets(store);
|
||||
nets.add("bench", "");
|
||||
roro::net::FixedIp bad{0x0A272700, 24, 0}; // 10.39.39.0: the network's own address
|
||||
TEST_ASSERT_EQUAL_STRING("10.39.39.0 is the network's own address", nets.setIp("bench", &bad).c_str());
|
||||
TEST_ASSERT_FALSE(nets.find("bench")->fixed);
|
||||
roro::net::FixedIp ok{0x0A27270D, 24, 0};
|
||||
TEST_ASSERT_EQUAL_STRING("Not a saved network", nets.setIp("nowhere", &ok).c_str());
|
||||
}
|
||||
|
||||
// Forgetting a network moves the ones after it: each keeps its own setting.
|
||||
void test_ip_setting_follows_its_network() {
|
||||
MemoryStore store;
|
||||
SavedNetworks nets(store);
|
||||
nets.add("a", "");
|
||||
nets.add("b", "");
|
||||
nets.add("c", "");
|
||||
roro::net::FixedIp f{0x0A27270D, 24, 0};
|
||||
nets.setIp("c", &f);
|
||||
nets.forget("a");
|
||||
SavedNetworks again(store);
|
||||
again.load();
|
||||
TEST_ASSERT_FALSE(again.find("b")->fixed);
|
||||
TEST_ASSERT_TRUE(again.find("c")->fixed);
|
||||
// And changing the password keeps it.
|
||||
again.add("c", "password2");
|
||||
TEST_ASSERT_TRUE(again.find("c")->fixed);
|
||||
}
|
||||
|
||||
// A stored setting that no longer passes the checks falls back to Automatic.
|
||||
void test_bad_stored_ip_setting_is_ignored() {
|
||||
MemoryStore store;
|
||||
SavedNetworks nets(store);
|
||||
nets.add("bench", "");
|
||||
store.strings["net0_ip"] = "10.39.39.13/99";
|
||||
SavedNetworks again(store);
|
||||
again.load();
|
||||
TEST_ASSERT_FALSE(again.find("bench")->fixed);
|
||||
}
|
||||
|
||||
int main() {
|
||||
UNITY_BEGIN();
|
||||
RUN_TEST(test_empty_store_has_no_networks);
|
||||
@@ -107,5 +177,10 @@ int main() {
|
||||
RUN_TEST(test_validation_follows_wifi_limits);
|
||||
RUN_TEST(test_hidden_flag_survives_reload);
|
||||
RUN_TEST(test_find);
|
||||
RUN_TEST(test_new_network_is_automatic);
|
||||
RUN_TEST(test_ip_setting_survives_reload);
|
||||
RUN_TEST(test_ip_setting_is_checked);
|
||||
RUN_TEST(test_ip_setting_follows_its_network);
|
||||
RUN_TEST(test_bad_stored_ip_setting_is_ignored);
|
||||
return UNITY_END();
|
||||
}
|
||||
|
||||
@@ -154,6 +154,37 @@ void test_storage_keys_fit_nvs_limit() {
|
||||
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, ""));
|
||||
}
|
||||
|
||||
int main() {
|
||||
UNITY_BEGIN();
|
||||
RUN_TEST(test_defaults_when_store_is_empty);
|
||||
@@ -168,5 +199,7 @@ int main() {
|
||||
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);
|
||||
return UNITY_END();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user