Add admin firmware upload support

Implement authenticated HTTPS OTA uploads with bounded streaming, image
validation, reboot coordination, and lifecycle exclusion. Add the admin
UI,
regression tests, and Phase 10 acceptance documentation.
This commit is contained in:
2026-09-18 22:22:11 +02:00
parent 4f628a4098
commit 31a22eba06
31 changed files with 1442 additions and 88 deletions
+5
View File
@@ -15,6 +15,11 @@ static esp_err_t web_server_start(void) {
OUTSIDE(); assert(!httpd_owner && rotations && web_stops == 1 && web_stop_result == ESP_OK);
++web_starts; return web_start_result;
}
static bool reboot_busy;
static esp_err_t web_firmware_update_reserve_reboot(void) {
OUTSIDE(); assert(!httpd_owner);
return reboot_busy ? ESP_ERR_INVALID_STATE : ESP_OK;
}
static void esp_restart(void) { OUTSIDE(); assert(!httpd_owner); ++reboots; }
esp_err_t web_server_stop(void) { OUTSIDE(); assert(!httpd_owner); ++web_stops; return web_stop_result; }
static esp_err_t web_server_replace_identity(uint32_t service, uint32_t identity, bool reset, bool *committed) {
@@ -180,20 +180,20 @@ static void pipeline_tests(void) {
reset(); lifecycle_fail_at = failure; start();
unsigned failed_route = (failure + 1) / 2;
assert(lifecycle_calls == failed_route && lifecycle_allocations == failure);
assert(registered_count == (failed_route == 1 ? 36 : 37));
assert(registered_count == (failed_route == 1 ? 37 : 38));
assert(!method_route("/api/settings/lifecycle-operation", HTTP_POST));
assert(!method_route("/api/settings/lifecycle-operation", HTTP_GET));
assert(!!method_route("/api/settings/lifecycle", HTTP_GET) == (failed_route != 1));
other_domains_complete(); network_complete(); display_complete(); broker_complete(); ssh_complete();
assert(web_server_stop() == ESP_OK);
lifecycle_fail_at = 0; fresh_registration(); start(); assert(registered_count == 39);
lifecycle_fail_at = 0; fresh_registration(); start(); assert(registered_count == 40);
assert(method_route("/api/settings/lifecycle-operation", HTTP_POST)->handler == web_lifecycle_operation_handler);
assert(web_server_stop() == ESP_OK);
}
puts("PASS all six lifecycle route allocation positions preserve other domains and restart recovers the complete optional API");
for (unsigned failure = 5; failure <= 6; ++failure) {
reset(); lifecycle_fail_at = failure; unregister_fail = true; start();
assert(registered_count == 38 && unregister_calls == 1);
assert(registered_count == 39 && unregister_calls == 1);
assert(method_route("/api/settings/lifecycle-operation", HTTP_GET));
assert(!method_route("/api/settings/lifecycle-operation", HTTP_POST));
other_domains_complete(); ssh_complete();
@@ -201,7 +201,7 @@ static void pipeline_tests(void) {
assert(web_server_start() == ESP_ERR_INVALID_STATE);
ssl_stop_error = ESP_OK; assert(web_server_stop() == ESP_OK);
unregister_fail = false; lifecycle_fail_at = 0; fresh_registration(); start();
assert(registered_count == 39 && web_server_stop() == ESP_OK);
assert(registered_count == 40 && web_server_stop() == ESP_OK);
}
puts("PASS lifecycle failed unregister leaves reads only; failed shutdown preserves ownership before successful restart");
}
+54 -49
View File
@@ -37,8 +37,8 @@ def define(path, name):
uri_tables = re.findall(r'^static const httpd_uri_t(?: \*const)? \w+\[?\]? = \{.*?^\};',
source, re.M | re.S)
# Non-array declarations have no brackets; explicit shape avoids silent omission.
if len(uri_tables) != 36:
raise RuntimeError('Review URI extraction: expected 34 descriptors and two tables')
if len(uri_tables) != 37:
raise RuntimeError('Review URI extraction: expected 35 descriptors and two tables')
state = source[source.index('static SemaphoreHandle_t s_server_mutex;'):
source.index('static esp_err_t ensure_mutex(void)')]
header = (ROOT / 'src/web_server.h').read_text()
@@ -46,6 +46,7 @@ header = '\n'.join(line for line in header.splitlines()
if not line.startswith(('#include', '#pragma once')))
constants = define(SOURCE, 'WEB_SERVER_PORT')
for filename, names in {
'web_firmware_update.h': ('WEB_FIRMWARE_UPDATE_URI',),
'web_admin_transport.h': ('WEB_ADMIN_TICKET_URI', 'WEB_ADMIN_WS_URI'),
'web_serial_transport.h': ('WEB_SERIAL_TRANSPORT_TICKET_URI', 'WEB_SERIAL_TRANSPORT_WS_URI'),
'web_security.h': ('WEB_SECURITY_CERTIFICATE_DER_CAPACITY', 'WEB_SECURITY_PRIVATE_KEY_DER_CAPACITY'),
@@ -104,7 +105,7 @@ static bool unregister_fail;
static bool settings_fail;
static unsigned settings_calls;
static unsigned operation_calls, operation_fail_at;
static const httpd_uri_t *registered[39];
static const httpd_uri_t *registered[40];
static char events[128]; static size_t event_length;
static void event(char value) { assert(!locked && event_length + 1 < sizeof(events)); events[event_length++] = value; events[event_length] = 0; }
static SemaphoreHandle_t xSemaphoreCreateMutex(void) { assert(!locked); return mutex_fail ? NULL : &mutex_storage; }
@@ -134,6 +135,7 @@ static esp_err_t web_security_replace_reserved(uint32_t token) {
}
static void web_security_release_identity(uint32_t token) { assert(!locked); if (token == identity_token) identity_token = 0; }
#define HANDLER(name) static esp_err_t name(httpd_req_t *r) { (void)r; assert(!"HTTP handler must not run in lifecycle harness"); return ESP_FAIL; }
HANDLER(web_firmware_update_handler)
HANDLER(root_handler) HANDLER(status_handler) HANDLER(traced_ticket_handler)
HANDLER(traced_websocket_handler) HANDLER(asset_handler) HANDLER(web_cookie_auth_handler)
HANDLER(traced_admin_ticket_handler) HANDLER(traced_admin_upgrade_handler)
@@ -277,7 +279,7 @@ static esp_err_t web_security_copy_tls_material(uint8_t *cert, size_t nc, size_t
static esp_err_t httpd_ssl_start(httpd_handle_t *server, const httpd_ssl_config_t *config) {
assert(!locked && auth_live && !ssl_live); ++ssl_starts;
assert(config->httpd.max_open_sockets == 6 && !config->httpd.lru_purge_enable);
assert(config->httpd.max_uri_handlers == 39 && config->port_secure == 443);
assert(config->httpd.max_uri_handlers == 40 && config->port_secure == 443);
assert(config->httpd.recv_wait_timeout == 1 && config->httpd.send_wait_timeout == 1);
assert(config->tls_handshake_timeout_ms == 5000);
assert(config->user_cb == tls_session_callback);
@@ -293,7 +295,7 @@ static esp_err_t register_one(httpd_handle_t server) {
}
static esp_err_t httpd_register_uri_handler(httpd_handle_t s, const httpd_uri_t *uri) {
if (!strcmp(uri->uri, "/api/settings/serial")) {
assert(s == SERVER && auth_live && ssl_live && registration_calls >= 17);
assert(s == SERVER && auth_live && ssl_live && registration_calls >= 18);
assert(uri->method == HTTP_GET && uri->handler == serial_settings_handler);
++settings_calls;
if (settings_fail) return ESP_ERR_NO_MEM;
@@ -301,11 +303,11 @@ static esp_err_t httpd_register_uri_handler(httpd_handle_t s, const httpd_uri_t
return ESP_OK;
}
if (!strcmp(uri->uri, "/api/admin/ws-ticket") || !strcmp(uri->uri, "/ws/admin")) {
assert(registration_calls >= 16);
assert(registration_calls >= 17);
assert(serial_init_error != ESP_OK || serial_live);
} else assert(registration_calls < 14);
} else assert(registration_calls < 15);
esp_err_t error = register_one(s);
if (error == ESP_OK) { assert(registered_count < 39); registered[registered_count++] = uri; }
if (error == ESP_OK) { assert(registered_count < 40); registered[registered_count++] = uri; }
return error;
}
static esp_err_t account_register(httpd_handle_t s, const httpd_uri_t *uri) {
@@ -355,7 +357,7 @@ static esp_err_t web_httpd_register_optional(httpd_handle_t s, const httpd_uri_t
}
static esp_err_t httpd_unregister_uri_handler(httpd_handle_t s, const char *uri, int method) {
assert(!locked && s == SERVER && ssl_live && auth_live && serial_live);
assert((registration_calls == 18 && !strcmp(uri, "/api/admin/ws-ticket") && method == HTTP_POST) ||
assert((registration_calls == 19 && !strcmp(uri, "/api/admin/ws-ticket") && method == HTTP_POST) ||
((!strcmp(uri, "/api/settings/serial-operation") || !strcmp(uri, "/api/settings/account-operation") || !strcmp(uri, "/api/settings/network-operation") || !strcmp(uri, "/api/settings/display-operation") || !strcmp(uri, "/api/settings/broker-operation") || !strcmp(uri, "/api/settings/ssh-operation") || !strcmp(uri, "/api/settings/lifecycle-operation")) && method == HTTP_GET));
++unregister_calls;
for (unsigned i = 0; i < registered_count; ++i) {
@@ -372,15 +374,15 @@ static esp_err_t httpd_unregister_uri_handler(httpd_handle_t s, const char *uri,
}
static esp_err_t httpd_register_err_handler(httpd_handle_t s, httpd_err_code_t code,
esp_err_t (*handler)(httpd_req_t *, httpd_err_code_t)) {
assert(registration_calls == 14 || registration_calls == 15);
assert(registration_calls == 15 || registration_calls == 16);
assert((code == 404 || code == 405) && handler == route_error_handler);
return register_one(s);
}
static esp_err_t web_serial_transport_attach_server(httpd_handle_t s) {
assert(!locked && s == SERVER && ssl_live && auth_live && registration_calls == 16);
assert(!locked && s == SERVER && ssl_live && auth_live && registration_calls == 17);
++serial_attaches; serial_live = true; return ESP_OK;
}
static esp_err_t web_admin_transport_init(void) { assert(!locked && auth_live && registration_calls == 18); ++admin_inits; return admin_init_error; }
static esp_err_t web_admin_transport_init(void) { assert(!locked && auth_live && registration_calls == 19); ++admin_inits; return admin_init_error; }
static esp_err_t web_admin_transport_attach(httpd_handle_t s) {
assert(!locked && s == SERVER && ssl_live && !admin_owned); ++admin_attaches;
admin_owned = admin_attach_error == ESP_OK; return admin_attach_error;
@@ -531,7 +533,8 @@ int main(void) {
}
puts("PASS optional admin init/attach failures do not disable M1 auth or serial attachment");
reset(); start(); assert(registered_count == 39 && registration_calls == 18 && settings_calls == 1 && operation_calls == 2);
reset(); start(); assert(registered_count == 40 && registration_calls == 19 && settings_calls == 1 && operation_calls == 2);
assert(route("/api/firmware")->method == HTTP_POST && route("/api/firmware")->handler == web_firmware_update_handler);
assert(generation_calls == 1 && route("/api/settings/accounts/generate-password")->handler == web_account_generate_password_handler);
assert(route("/api/settings/serial")->handler == serial_settings_handler);
assert(keys_calls == 1 && route("/api/settings/accounts/keys")->handler == web_account_keys_handler);
@@ -569,23 +572,23 @@ int main(void) {
assert(admin_detaches == 2 && serial_detaches == 1 && admin_stoppeds == 1 && !s_admin_transport_owned);
puts("PASS failed SSL stop retains admin ownership; stopped runs only after successful retry");
for (unsigned failure = 1; failure <= 16; ++failure) {
for (unsigned failure = 1; failure <= 17; ++failure) {
reset(); registration_fail_at = failure;
assert(web_server_start() == ESP_FAIL);
assert(registration_calls == failure && !admin_inits && !admin_attaches && !serial_attaches);
assert(!auth_live && !ssl_live && ssl_stops == 1 && !s_server && !s_admin_transport_owned);
assert(!admin_detaches && !admin_stoppeds && !s_transitioning && s_counters.start_failures == 1);
}
puts("PASS required registration positions 1..16 fail fatally before transport attachment");
puts("PASS required registration positions 1..17 fail fatally before transport attachment");
for (unsigned failure = 17; failure <= 18; ++failure) {
for (unsigned failure = 18; failure <= 19; ++failure) {
reset(); registration_fail_at = failure;
assert(web_server_start() == ESP_OK && registration_calls == failure);
assert(auth_live && ssl_live && serial_live && s_server == SERVER);
assert(s_serial_transport_attached && !s_admin_transport_owned && !admin_owned);
assert(!admin_inits && !admin_attaches && !auth_stops && !ssl_stops);
assert(!s_transitioning && s_last_error == ESP_OK && s_counters.starts == 1 && !s_counters.start_failures);
assert(registered_count == 37 && unregister_calls == failure - 17);
assert(registered_count == 38 && unregister_calls == failure - 18);
for (unsigned i = 0; i < registered_count; ++i)
assert(strcmp(registered[i]->uri, "/api/admin/ws-ticket") && strcmp(registered[i]->uri, "/ws/admin"));
assert(route("/ws/serial")->handler == traced_websocket_handler);
@@ -594,13 +597,13 @@ int main(void) {
clear_events(); assert(web_server_stop() == ESP_OK && !strcmp(events, "ASH"));
assert(!admin_detaches && !admin_stoppeds);
registration_fail_at = 0; fresh_registration(); start();
assert(registered_count == 39 && admin_attaches == 1 && s_counters.starts == 2);
assert(registered_count == 40 && admin_attaches == 1 && s_counters.starts == 2);
assert(web_server_stop() == ESP_OK && admin_stoppeds == 1);
}
puts("PASS optional positions 17..18 preserve M1, roll back ticket when needed and recover after stop/restart");
puts("PASS optional positions 18..19 preserve M1, roll back ticket when needed and recover after stop/restart");
reset(); registration_fail_at = 18; unregister_fail = true;
assert(web_server_start() == ESP_OK && unregister_calls == 1 && registered_count == 38);
reset(); registration_fail_at = 19; unregister_fail = true;
assert(web_server_start() == ESP_OK && unregister_calls == 1 && registered_count == 39);
assert(auth_live && ssl_live && serial_live && s_serial_transport_attached);
assert(!admin_inits && !admin_attaches && !admin_owned && !s_admin_transport_owned);
ticket = route("/api/admin/ws-ticket");
@@ -612,7 +615,7 @@ int main(void) {
clear_events(); assert(web_server_stop() == ESP_OK && !strcmp(events, "ASH"));
assert(!admin_detaches && !admin_stoppeds);
unregister_fail = false; registration_fail_at = 0; fresh_registration(); start();
assert(registered_count == 39 && admin_attaches == 1 && web_server_stop() == ESP_OK);
assert(registered_count == 40 && admin_attaches == 1 && web_server_stop() == ESP_OK);
puts("PASS failed unregister retains only original ticket handler, no admin attachment, and permits restart");
reset(); registration_fail_at = 6; ssl_stop_error = ESP_FAIL;
@@ -634,7 +637,7 @@ int main(void) {
assert(web_server_stop() == ESP_ERR_INVALID_STATE && !auth_stops);
puts("PASS auth/start failure gates and invalid/transitioning lifecycle rejection");
reset(); settings_fail = true; start();
assert(settings_calls == 1 && registered_count == 38);
assert(settings_calls == 1 && registered_count == 39);
assert(auth_live && serial_live && admin_owned && web_server_stop() == ESP_OK);
settings_fail = false; fresh_registration(); start();
assert(route("/api/settings/serial")->handler == serial_settings_handler);
@@ -642,7 +645,7 @@ int main(void) {
puts("PASS optional Settings registration failure preserves auth and both transports; restart recovers");
for (unsigned failure = 1; failure <= 2; ++failure) {
reset(); operation_fail_at = failure; start();
assert(registered_count == 37 && operation_calls == failure && unregister_calls == failure - 1);
assert(registered_count == 38 && operation_calls == failure && unregister_calls == failure - 1);
assert(auth_live && serial_live && admin_owned);
for (unsigned i = 0; i < registered_count; ++i) assert(strcmp(registered[i]->uri, "/api/settings/serial-operation"));
assert(web_server_stop() == ESP_OK);
@@ -650,25 +653,27 @@ int main(void) {
puts("PASS optional Serial operation GET/POST failure never publishes a mutation-only route or disables transports");
for (unsigned failure = 1; failure <= 3; ++failure) {
reset(); account_calls = 0; account_fail_at = failure; start();
assert(account_calls == failure && registered_count == (failure == 1 ? 36 : 37));
assert(account_calls == failure && registered_count == (failure == 1 ? 37 : 38));
assert(keys_calls == 1 && route("/api/settings/accounts/keys")->handler == web_account_keys_handler);
assert(generation_calls == 1 && route("/api/settings/accounts/generate-password")->handler == web_account_generate_password_handler);
assert(route("/api/firmware")->method == HTTP_POST && route("/api/firmware")->handler == web_firmware_update_handler);
assert(generation_calls == 1 && route("/api/settings/accounts/generate-password")->handler == web_account_generate_password_handler);
assert(auth_live && serial_live && admin_owned);
for (unsigned i = 0; i < registered_count; ++i)
assert(strcmp(registered[i]->uri, "/api/settings/account-operation"));
assert(web_server_stop() == ESP_OK);
account_fail_at = 0; account_calls = 0; fresh_registration(); start();
assert(registered_count == 39 && account_calls == 3);
assert(registered_count == 40 && account_calls == 3);
assert(web_server_stop() == ESP_OK);
}
reset(); account_calls = 0; account_fail_at = 3; unregister_fail = true; start();
assert(registered_count == 38 && auth_live && serial_live && admin_owned);
assert(registered_count == 39 && auth_live && serial_live && admin_owned);
for (unsigned i = 0; i < registered_count; ++i)
assert(strcmp(registered[i]->uri, "/api/settings/account-operation") || registered[i]->method == HTTP_GET);
assert(web_server_stop() == ESP_OK); account_fail_at = 0;
puts("PASS optional Accounts list/result/mutation allocation failures preserve transports and never expose mutation without reads (including failed unregister)");
reset(); generation_fail = true; start();
assert(generation_calls == 1 && registered_count == 38 && account_calls == 3);
assert(route("/api/firmware")->method == HTTP_POST && route("/api/firmware")->handler == web_firmware_update_handler);
assert(generation_calls == 1 && registered_count == 39 && account_calls == 3);
assert(keys_calls == 1 && route("/api/settings/accounts/keys")->handler == web_account_keys_handler);
assert(!auth_stops && !ssl_stops && !unregister_calls && !s_counters.start_failures);
assert(route("/api/settings/accounts")->handler == web_account_settings_handler);
@@ -680,12 +685,12 @@ int main(void) {
}
assert(account_mutations == 1 && web_server_stop() == ESP_OK);
generation_fail = false; fresh_registration(); start();
assert(generation_calls == 2 && registered_count == 39);
assert(generation_calls == 2 && registered_count == 40);
assert(route("/api/settings/accounts/generate-password")->handler == web_account_generate_password_handler);
assert(web_server_stop() == ESP_OK);
puts("PASS optional password generation allocation failure preserves account routes/auth/transports; restart recovers");
reset(); keys_fail = true; start();
assert(keys_calls == 1 && registered_count == 38 && account_calls == 3 && generation_calls == 1);
assert(keys_calls == 1 && registered_count == 39 && account_calls == 3 && generation_calls == 1);
assert(!auth_stops && !ssl_stops && !unregister_calls && !s_counters.start_failures);
assert(route("/api/settings/accounts")->handler == web_account_settings_handler);
assert(route("/api/settings/accounts/generate-password")->handler == web_account_generate_password_handler);
@@ -700,7 +705,7 @@ int main(void) {
}
assert(account_mutations == 1 && web_server_stop() == ESP_OK);
keys_fail = false; fresh_registration(); start();
assert(keys_calls == 2 && registered_count == 39);
assert(keys_calls == 2 && registered_count == 40);
assert(route("/api/settings/accounts/keys")->handler == web_account_keys_handler);
assert(web_server_stop() == ESP_OK);
puts("PASS optional account keys allocation failure preserves account/generation/auth/transports; restart recovers");
@@ -725,7 +730,7 @@ int main(void) {
reset(); network_fail_at = failure; start();
unsigned failed_route = (failure + 1) / 2;
assert(network_calls == failed_route && network_allocations == failure);
assert(registered_count == (failed_route == 1 ? 36 : 37));
assert(registered_count == (failed_route == 1 ? 37 : 38));
assert(unregister_calls == (failed_route == 3 ? 1 : 0));
assert(!method_route("/api/settings/network-operation", HTTP_GET));
assert(!method_route("/api/settings/network-operation", HTTP_POST));
@@ -733,13 +738,13 @@ int main(void) {
other_domains_complete();
assert(web_server_stop() == ESP_OK);
network_fail_at = 0; fresh_registration(); start();
assert(registered_count == 39); network_complete();
assert(registered_count == 40); network_complete();
assert(web_server_stop() == ESP_OK);
}
puts("PASS all six Network descriptor/name allocation positions isolate failures and recover after restart");
for (unsigned failure = 5; failure <= 6; ++failure) {
reset(); network_fail_at = failure; unregister_fail = true; start();
assert(registered_count == 38 && unregister_calls == 1);
assert(registered_count == 39 && unregister_calls == 1);
assert(route("/api/settings/network")->handler == web_network_snapshot_handler);
assert(method_route("/api/settings/network-operation", HTTP_GET)->handler == web_network_operation_handler);
assert(!method_route("/api/settings/network-operation", HTTP_POST));
@@ -749,7 +754,7 @@ int main(void) {
assert(web_server_start() == ESP_ERR_INVALID_STATE && ssl_starts == 1);
ssl_stop_error = ESP_OK; assert(web_server_stop() == ESP_OK);
unregister_fail = false; network_fail_at = 0; fresh_registration(); start();
assert(registered_count == 39); network_complete();
assert(registered_count == 40); network_complete();
assert(web_server_stop() == ESP_OK);
}
puts("PASS failed Network result unregister leaves reads only and preserves stop-failure ownership/restart");
@@ -757,7 +762,7 @@ int main(void) {
reset(); display_fail_at = failure; start();
unsigned failed_route = (failure + 1) / 2;
assert(display_calls == failed_route && display_allocations == failure);
assert(registered_count == (failed_route == 1 ? 36 : 37));
assert(registered_count == (failed_route == 1 ? 37 : 38));
assert(unregister_calls == (failed_route == 3 ? 1 : 0));
assert(!method_route("/api/settings/display-operation", HTTP_GET));
assert(!method_route("/api/settings/display-operation", HTTP_POST));
@@ -765,13 +770,13 @@ int main(void) {
other_domains_complete(); network_complete();
assert(web_server_stop() == ESP_OK);
display_fail_at = 0; fresh_registration(); start();
assert(registered_count == 39); display_complete();
assert(registered_count == 40); display_complete();
assert(web_server_stop() == ESP_OK);
}
puts("PASS all six Display descriptor/name allocation positions isolate failures and recover after restart");
for (unsigned failure = 5; failure <= 6; ++failure) {
reset(); display_fail_at = failure; unregister_fail = true; start();
assert(registered_count == 38 && unregister_calls == 1);
assert(registered_count == 39 && unregister_calls == 1);
assert(route("/api/settings/display")->handler == web_display_settings_handler);
assert(method_route("/api/settings/display-operation", HTTP_GET)->handler == web_display_operation_handler);
assert(!method_route("/api/settings/display-operation", HTTP_POST));
@@ -781,7 +786,7 @@ int main(void) {
assert(web_server_start() == ESP_ERR_INVALID_STATE && ssl_starts == 1);
ssl_stop_error = ESP_OK; assert(web_server_stop() == ESP_OK);
unregister_fail = false; display_fail_at = 0; fresh_registration(); start();
assert(registered_count == 39); display_complete();
assert(registered_count == 40); display_complete();
assert(web_server_stop() == ESP_OK);
}
puts("PASS failed Display result unregister leaves reads only and preserves stop-failure ownership/restart");
@@ -789,7 +794,7 @@ int main(void) {
reset(); broker_fail_at = failure; start();
unsigned failed_route = (failure + 1) / 2;
assert(broker_calls == failed_route && broker_allocations == failure);
assert(registered_count == (failed_route == 1 ? 36 : 37));
assert(registered_count == (failed_route == 1 ? 37 : 38));
assert(unregister_calls == (failed_route == 3 ? 1 : 0));
assert(!method_route("/api/settings/broker-operation", HTTP_GET));
assert(!method_route("/api/settings/broker-operation", HTTP_POST));
@@ -797,13 +802,13 @@ int main(void) {
other_domains_complete(); network_complete(); display_complete();
assert(web_server_stop() == ESP_OK);
broker_fail_at = 0; fresh_registration(); start();
assert(registered_count == 39); broker_complete();
assert(registered_count == 40); broker_complete();
assert(web_server_stop() == ESP_OK);
}
puts("PASS all six Broker descriptor/name allocation positions isolate failures and recover after restart");
for (unsigned failure = 5; failure <= 6; ++failure) {
reset(); broker_fail_at = failure; unregister_fail = true; start();
assert(registered_count == 38 && unregister_calls == 1);
assert(registered_count == 39 && unregister_calls == 1);
assert(route("/api/settings/broker")->handler == web_broker_settings_handler);
assert(method_route("/api/settings/broker-operation", HTTP_GET)->handler == web_broker_operation_handler);
assert(!method_route("/api/settings/broker-operation", HTTP_POST));
@@ -813,7 +818,7 @@ int main(void) {
assert(web_server_start() == ESP_ERR_INVALID_STATE && ssl_starts == 1);
ssl_stop_error = ESP_OK; assert(web_server_stop() == ESP_OK);
unregister_fail = false; broker_fail_at = 0; fresh_registration(); start();
assert(registered_count == 39); broker_complete();
assert(registered_count == 40); broker_complete();
assert(web_server_stop() == ESP_OK);
}
puts("PASS failed Broker result unregister leaves reads only and preserves stop-failure ownership/restart");
@@ -821,7 +826,7 @@ int main(void) {
reset(); ssh_fail_at = failure; start();
unsigned failed_route = (failure + 1) / 2;
assert(ssh_calls == failed_route && ssh_allocations == failure);
assert(registered_count == (failed_route == 1 ? 36 : 37));
assert(registered_count == (failed_route == 1 ? 37 : 38));
assert(unregister_calls == (failed_route == 3 ? 1 : 0));
assert(!method_route("/api/settings/ssh-operation", HTTP_GET));
assert(!method_route("/api/settings/ssh-operation", HTTP_POST));
@@ -829,13 +834,13 @@ int main(void) {
other_domains_complete(); network_complete(); display_complete(); broker_complete();
assert(web_server_stop() == ESP_OK);
ssh_fail_at = 0; fresh_registration(); start();
assert(registered_count == 39); ssh_complete();
assert(registered_count == 40); ssh_complete();
assert(web_server_stop() == ESP_OK);
}
puts("PASS all six SSH descriptor/name allocation positions isolate failures and recover after restart");
for (unsigned failure = 5; failure <= 6; ++failure) {
reset(); ssh_fail_at = failure; unregister_fail = true; start();
assert(registered_count == 38 && unregister_calls == 1);
assert(registered_count == 39 && unregister_calls == 1);
assert(route("/api/settings/ssh")->handler == web_ssh_settings_handler);
assert(method_route("/api/settings/ssh-operation", HTTP_GET)->handler == web_ssh_operation_handler);
assert(!method_route("/api/settings/ssh-operation", HTTP_POST));
@@ -845,7 +850,7 @@ int main(void) {
assert(web_server_start() == ESP_ERR_INVALID_STATE && ssl_starts == 1);
ssl_stop_error = ESP_OK; assert(web_server_stop() == ESP_OK);
unregister_fail = false; ssh_fail_at = 0; fresh_registration(); start();
assert(registered_count == 39); ssh_complete();
assert(registered_count == 40); ssh_complete();
assert(web_server_stop() == ESP_OK);
}
puts("PASS failed SSH result unregister leaves reads only and preserves stop-failure ownership/restart");
@@ -927,7 +932,7 @@ static void management_tests(void) {
assert(reserved_gaps == 1 && !unlock_hook && !strcmp(events, "ADSHR"));
assert(s_generation == original + 2 && s_server == SERVER && !s_transitioning);
assert(auth_live && ssl_live && admin_owned && serial_live && idle_owned);
assert(ssl_starts == 2 && ssl_stops == 1 && serial_inits == 1 && registered_count == 39);
assert(ssl_starts == 2 && ssl_stops == 1 && serial_inits == 1 && registered_count == 40);
assert(web_server_stop_current(original) == ESP_ERR_INVALID_STATE);
assert(web_server_stop_current(s_generation) == ESP_OK);
puts("PASS conditional restart reserves stop-to-start gap against canonical and conditional callers");
+3
View File
@@ -241,6 +241,9 @@ int main(void) {
assert(web_stops == 1 && !reboots && !closes);
web_stop_result = ESP_OK;
assert(owner_perform(&s_slot.token, ADMIN_CONSOLE_DEFER_WEB_STOP, 0) == ESP_OK && web_stops == 2);
reboot_busy = true;
assert(owner_perform(&s_slot.token, ADMIN_SSH_DEFER_REBOOT, 0) == ESP_ERR_INVALID_STATE && !reboots);
reboot_busy = false;
assert(owner_perform(&s_slot.token, ADMIN_SSH_DEFER_REBOOT, 0) == ESP_OK && reboots == 1);
httpd_owner = true;
ok("HTTPS stop/reboot marshal to lifecycle APIs outside HTTPD/locks; stop failure propagates");