Add typed serial settings operations

Route bounded admin mutations through the existing administration
dispatcher,
covering apply, lifecycle, persistence, authorization, and result
tracking.
Add the browser controls, automatic result refresh, regression coverage,
and
phase documentation.
This commit is contained in:
2026-09-08 00:25:31 +02:00
parent 5a2aa0d4d8
commit 42548f6334
27 changed files with 1398 additions and 42 deletions
+26
View File
@@ -44,6 +44,7 @@ esp_err_t httpd_ws_respond_server_handshake(httpd_req_t *, const char *);
admin = "--admin" in sys.argv
settings = "--settings" in sys.argv
serial_settings = "--serial-settings" in sys.argv
if admin:
HEADERS["esp_system.h"] = "#pragma once\nvoid esp_restart(void);\n"
HEADERS["esp_heap_caps.h"] = """#pragma once
@@ -113,7 +114,31 @@ with tempfile.TemporaryDirectory(prefix="web-cookie-auth-") as directory:
settings_source += function(config_source[config_source.index('const char *serial_config_' + name + '_to_string'):], 'serial_config_' + name + '_to_string') + '\n'
for name in ('set_common_headers', 'send_plain_error', 'authorize_or_respond', 'safe_string', 'serial_settings_handler'):
settings_source += function(server_source, name) + '\n'
# Exercise the serial projection of /api/status without doubling every
# unrelated subsystem. Copy its acquisition, format and arguments verbatim.
status = function(server_source, 'status_handler')
assert 'serial_service_get_config(' not in status, 'status must not use the blocking config getter'
assert 'serial_service_is_running(' not in status, 'status must use snapshot running state'
assert status.count('serial_service_get_snapshot(') == 1
acquisition = status[status.index(' bool serial_config_available ='):
status.index(' serial_service_get_counters(')]
serial_format = status[status.index(' " \\"serial\\":'):
status.index(' " \\"broker\\":')]
argument_start = status.index(' wifi_available ? (unsigned int)wifi.ap_client_count : 0U,')
argument_start = status.index('\n', argument_start) + 1
serial_arguments = status[argument_start:status.index(' broker_available ? "true"')].rstrip().removesuffix(',')
settings_source += '\nstatic int status_serial_projection(char *response, size_t capacity) {\n'
settings_source += ' serial_service_snapshot_t serial_snapshot = {0};\n'
settings_source += ' serial_service_counters_t serial_counters = {0};\n' + acquisition
settings_source += ' return snprintf(response, capacity,\n' + serial_format + ',\n' + serial_arguments + ');\n}\n'
(tmp / 'settings_production.h').write_text(settings_source)
if serial_settings:
config_source = (ROOT / 'src/serial_config.c').read_text()
names = ['serial_config_defaults', 'serial_config_validate']
names += ['serial_config_parse_' + name for name in ('data_bits', 'parity', 'stop_bits', 'flow_control', 'dtr_behavior')]
(tmp / 'serial_config_production.h').write_text('\n'.join(function(config_source, name) for name in names))
console_source = (ROOT / 'src/serial_console.c').read_text()
(tmp / 'serial_console_production.h').write_text('\n'.join(function(console_source, name) for name in ('parse_unsigned', 'set_parameter', 'command_serial')))
sources = [HERE / "test.c", tmp / "installed_httpd.c"]
sources += [ROOT / "src" / name for name in ["web_session_store.c", "web_auth_parse.c", "web_cookie_auth.c", "web_httpd_adapter.c"]]
if admin:
@@ -129,6 +154,7 @@ with tempfile.TemporaryDirectory(prefix="web-cookie-auth-") as directory:
subprocess.run(["cc", "-std=c11", "-Wall", "-Wextra", "-Werror", "-g", "-DHOST_OPENSSL",
*(["-DHOST_ADMIN"] if admin else []),
*(["-DHOST_SETTINGS"] if settings else []),
*(["-DHOST_SERIAL_SETTINGS"] if serial_settings else []),
"-I" + str(tmp), "-I" + str(ROOT / "src"), *map(str, sources), "-lcrypto",
"-o", str(tmp / "test")], check=True, timeout=30)
subprocess.run([str(tmp / "test")], check=True, timeout=20)
@@ -0,0 +1,264 @@
/* Real HTTP policy/store/parser/operation owner; deterministic serial/NVS/queue. */
#include "serial_service.h"
#include <errno.h>
#include <stdlib.h>
#define ESP_ERR_TIMEOUT 0x107
#include "serial_config_production.h"
#include "../../src/web_serial_settings.c"
static serial_config_t working, persisted;
static bool on_dispatcher, have_stored = true, queue_fail;
static unsigned api_calls, apply_calls, save_calls, load_calls, start_calls, stop_calls;
static unsigned apply_fail_at;
static esp_err_t get_error, save_error, load_error, start_error, stop_error;
static uint32_t queued_id;
static void (*apply_hook)(void);
esp_err_t admin_ssh_console_submit_serial_settings(uint32_t id) {
assert(!host_lock_depth && !on_dispatcher && id);
if (queue_fail) return ESP_ERR_TIMEOUT;
queued_id = id; return ESP_OK;
}
static void serial_api(void) { assert(on_dispatcher && !host_lock_depth); ++api_calls; }
esp_err_t serial_service_apply_config(const serial_config_t *config) {
serial_api(); ++apply_calls;
assert(serial_config_validate(config) == ESP_OK);
if (apply_hook) { void (*hook)(void) = apply_hook; apply_hook = NULL; hook(); }
if (apply_fail_at == apply_calls) return ESP_FAIL;
working = *config; return ESP_OK;
}
esp_err_t serial_service_get_config(serial_config_t *config) {
serial_api(); *config = working; return get_error;
}
esp_err_t serial_service_start(void) { serial_api(); ++start_calls; return start_error; }
esp_err_t serial_service_stop(void) { serial_api(); ++stop_calls; return stop_error; }
esp_err_t serial_config_save(const serial_config_t *config) {
serial_api(); ++save_calls;
if (save_error == ESP_OK) { persisted = *config; have_stored = true; }
return save_error;
}
esp_err_t serial_config_load(serial_config_t *config, bool *stored) {
serial_api(); ++load_calls; *stored = have_stored;
if (have_stored) *config = persisted; else serial_config_defaults(config);
return load_error;
}
esp_err_t serial_config_reset_storage(void) {
serial_config_t defaults; serial_config_defaults(&defaults); return serial_config_save(&defaults);
}
static void operation_begin(const issued_t *identity, const char *body) {
begin("/api/settings/serial-operation", body ? HTTP_POST : HTTP_GET, body);
same_origin();
if (body) add("Content-Type", "application/json");
if (identity) {
char cookies[100]; snprintf(cookies, sizeof(cookies), "__Host-sak-session=%s", identity->token);
add("Cookie", cookies);
if (body) add("X-CSRF-Token", identity->view.csrf);
}
}
static void operation_expect(const char *status) {
unsigned before = api_calls;
esp_err_t error = web_serial_settings_handler(&req);
assert(error == (send_fail || aux.remaining_len ? ESP_FAIL : ESP_OK));
if (strcmp(response_status, status)) fprintf(stderr, "Expected %s, got %s\n", status, response_status);
assert(!strcmp(response_status, status) && api_calls == before);
assert(strlen(output) < 96); zero(scratch, sizeof(scratch));
}
static void execute(void) {
assert(queued_id); on_dispatcher = true;
web_serial_settings_execute(queued_id); on_dispatcher = false;
}
static void submit(const issued_t *identity, const char *body) {
operation_begin(identity, body); operation_expect("202 Accepted");
assert(s_operation.state == PENDING && queued_id == s_operation.id);
}
static void action(const issued_t *identity, const char *name) {
char body[40]; snprintf(body, sizeof(body), "{\"action\":\"%s\"}", name);
submit(identity, body); execute();
}
static const char apply_body[] = "{\"action\":\"apply\",\"baud\":230400,\"data_bits\":\"7\",\"parity\":\"even\",\"stop_bits\":\"2\",\"flow\":\"rts-cts\",\"dtr\":\"on-connect\",\"rts_threshold\":96}";
static void revoke_during_apply(void) { web_session_store_invalidate(s_operation.session); }
static issued_t executing_identity;
static void request_during_apply(void) {
serial_operation_t executing = s_operation;
operation_begin(&executing_identity, "{\"action\":\"reset\"}");
operation_expect("503 Service Unavailable");
assert(!memcmp(&executing, &s_operation, sizeof(executing)));
operation_begin(&executing_identity, NULL); operation_expect("200 OK");
assert(strstr(output, "\"state\":\"pending\""));
}
static int show_status(void) { return 0; }
static int show_counters(void) { return 0; }
static void print_config(const serial_config_t *config) { (void)config; }
static void print_usage(void) {}
bool serial_service_is_running(void) { return false; }
void serial_service_clear_counters(void) {}
const char *esp_err_to_name(esp_err_t error) { (void)error; return "fake error"; }
#include "serial_console_production.h"
static void serial_settings_tests(void) {
auth_reset(); issued_t admin = mint(&alice), user = mint(&bob), other = mint(&alice);
receive_fragment = 64;
serial_config_defaults(&working); persisted = working;
operation_begin(NULL, "{\"action\":\"save\"}"); operation_expect("401 Unauthorized");
operation_begin(&user, "{\"action\":\"save\"}"); operation_expect("403 Forbidden");
operation_begin(&user, NULL); operation_expect("403 Forbidden");
for (unsigned mode = 0; mode < 9; ++mode) {
operation_begin(&admin, "{\"action\":\"save\"}");
if (mode == 0) req.content_len = aux.remaining_len = 257;
if (mode == 1) req.uri = "/api/settings/serial-operation?command=save";
if (mode == 2) req.method = HTTP_GET;
if (mode == 3) add("X-CSRF-Token", "duplicate");
if (mode == 4) add("Origin", "https://evil.example");
if (mode == 5) add("Transfer-Encoding", "chunked");
if (mode == 6) add("Content-Type", "text/plain");
if (mode == 7) { begin("/api/settings/serial-operation", HTTP_POST, "{\"action\":\"save\"}"); same_origin(); }
if (mode == 8) add("Sec-Fetch-Site", "cross-site");
unsigned before = s_next_id;
(void)web_serial_settings_handler(&req);
assert(strncmp(response_status, "4", 1) == 0 && s_next_id == before && !api_calls);
}
puts("PASS Serial mutation security: cookie/admin/current policy, duplicate headers, CSRF/Origin, body/query/method/framing bounds before admission");
const char *invalid[] = {"{}", "[]", "{\"action\":\"serial save\"}", "{\"action\":\"save\",\"action\":\"stop\"}",
"{\"action\":\"apply\"}", "{\"action\":\"save\",\"baud\":110}", "{\"action\":\"save\",}",
"{\"action\":\"save\"}x", "{\"action\":true}", "{\"action\":\"sa\\u0076e\"}", "{\"command\":\"save\"}",
"{\"baud\":1e3,\"action\":\"apply\"}", "{\"baud\":-1,\"action\":\"apply\"}", "{\"baud\":0110,\"action\":\"apply\"}",
"{\"baud\":42949672960,\"action\":\"apply\"}", "{\"baud\":1.1,\"action\":\"apply\"}"};
for (unsigned i = 0; i < sizeof(invalid) / sizeof(*invalid); ++i) {
operation_begin(&admin, invalid[i]); operation_expect("400 Bad Request");
}
serial_operation_t parsed;
assert(parse(apply_body, strlen(apply_body), &parsed));
char bad[256];
const char *fields[] = {"230400", "\"7\"", "\"even\"", "\"2\"", "\"rts-cts\"", "\"on-connect\"", "96"};
const char *replacements[] = {"109", "\"9\"", "\"mark\"", "\"1.5\"", "\"xon-xoff\"", "\"bad\"", "128"};
for (unsigned i = 0; i < 7; ++i) {
const char *at = strstr(apply_body, fields[i]); assert(at);
snprintf(bad, sizeof(bad), "%.*s%s%s", (int)(at - apply_body), apply_body, replacements[i], at + strlen(fields[i]));
operation_begin(&admin, bad); operation_expect("400 Bad Request");
}
for (size_t size = 0; size < strlen(apply_body); ++size) assert(!parse(apply_body, size, &parsed));
for (unsigned bits = 7; bits <= 8; ++bits)
for (unsigned parity = 0; parity < 3; ++parity)
for (unsigned stops = 1; stops <= 2; ++stops)
for (unsigned flow = 0; flow < 2; ++flow)
for (unsigned dtr = 0; dtr < 3; ++dtr)
for (unsigned baud = 0; baud < 2; ++baud)
for (unsigned threshold = 0; threshold < 2; ++threshold) {
const char *parities[] = {"none", "even", "odd"};
const char *flows[] = {"none", "rts-cts"};
const char *dtrs[] = {"inactive", "active", "on-connect"};
int size = snprintf(bad, sizeof(bad),
"{\"rts_threshold\":%u,\"dtr\":\"%s\",\"flow\":\"%s\",\"stop_bits\":\"%u\","
"\"parity\":\"%s\",\"data_bits\":\"%u\",\"baud\":%u,\"action\":\"apply\"}",
threshold ? 127U : 1U, dtrs[dtr], flows[flow], stops,
parities[parity], bits, baud ? 1000000U : 110U);
assert(size > 0 && (size_t)size < sizeof(bad) && parse(bad, (size_t)size, &parsed));
assert(parsed.config.baud_rate == (baud ? 1000000U : 110U));
assert(parsed.config.rts_threshold == (threshold ? 127U : 1U));
}
memcpy(bad, apply_body, sizeof(apply_body));
assert(!parse(bad, sizeof(apply_body), &parsed)); /* NUL is not JSON whitespace. */
char *control = strstr(bad, "on-connect"); assert(control); *control = '\1';
assert(!parse(bad, strlen(apply_body), &parsed));
receive_fragment = 1; operation_begin(&admin, apply_body); operation_expect("400 Bad Request"); assert(body_offset == 4);
receive_fragment = 64; recv_fail = true; operation_begin(&admin, apply_body); operation_expect("400 Bad Request"); recv_fail = false;
assert(!api_calls);
puts("PASS Serial JSON: exact seven-field Apply/action-only schema, enum/range rejection, truncation, unknown/duplicate/escaped/oversized numbers, four-read bound");
char full_body[257];
memset(full_body, ' ', sizeof(full_body) - 1);
memcpy(full_body, apply_body, strlen(apply_body)); full_body[256] = 0;
queue_fail = true; operation_begin(&admin, full_body); operation_expect("503 Service Unavailable"); queue_fail = false;
assert(body_offset == 256);
puts("PASS Serial boundaries: 288 valid framing/range combinations, action-last order, binary/control rejection, exact 256-byte/four-read body, rejected unread bodies close");
assert(s_operation.state == IDLE);
submit(&admin, apply_body); uint32_t first = queued_id;
operation_begin(&other, "{\"action\":\"stop\"}"); operation_expect("503 Service Unavailable"); assert(queued_id == first);
operation_begin(&other, NULL); operation_expect("200 OK"); assert(strstr(output, "\"state\":\"idle\""));
operation_begin(&admin, NULL); operation_expect("200 OK"); assert(strstr(output, "pending"));
execute(); assert(s_operation.state == OK && working.baud_rate == 230400 && persisted.baud_rate == 115200);
zero(&s_operation.principal, sizeof(s_operation.principal)); zero(&s_operation.config, sizeof(s_operation.config));
unsigned before = api_calls; execute(); assert(api_calls == before);
action(&admin, "save"); assert(persisted.baud_rate == 230400 && s_operation.state == OK);
action(&admin, "defaults"); assert(working.baud_rate == 115200 && persisted.baud_rate == 230400);
action(&admin, "load"); assert(working.baud_rate == 230400);
have_stored = false; action(&admin, "load"); assert(s_operation.state == LOADED_DEFAULTS && working.baud_rate == 115200);
action(&admin, "start"); action(&admin, "stop"); assert(start_calls == 1 && stop_calls == 1);
action(&admin, "reset"); assert(s_operation.state == OK && persisted.baud_rate == 115200);
puts("PASS Serial ownership: no serial/NVS on HTTPD, single pending slot, isolated results, replay fence, explicit apply/save/load/default/reset/start/stop semantics");
working.baud_rate = 460800; persisted.baud_rate = 230400; save_error = ESP_FAIL;
action(&admin, "reset"); assert(s_operation.state == FAILED && working.baud_rate == 460800 && persisted.baud_rate == 230400);
apply_fail_at = apply_calls + 2; action(&admin, "reset"); assert(s_operation.state == ROLLBACK_FAILED);
save_error = ESP_OK; apply_fail_at = apply_calls + 1; before = save_calls;
action(&admin, "reset"); assert(s_operation.state == FAILED && save_calls == before);
apply_fail_at = 0; get_error = ESP_FAIL; action(&admin, "save"); assert(s_operation.state == FAILED && save_calls == before); get_error = ESP_OK;
load_error = ESP_FAIL; before = apply_calls; action(&admin, "load"); assert(s_operation.state == FAILED && apply_calls == before); load_error = ESP_OK;
start_error = stop_error = ESP_FAIL; action(&admin, "start"); assert(s_operation.state == FAILED); action(&admin, "stop"); assert(s_operation.state == FAILED);
start_error = stop_error = ESP_OK;
puts("PASS Serial failure ordering: apply/get/load/save/lifecycle failures, reset commit failure rollback and explicit failed rollback");
for (unsigned i = 0; i < ACTION_COUNT; ++i) {
working.baud_rate = 460800; persisted.baud_rate = 230400; have_stored = true;
if (i == APPLY) { submit(&admin, apply_body); execute(); }
else action(&admin, s_actions[i]);
serial_config_t typed_working = working, typed_persisted = persisted;
working.baud_rate = 460800; persisted.baud_rate = 230400;
on_dispatcher = true;
if (i == APPLY) {
const char *names[] = {"baud", "data-bits", "parity", "stop-bits", "flow", "dtr", "rts-threshold"};
const char *values[] = {"230400", "7", "even", "2", "rts-cts", "on-connect", "96"};
for (unsigned j = 0; j < 7; ++j) assert(set_parameter(names[j], values[j]) == 0);
} else {
char *args[] = {"serial", (char *)s_actions[i]}; assert(command_serial(2, args) == 0);
}
on_dispatcher = false;
assert(!memcmp(&working, &typed_working, sizeof(working)) && !memcmp(&persisted, &typed_persisted, sizeof(persisted)));
}
puts("PASS Serial CLI comparison: exact canonical command/set handlers and typed actions produce matching working/persisted config (UART/NVS doubled)");
submit(&admin, apply_body);
serial_operation_t pending = s_operation;
before = api_calls;
on_dispatcher = true;
web_serial_settings_execute(0); web_serial_settings_execute(first);
on_dispatcher = false;
assert(api_calls == before && !memcmp(&pending, &s_operation, sizeof(pending)));
executing_identity = admin; apply_hook = request_during_apply; execute();
assert(s_operation.state == OK && api_calls == before + 1);
operation_begin(&other, NULL); operation_expect("200 OK"); assert(strstr(output, "\"state\":\"idle\""));
operation_begin(&admin, NULL); operation_expect("200 OK"); assert(strstr(output, "\"state\":\"ok\""));
puts("PASS Serial interleaving: obsolete/zero IDs cannot execute replacement, executing slot rejects reset and remains pollable, completed result isolated by session");
submit(&admin, apply_body); before = api_calls; now += 30000000LL; execute(); assert(s_operation.state == CANCELLED && api_calls == before);
submit(&admin, apply_body); web_session_store_invalidate(admin.view.id); execute(); assert(s_operation.state == CANCELLED && api_calls == before);
admin = mint(&alice); submit(&admin, apply_body); db_fail = true; execute(); db_fail = false; assert(s_operation.state == CANCELLED && api_calls == before);
submit(&other, apply_body); apply_hook = revoke_during_apply; execute(); assert(s_operation.state == OK); /* admitted work can finish */
operation_begin(&other, NULL); operation_expect("401 Unauthorized");
web_cookie_auth_stop(); assert(web_cookie_auth_start() == ESP_OK); admin = mint(&alice);
operation_begin(&admin, NULL); operation_expect("200 OK"); assert(strstr(output, "idle"));
submit(&admin, apply_body); web_cookie_auth_stop(); assert(web_cookie_auth_start() == ESP_OK); before = api_calls; execute(); assert(s_operation.state == CANCELLED && api_calls == before);
puts("PASS Serial currentness: queue deadline, session revocation, database failure, stop/restart IDs and admitted-work completion after revocation");
admin = mint(&alice); submit(&admin, apply_body); before = api_calls;
stale_user = alice.user_id; execute(); stale_user = 0;
assert(s_operation.state == CANCELLED && api_calls == before);
zero(&s_operation.principal, sizeof(s_operation.principal)); zero(&s_operation.config, sizeof(s_operation.config));
admin = mint(&alice); now = admin.view.expires_at_us - 1;
submit(&admin, apply_body);
now = admin.view.expires_at_us;
assert(now < s_operation.deadline); execute();
assert(s_operation.state == CANCELLED && api_calls == before);
puts("PASS Serial queued authorization: missed account revocation and absolute cookie expiry cancel without serial/NVS calls and wipe retained request data");
admin = mint(&alice); send_fail = true; submit(&admin, apply_body); send_fail = false; execute(); assert(s_operation.state == OK);
operation_begin(&admin, NULL); operation_expect("200 OK"); assert(strstr(output, "\"state\":\"ok\""));
for (unsigned failure = 1; failure <= 3; ++failure) {
operation_begin(&admin, NULL); server.config.max_resp_headers = failure - 1;
before = sends; assert(web_serial_settings_handler(&req) != ESP_OK && sends == before);
}
server.config.max_resp_headers = 8;
s_next_id = UINT32_MAX; operation_begin(&admin, apply_body); operation_expect("503 Service Unavailable");
puts("PASS Serial result transport: lost acknowledgement does not cancel/replay, bounded headers/response, no ID wrap");
}
+51 -1
View File
@@ -12,6 +12,7 @@ static atomic_bool s_running;
static serial_config_t s_config;
static int s_state_mutex;
static unsigned serial_takes;
static bool change_after_snapshot;
static web_server_counters_t s_counters;
static unsigned allocations, allocation_fail_at, frees;
static httpd_uri_t *routes[2];
@@ -28,7 +29,13 @@ static int xSemaphoreTake(int mutex, unsigned wait) {
if (serial_busy) return 0;
serial_locked = true; return pdTRUE;
}
static void xSemaphoreGive(int mutex) { (void)mutex; assert(serial_locked); serial_locked = false; }
static void xSemaphoreGive(int mutex) {
(void)mutex; assert(serial_locked); serial_locked = false;
if (change_after_snapshot) {
s_running = !s_running;
s_config.baud_rate = 110;
}
}
static void increment_counter(uint64_t *counter) { ++*counter; }
esp_err_t httpd_resp_send(httpd_req_t *r, const char *body, int length) {
assert(!serial_locked && length >= 0 && length < 256 && (size_t)length == strlen(body));
@@ -54,6 +61,38 @@ static void settings_expect(const char *status) {
assert(strlen(output) < 128 && !body_offset);
zero(scratch, sizeof(scratch));
}
static void status_snapshot_tests(void) {
char response[256];
serial_config_t saved_config = s_config;
bool saved_running = s_running;
for (unsigned running = 0; running < 2; ++running) {
s_config = saved_config; s_running = running;
unsigned before = serial_takes;
change_after_snapshot = true;
int length = status_serial_projection(response, sizeof(response));
change_after_snapshot = false;
assert(length > 0 && (size_t)length < sizeof(response));
assert(serial_takes == before + 1 && !serial_locked);
assert(s_running == !running && s_config.baud_rate == 110);
assert(strstr(response, running
? "\"running\":true,\"config_available\":true,\"baud\":230400"
: "\"running\":false,\"config_available\":true,\"baud\":230400"));
}
for (unsigned unavailable = 0; unavailable < 2; ++unavailable)
for (unsigned running = 0; running < 2; ++running) {
s_running = running; s_config = saved_config;
serial_busy = unavailable == 0; s_initialized = unavailable == 0;
unsigned before = serial_takes;
int length = status_serial_projection(response, sizeof(response));
assert(length > 0 && (size_t)length < sizeof(response));
assert(serial_takes == before + (unavailable == 0) && !serial_locked);
assert(strstr(response, "\"running\":null,\"config_available\":false,\"baud\":0"));
assert(strstr(response, "\"data_bits\":\"unknown\""));
}
s_initialized = true; serial_busy = false;
s_config = saved_config; s_running = saved_running;
puts("PASS Status serial projection: same-snapshot running/config despite post-unlock changes, busy/uninitialized => null for either live state, zero-wait acquisition and no blocking getter");
}
static void settings_tests(void) {
httpd_uri_t existing = {.uri = "/", .method = HTTP_GET, .handler = serial_settings_handler};
httpd_uri_t route = {.uri = "/api/settings/serial", .method = HTTP_GET, .handler = serial_settings_handler};
@@ -84,6 +123,16 @@ static void settings_tests(void) {
}
assert(web_httpd_register_optional_get(NULL, &route) == ESP_ERR_INVALID_ARG);
assert(web_httpd_register_optional_get(&server, NULL) == ESP_ERR_INVALID_ARG);
other = route; other.method = HTTP_POST;
for (unsigned failure = 1; failure <= 2; ++failure) {
allocations = frees = 0; allocation_fail_at = failure;
assert(web_httpd_register_optional(&server, &other) == ESP_ERR_NO_MEM && !routes[1]);
assert(allocations == failure && frees == failure - 1);
}
allocation_fail_at = 0;
assert(web_httpd_register_optional(&server, &other) == ESP_OK);
assert(web_httpd_register_optional(&server, &other) == ESP_ERR_INVALID_STATE);
assert(httpd_unregister_uri_handler(&server, other.uri, HTTP_POST) == ESP_OK && !routes[1]);
puts("PASS Settings registration: both allocation failures leave table intact, duplicate/full/shape bounds, installed IDF unregister frees successful ownership");
auth_reset(); issued_t admin = mint(&alice), user = mint(&bob);
s_initialized = true; s_running = true;
@@ -101,6 +150,7 @@ static void settings_tests(void) {
assert(serial_service_get_snapshot(&value) == ESP_OK && value.running);
assert(!memcmp(&value.config, &s_config, sizeof(s_config)) && !serial_locked);
puts("PASS Settings snapshot: exact nonblocking production body, failure clearing and lock-consistent copy");
status_snapshot_tests();
settings_begin(NULL); settings_expect("401 Unauthorized");
settings_begin(&user); settings_expect("403 Forbidden");
+8 -1
View File
@@ -18,6 +18,7 @@ static size_t body_offset;
static unsigned password_calls, cookie_count, sends, upgrades;
static unsigned fail_header, setter_calls;
static bool send_fail, recv_fail;
static size_t receive_fragment = 7;
static void (*password_hook)(void);
static char response_status[48];
static struct httpd_req_aux aux;
@@ -46,7 +47,7 @@ esp_err_t httpd_resp_sendstr(httpd_req_t *r, const char *body) {
}
int httpd_req_recv(httpd_req_t *r, char *out, size_t size) {
(void)r; if (recv_fail) return -1;
if (size > 7) size = 7; /* Fragment every login body. */
if (size > receive_fragment) size = receive_fragment;
memcpy(out, request_body + body_offset, size); body_offset += size;
aux.remaining_len -= size; return (int)size;
}
@@ -122,6 +123,9 @@ static void auth_reset(void) {
#ifdef HOST_SETTINGS
#include "settings_test.c"
#endif
#ifdef HOST_SERIAL_SETTINGS
#include "serial_settings_test.c"
#endif
int main(void) {
assert(store_tests() == 0); auth_reset();
@@ -272,6 +276,9 @@ int main(void) {
#endif
#ifdef HOST_SETTINGS
settings_tests();
#endif
#ifdef HOST_SERIAL_SETTINGS
serial_settings_tests();
#endif
return 0;
}