Add broker management and writer transfer UI
This commit is contained in:
@@ -31,6 +31,8 @@ typedef int *SemaphoreHandle_t;
|
||||
#define pdMS_TO_TICKS(x) (x)
|
||||
#define CONSOLE_COMPLETION_OUTPUT_CAPACITY 1024U
|
||||
static unsigned lock_depth, ticks, runs, actions;
|
||||
static uint32_t broker_settings_executed;
|
||||
static void web_broker_settings_execute(uint32_t id) { assert(!lock_depth); broker_settings_executed = id; }
|
||||
static uint32_t serial_settings_executed, account_settings_executed, network_settings_executed, display_settings_executed;
|
||||
static void web_display_settings_execute(uint32_t id) { assert(!lock_depth); display_settings_executed = id; }
|
||||
static void web_network_settings_execute(uint32_t id) { assert(!lock_depth); network_settings_executed = id; }
|
||||
|
||||
@@ -379,5 +379,20 @@ int main(void)
|
||||
assert(serial_settings_executed == 41 && network_settings_executed == 42 && account_settings_executed == 43 && display_settings_executed == 44);
|
||||
assert(runs == before_serial + 5 && s_request_queue->capacity == 4);
|
||||
puts("PASS: typed Display IDs share all four unchanged queue slots; full/not-ready admission fails, routing never invokes command runner");
|
||||
assert(admin_ssh_console_submit_broker_settings(0) == ESP_ERR_INVALID_STATE);
|
||||
s_dispatch_ready = false;
|
||||
assert(admin_ssh_console_submit_broker_settings(1) == ESP_ERR_INVALID_STATE);
|
||||
s_dispatch_ready = true; queue_full = true;
|
||||
assert(admin_ssh_console_submit_broker_settings(1) == ESP_ERR_TIMEOUT && queue_send_wait == 0);
|
||||
queue_full = false;
|
||||
assert(admin_ssh_console_submit_broker_settings(51) == ESP_OK);
|
||||
assert(admin_ssh_console_submit_display_settings(52) == ESP_OK);
|
||||
assert(admin_ssh_console_submit_network_settings(53) == ESP_OK);
|
||||
assert(admin_ssh_console_submit_account_settings(54) == ESP_OK);
|
||||
assert(admin_ssh_console_submit_broker_settings(55) == ESP_ERR_TIMEOUT);
|
||||
pump(worker_task);
|
||||
assert(broker_settings_executed == 51 && display_settings_executed == 52 && network_settings_executed == 53 && account_settings_executed == 54);
|
||||
assert(runs == before_serial + 5 && s_request_queue->capacity == 4);
|
||||
puts("PASS: Broker typed IDs, not-ready/full queue, routing and unchanged dispatcher capacity");
|
||||
puts("PASS: admission/identity, two owners, completion contention/reopen, history, queued stale/revoked work, UART dispatch, hidden/disconnected prompts, exit-to-SELF_CLOSE, deferred rejection/drain/close, 5s output backpressure");
|
||||
}
|
||||
|
||||
@@ -150,6 +150,61 @@ int main(void) {
|
||||
comparison(1); comparison(2);
|
||||
assert(allocations == initial_allocations);
|
||||
puts("PASS no post-init broker allocations; 7 diagnostic groups passed");
|
||||
session_broker_management_snapshot_t management;
|
||||
assert(session_broker_get_management_snapshot(NULL) == ESP_ERR_INVALID_ARG);
|
||||
mutex = 1; assert(session_broker_get_management_snapshot(&management) == ESP_ERR_TIMEOUT); mutex = 0;
|
||||
session_broker_client_id_t usb = connect_type(SESSION_BROKER_CLIENT_USB);
|
||||
session_broker_client_id_t ssh = connect_type(SESSION_BROKER_CLIENT_SSH);
|
||||
session_broker_client_id_t web = connect_type(SESSION_BROKER_CLIENT_WEB);
|
||||
assert(session_broker_request_writer(usb) == ESP_OK);
|
||||
feed(128);
|
||||
assert(session_broker_get_management_snapshot(&management) == ESP_OK);
|
||||
assert(management.count == 3 && management.writer_id == usb && management.clients[0].pending == 128);
|
||||
uint32_t generation = management.generation;
|
||||
before = global();
|
||||
assert(session_broker_assign_writer_current(0, generation) == ESP_ERR_INVALID_ARG);
|
||||
assert(session_broker_assign_writer_current(ssh, 0) == ESP_ERR_INVALID_ARG);
|
||||
assert(session_broker_get_management_snapshot(&management) == ESP_OK && management.generation == generation);
|
||||
assert(global().latest_event_sequence == before.latest_event_sequence && snapshot(usb).output_bytes_pending == 128);
|
||||
assert(session_broker_assign_writer_current(ssh, generation) == ESP_OK);
|
||||
assert(global().writer_id == ssh && !snapshot(usb).is_writer && snapshot(ssh).is_writer);
|
||||
before = global();
|
||||
assert(session_broker_assign_writer_current(web, generation) == ESP_ERR_INVALID_STATE);
|
||||
assert(global().writer_id == ssh && global().latest_event_sequence == before.latest_event_sequence);
|
||||
assert(session_broker_get_management_snapshot(&management) == ESP_OK); generation = management.generation;
|
||||
assert(session_broker_release_writer(ssh) == ESP_OK && session_broker_request_writer(ssh) == ESP_OK);
|
||||
assert(session_broker_assign_writer_current(web, generation) == ESP_ERR_INVALID_STATE);
|
||||
assert(session_broker_get_management_snapshot(&management) == ESP_OK); generation = management.generation;
|
||||
assert(session_broker_disconnect(web) == ESP_OK);
|
||||
session_broker_client_id_t reused = connect_type(SESSION_BROKER_CLIENT_WEB);
|
||||
assert(reused != web && (reused & 7) == (web & 7));
|
||||
before = global();
|
||||
assert(session_broker_assign_writer_current(web, generation) == ESP_ERR_NOT_FOUND);
|
||||
assert(global().writer_id == ssh && global().latest_event_sequence == before.latest_event_sequence);
|
||||
assert(session_broker_clear_counters() == ESP_OK);
|
||||
assert(session_broker_get_management_snapshot(&management) == ESP_OK && management.generation == generation);
|
||||
assert(session_broker_assign_writer_current(reused, generation) == ESP_OK);
|
||||
assert(session_broker_get_management_snapshot(&management) == ESP_OK); generation = management.generation;
|
||||
assert(session_broker_force_release_writer(reused) == ESP_OK);
|
||||
assert(session_broker_assign_writer_current(usb, generation) == ESP_ERR_INVALID_STATE);
|
||||
assert(session_broker_request_writer(usb) == ESP_OK);
|
||||
assert(session_broker_get_management_snapshot(&management) == ESP_OK); generation = management.generation;
|
||||
assert(session_broker_disconnect(usb) == ESP_OK);
|
||||
assert(session_broker_assign_writer_current(ssh, generation) == ESP_ERR_INVALID_STATE);
|
||||
puts("PASS Broker management: atomic nonconsuming snapshot, one writer, USB/SSH/Web interleavings, stale target/reuse/ABA and counter-clear fencing");
|
||||
s_writer_generation = UINT32_MAX - 1;
|
||||
assert(session_broker_request_writer(ssh) == ESP_OK && s_writer_generation == UINT32_MAX);
|
||||
assert(session_broker_assign_writer_current(reused, UINT32_MAX) == ESP_ERR_INVALID_STATE);
|
||||
assert(session_broker_force_writer(reused) == ESP_OK && s_writer_generation == UINT32_MAX);
|
||||
disconnect_all();
|
||||
for (size_t i = 0; i < SESSION_BROKER_MAX_CLIENTS; ++i) s_slots[i].generation = SESSION_BROKER_MAX_GENERATION;
|
||||
s_slots[7].generation--;
|
||||
session_broker_client_id_t last = connect_type(SESSION_BROKER_CLIENT_USB);
|
||||
assert(last == UINT32_MAX);
|
||||
assert(session_broker_disconnect(last) == ESP_OK);
|
||||
assert(session_broker_connect(SESSION_BROKER_CLIENT_USB, "exhausted", &last) == ESP_ERR_NO_MEM);
|
||||
assert(!global().connected_clients && !global().writer_id);
|
||||
puts("PASS Broker wrap: saturated confirmation rejects, ordinary recovery remains; all 29-bit client generations retire without reuse");
|
||||
cleanup_allocations();
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -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) != 27:
|
||||
raise RuntimeError('Review URI extraction: expected 25 descriptors and two tables')
|
||||
if len(uri_tables) != 30:
|
||||
raise RuntimeError('Review URI extraction: expected 28 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()
|
||||
@@ -97,7 +97,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[32];
|
||||
static const httpd_uri_t *registered[36];
|
||||
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; }
|
||||
@@ -154,6 +154,26 @@ static esp_err_t display_register(httpd_handle_t s, const httpd_uri_t *uri) {
|
||||
registered[registered_count++] = uri;
|
||||
return ESP_OK;
|
||||
}
|
||||
HANDLER(web_broker_settings_handler) HANDLER(web_broker_operation_handler)
|
||||
static unsigned broker_calls, broker_allocations, broker_fail_at;
|
||||
static esp_err_t broker_register(httpd_handle_t s, const httpd_uri_t *uri) {
|
||||
assert(s == SERVER && auth_live && ssl_live && !locked);
|
||||
assert(!uri->is_websocket && !uri->handle_ws_control_frames && !uri->user_ctx);
|
||||
++broker_calls;
|
||||
if (broker_calls == 1) {
|
||||
assert(!strcmp(uri->uri, "/api/settings/broker") && uri->method == HTTP_GET);
|
||||
assert(uri->handler == web_broker_settings_handler);
|
||||
} else {
|
||||
assert(!strcmp(uri->uri, "/api/settings/broker-operation"));
|
||||
assert(uri->method == (broker_calls == 2 ? HTTP_GET : HTTP_POST));
|
||||
assert(uri->handler == web_broker_operation_handler && broker_calls <= 3);
|
||||
}
|
||||
/* Model the adapter's staged descriptor/name allocations, before publication. */
|
||||
for (unsigned allocation = 0; allocation < 2; ++allocation)
|
||||
if (++broker_allocations == broker_fail_at) return ESP_ERR_NO_MEM;
|
||||
registered[registered_count++] = uri;
|
||||
return ESP_OK;
|
||||
}
|
||||
static unsigned network_calls, network_allocations, network_fail_at;
|
||||
static esp_err_t network_register(httpd_handle_t s, const httpd_uri_t *uri) {
|
||||
assert(s == SERVER && auth_live && ssl_live && !locked);
|
||||
@@ -189,7 +209,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 == 30 && config->port_secure == 443);
|
||||
assert(config->httpd.max_uri_handlers == 33 && 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);
|
||||
@@ -217,7 +237,7 @@ static esp_err_t httpd_register_uri_handler(httpd_handle_t s, const httpd_uri_t
|
||||
assert(serial_init_error != ESP_OK || serial_live);
|
||||
} else assert(registration_calls < 14);
|
||||
esp_err_t error = register_one(s);
|
||||
if (error == ESP_OK) { assert(registered_count < 32); registered[registered_count++] = uri; }
|
||||
if (error == ESP_OK) { assert(registered_count < 36); registered[registered_count++] = uri; }
|
||||
return error;
|
||||
}
|
||||
static esp_err_t account_register(httpd_handle_t s, const httpd_uri_t *uri) {
|
||||
@@ -227,6 +247,7 @@ static esp_err_t account_register(httpd_handle_t s, const httpd_uri_t *uri) {
|
||||
}
|
||||
static esp_err_t web_httpd_register_optional_get(httpd_handle_t s, const httpd_uri_t *uri) {
|
||||
assert(uri->method == HTTP_GET);
|
||||
if (uri->handler == web_broker_settings_handler || uri->handler == web_broker_operation_handler) return broker_register(s, uri);
|
||||
if (uri->handler == web_display_settings_handler || uri->handler == web_display_operation_handler) return display_register(s, uri);
|
||||
if (uri->handler == web_network_snapshot_handler || uri->handler == web_network_operation_handler)
|
||||
return network_register(s, uri);
|
||||
@@ -235,6 +256,7 @@ static esp_err_t web_httpd_register_optional_get(httpd_handle_t s, const httpd_u
|
||||
return httpd_register_uri_handler(s, uri);
|
||||
}
|
||||
static esp_err_t web_httpd_register_optional(httpd_handle_t s, const httpd_uri_t *uri) {
|
||||
if (uri->handler == web_broker_settings_handler || uri->handler == web_broker_operation_handler) return broker_register(s, uri);
|
||||
if (uri->handler == web_display_operation_handler) return display_register(s, uri);
|
||||
if (uri->handler == web_network_operation_handler) return network_register(s, uri);
|
||||
if (uri->handler == web_account_keys_handler) {
|
||||
@@ -262,7 +284,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) ||
|
||||
((!strcmp(uri, "/api/settings/serial-operation") || !strcmp(uri, "/api/settings/account-operation") || !strcmp(uri, "/api/settings/network-operation") || !strcmp(uri, "/api/settings/display-operation")) && method == HTTP_GET));
|
||||
((!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")) && method == HTTP_GET));
|
||||
++unregister_calls;
|
||||
for (unsigned i = 0; i < registered_count; ++i) {
|
||||
if (!strcmp(registered[i]->uri, uri) && registered[i]->method == method) {
|
||||
@@ -332,6 +354,7 @@ static void reset(void) {
|
||||
operation_calls = operation_fail_at = 0;
|
||||
network_calls = network_allocations = network_fail_at = 0;
|
||||
display_calls = display_allocations = display_fail_at = 0;
|
||||
broker_calls = broker_allocations = broker_fail_at = 0;
|
||||
account_calls = account_fail_at = generation_calls = keys_calls = 0;
|
||||
generation_fail = keys_fail = false;
|
||||
}
|
||||
@@ -339,6 +362,7 @@ static void fresh_registration(void) {
|
||||
registration_calls = registered_count = 0;
|
||||
network_calls = network_allocations = 0;
|
||||
display_calls = display_allocations = 0;
|
||||
broker_calls = broker_allocations = 0;
|
||||
}
|
||||
static void start(void) {
|
||||
assert(web_server_start() == ESP_OK);
|
||||
@@ -368,6 +392,14 @@ static void display_complete(void) {
|
||||
assert(r && r->handler == web_display_operation_handler);
|
||||
}
|
||||
}
|
||||
static void broker_complete(void) {
|
||||
assert(broker_calls == 3 && broker_allocations == 6);
|
||||
assert(route("/api/settings/broker")->handler == web_broker_settings_handler);
|
||||
for (int method = HTTP_GET; method <= HTTP_POST; ++method) {
|
||||
const httpd_uri_t *r = method_route("/api/settings/broker-operation", method);
|
||||
assert(r && r->handler == web_broker_operation_handler);
|
||||
}
|
||||
}
|
||||
static void network_complete(void) {
|
||||
assert(network_calls == 3 && network_allocations == 6);
|
||||
assert(route("/api/settings/network")->handler == web_network_snapshot_handler);
|
||||
@@ -410,7 +442,7 @@ int main(void) {
|
||||
}
|
||||
puts("PASS optional admin init/attach failures do not disable M1 auth or serial attachment");
|
||||
|
||||
reset(); start(); assert(registered_count == 30 && registration_calls == 18 && settings_calls == 1 && operation_calls == 2);
|
||||
reset(); start(); assert(registered_count == 33 && registration_calls == 18 && settings_calls == 1 && operation_calls == 2);
|
||||
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);
|
||||
@@ -464,7 +496,7 @@ int main(void) {
|
||||
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 == 28 && unregister_calls == failure - 17);
|
||||
assert(registered_count == 31 && unregister_calls == failure - 17);
|
||||
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);
|
||||
@@ -473,13 +505,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 == 30 && admin_attaches == 1 && s_counters.starts == 2);
|
||||
assert(registered_count == 33 && 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");
|
||||
|
||||
reset(); registration_fail_at = 18; unregister_fail = true;
|
||||
assert(web_server_start() == ESP_OK && unregister_calls == 1 && registered_count == 29);
|
||||
assert(web_server_start() == ESP_OK && unregister_calls == 1 && registered_count == 32);
|
||||
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");
|
||||
@@ -491,7 +523,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 == 30 && admin_attaches == 1 && web_server_stop() == ESP_OK);
|
||||
assert(registered_count == 33 && 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;
|
||||
@@ -513,7 +545,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 == 29);
|
||||
assert(settings_calls == 1 && registered_count == 32);
|
||||
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);
|
||||
@@ -521,7 +553,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 == 28 && operation_calls == failure && unregister_calls == failure - 1);
|
||||
assert(registered_count == 31 && 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);
|
||||
@@ -529,7 +561,7 @@ 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 ? 27 : 28));
|
||||
assert(account_calls == failure && registered_count == (failure == 1 ? 30 : 31));
|
||||
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(auth_live && serial_live && admin_owned);
|
||||
@@ -537,17 +569,17 @@ int main(void) {
|
||||
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 == 30 && account_calls == 3);
|
||||
assert(registered_count == 33 && account_calls == 3);
|
||||
assert(web_server_stop() == ESP_OK);
|
||||
}
|
||||
reset(); account_calls = 0; account_fail_at = 3; unregister_fail = true; start();
|
||||
assert(registered_count == 29 && auth_live && serial_live && admin_owned);
|
||||
assert(registered_count == 32 && 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 == 29 && account_calls == 3);
|
||||
assert(generation_calls == 1 && registered_count == 32 && 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);
|
||||
@@ -559,12 +591,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 == 30);
|
||||
assert(generation_calls == 2 && registered_count == 33);
|
||||
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 == 29 && account_calls == 3 && generation_calls == 1);
|
||||
assert(keys_calls == 1 && registered_count == 32 && 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);
|
||||
@@ -579,7 +611,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 == 30);
|
||||
assert(keys_calls == 2 && registered_count == 33);
|
||||
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");
|
||||
@@ -604,7 +636,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 ? 27 : 28));
|
||||
assert(registered_count == (failed_route == 1 ? 30 : 31));
|
||||
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));
|
||||
@@ -612,13 +644,13 @@ int main(void) {
|
||||
other_domains_complete();
|
||||
assert(web_server_stop() == ESP_OK);
|
||||
network_fail_at = 0; fresh_registration(); start();
|
||||
assert(registered_count == 30); network_complete();
|
||||
assert(registered_count == 33); 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 == 29 && unregister_calls == 1);
|
||||
assert(registered_count == 32 && 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));
|
||||
@@ -628,7 +660,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 == 30); network_complete();
|
||||
assert(registered_count == 33); network_complete();
|
||||
assert(web_server_stop() == ESP_OK);
|
||||
}
|
||||
puts("PASS failed Network result unregister leaves reads only and preserves stop-failure ownership/restart");
|
||||
@@ -636,7 +668,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 ? 27 : 28));
|
||||
assert(registered_count == (failed_route == 1 ? 30 : 31));
|
||||
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));
|
||||
@@ -644,13 +676,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 == 30); display_complete();
|
||||
assert(registered_count == 33); 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 == 29 && unregister_calls == 1);
|
||||
assert(registered_count == 32 && 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));
|
||||
@@ -660,10 +692,42 @@ 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 == 30); display_complete();
|
||||
assert(registered_count == 33); display_complete();
|
||||
assert(web_server_stop() == ESP_OK);
|
||||
}
|
||||
puts("PASS failed Display result unregister leaves reads only and preserves stop-failure ownership/restart");
|
||||
for (unsigned failure = 1; failure <= 6; ++failure) {
|
||||
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 ? 30 : 31));
|
||||
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));
|
||||
assert(!!method_route("/api/settings/broker", HTTP_GET) == (failed_route != 1));
|
||||
other_domains_complete(); network_complete(); display_complete();
|
||||
assert(web_server_stop() == ESP_OK);
|
||||
broker_fail_at = 0; fresh_registration(); start();
|
||||
assert(registered_count == 33); 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 == 32 && 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));
|
||||
other_domains_complete(); network_complete(); display_complete();
|
||||
ssl_stop_error = ESP_FAIL;
|
||||
assert(web_server_stop() == ESP_FAIL && s_server == SERVER);
|
||||
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 == 33); broker_complete();
|
||||
assert(web_server_stop() == ESP_OK);
|
||||
}
|
||||
puts("PASS failed Broker result unregister leaves reads only and preserves stop-failure ownership/restart");
|
||||
for (unsigned failure = 0; failure < 8; ++failure) {
|
||||
reset();
|
||||
if (failure == 0) settings_fail = true;
|
||||
@@ -671,10 +735,10 @@ int main(void) {
|
||||
else if (failure <= 5) account_fail_at = failure - 2;
|
||||
else if (failure == 6) generation_fail = true;
|
||||
else keys_fail = true;
|
||||
start(); network_complete(); display_complete(); assert(web_server_stop() == ESP_OK);
|
||||
start(); network_complete(); display_complete(); broker_complete(); assert(web_server_stop() == ESP_OK);
|
||||
}
|
||||
puts("PASS every other settings route failure leaves the complete Network domain available");
|
||||
puts("23 lifecycle groups passed (16 required fatal positions, 16 optional routes, Network/Display allocation positions and failed unregister)");
|
||||
puts("25 lifecycle groups passed (16 required fatal positions, 19 optional routes, Network/Display/Broker allocation positions and failed unregister)");
|
||||
return 0;
|
||||
}
|
||||
'''
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
/* Real HTTP policy/store/parser/operation module, deterministic broker owner double.
|
||||
* Canonical lock/lease/ID tests are in session_broker_diagnostics. */
|
||||
#include "../../src/web_broker_settings.c"
|
||||
static bool on_dispatcher, queue_fail;
|
||||
static uint32_t queued_id;
|
||||
static unsigned assignments, snapshots;
|
||||
static esp_err_t broker_error;
|
||||
static session_broker_management_snapshot_t broker_snapshot;
|
||||
esp_err_t session_broker_get_management_snapshot(session_broker_management_snapshot_t *out) {
|
||||
assert(!host_lock_depth && !on_dispatcher); ++snapshots;
|
||||
*out = broker_snapshot; return broker_error;
|
||||
}
|
||||
esp_err_t session_broker_assign_writer_current(session_broker_client_id_t target, uint32_t generation) {
|
||||
assert(on_dispatcher && !host_lock_depth && target == 9 && generation == 7);
|
||||
++assignments; return broker_error;
|
||||
}
|
||||
esp_err_t admin_ssh_console_submit_broker_settings(uint32_t id) {
|
||||
assert(id && !on_dispatcher && !host_lock_depth);
|
||||
if (queue_fail) return ESP_FAIL;
|
||||
queued_id = id; return ESP_OK;
|
||||
}
|
||||
static void operation_begin(const issued_t *identity, const char *body) {
|
||||
begin("/api/settings/broker-operation", body ? HTTP_POST : HTTP_GET, body);
|
||||
same_origin(); if (body) add("Content-Type", "application/json");
|
||||
if (identity) {
|
||||
char cookie[100]; snprintf(cookie, sizeof(cookie), "__Host-sak-session=%s", identity->token);
|
||||
add("Cookie", cookie); if (body) add("X-CSRF-Token", identity->view.csrf);
|
||||
}
|
||||
}
|
||||
static void broker_expect(const char *status, bool snapshot) {
|
||||
unsigned before = assignments;
|
||||
esp_err_t e = snapshot ? web_broker_settings_handler(&req) : web_broker_operation_handler(&req);
|
||||
assert(e == (send_fail || aux.remaining_len ? ESP_FAIL : ESP_OK));
|
||||
if (strcmp(response_status, status)) fprintf(stderr, "expected %s got %s: %s\n", status, response_status, output);
|
||||
assert(!strcmp(response_status, status) && assignments == before);
|
||||
assert(strlen(output) < (snapshot ? 2048 : 96)); zero(scratch, sizeof(scratch));
|
||||
}
|
||||
static void execute(void) { on_dispatcher = true; web_broker_settings_execute(queued_id); on_dispatcher = false; }
|
||||
static const char *assign_body = "{\"action\":\"assign\",\"generation\":7,\"target\":9}";
|
||||
static void submit(const issued_t *who) {
|
||||
operation_begin(who, assign_body); broker_expect("202 Accepted", false); assert(s_operation.state == PENDING);
|
||||
}
|
||||
static void broker_settings_tests(void) {
|
||||
auth_reset(); issued_t admin = mint(&alice), user = mint(&bob), other = mint(&alice);
|
||||
receive_fragment = 64;
|
||||
operation_begin(NULL, assign_body); broker_expect("401 Unauthorized", false);
|
||||
operation_begin(&user, assign_body); broker_expect("403 Forbidden", false);
|
||||
operation_begin(&user, NULL); broker_expect("403 Forbidden", false);
|
||||
for (unsigned mode = 0; mode < 8; ++mode) {
|
||||
operation_begin(&admin, assign_body);
|
||||
if (mode == 0) req.content_len = aux.remaining_len = 257;
|
||||
if (mode == 1) req.uri = "/api/settings/broker-operation?x=1";
|
||||
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) add("Sec-Fetch-Site", "cross-site");
|
||||
(void)web_broker_operation_handler(&req);
|
||||
assert(response_status[0] == '4' && !s_next_id && !assignments);
|
||||
}
|
||||
puts("PASS Broker HTTP admin/cookie/Origin/CSRF, duplicate/query/body/framing bounds");
|
||||
const char *invalid[] = {"{}", "[]", "{\"action\":\"assign\"}",
|
||||
"{\"action\":\"assign\",\"generation\":0,\"target\":9}",
|
||||
"{\"action\":\"assign\",\"generation\":4294967295,\"target\":9}",
|
||||
"{\"action\":\"assign\",\"generation\":7,\"target\":0}",
|
||||
"{\"action\":\"assign\",\"generation\":7,\"target\":4294967296}",
|
||||
"{\"action\":\"assign\",\"generation\":7,\"target\":09}",
|
||||
"{\"action\":\"assign\",\"generation\":7,\"target\":9.0}",
|
||||
"{\"action\":\"assign\",\"generation\":7,\"target\":9e0}",
|
||||
"{\"action\":\"assign\",\"generation\":7,\"target\":-9}",
|
||||
"{\"action\":\"assign\",\"generation\":7,\"generation\":9}",
|
||||
"{\"action\":\"assign\",\"generation\":7,\"target\":9,\"extra\":1}",
|
||||
"{\"action\":\"as\\u0073ign\",\"generation\":7,\"target\":9}"};
|
||||
for (unsigned i = 0; i < sizeof(invalid)/sizeof(*invalid); ++i) {
|
||||
operation_begin(&admin, invalid[i]); broker_expect("400 Bad Request", false);
|
||||
}
|
||||
broker_operation_t parsed = {0};
|
||||
for (size_t n = 0; n < strlen(assign_body); ++n) assert(!parse(assign_body, n, &parsed));
|
||||
assert(parse(assign_body, strlen(assign_body), &parsed));
|
||||
assert(!parse(assign_body, strlen(assign_body)+1, &parsed));
|
||||
const char *reordered = " { \"target\":4294967295, \"generation\":4294967294, \"action\":\"assign\" } ";
|
||||
assert(parse(reordered, strlen(reordered), &parsed));
|
||||
receive_fragment = 1; operation_begin(&admin, assign_body); broker_expect("400 Bad Request", false); assert(body_offset == 4); receive_fragment = 64;
|
||||
char full[257]; memset(full, ' ', 256); memcpy(full, assign_body, strlen(assign_body)); full[256] = 0;
|
||||
queue_fail = true; operation_begin(&admin, full); broker_expect("503 Service Unavailable", false); queue_fail = false;
|
||||
assert(body_offset == 256 && s_operation.state == IDLE);
|
||||
puts("PASS Broker strict parser, truncation/order/integer limits, exact 256 bytes and four receives");
|
||||
broker_snapshot.generation = 7; broker_snapshot.writer_id = 8; broker_snapshot.count = 8;
|
||||
for (unsigned i = 0; i < 8; ++i) {
|
||||
session_broker_management_client_t *c = &broker_snapshot.clients[i];
|
||||
c->id = i + 8; c->type = i % 5; memset(c->name, '\"', 23); c->name[23] = 0;
|
||||
c->pending = c->high_water = 4096; c->dropped = UINT64_MAX;
|
||||
}
|
||||
for (unsigned mode = 0; mode < 5; ++mode) {
|
||||
unsigned before = snapshots;
|
||||
operation_begin(mode == 0 ? NULL : mode == 1 ? &user : &admin, NULL); req.uri = "/api/settings/broker";
|
||||
broker_error = mode >= 3 ? ESP_FAIL : ESP_OK;
|
||||
broker_expect(mode == 0 ? "401 Unauthorized" : mode == 1 ? "403 Forbidden" : mode >= 3 ? "503 Service Unavailable" : "200 OK", true);
|
||||
if (mode < 2) assert(snapshots == before);
|
||||
if (mode == 2) assert(strstr(output, "18446744073709551615") && strstr(output, "2222222222222222222222222222222222222222222222"));
|
||||
}
|
||||
broker_error = ESP_OK;
|
||||
puts("PASS Broker bounded eight-row admin snapshot, exact 64-bit strings/safe name encoding and unavailable owner");
|
||||
submit(&admin); uint32_t first = queued_id;
|
||||
operation_begin(&other, assign_body); broker_expect("503 Service Unavailable", false);
|
||||
operation_begin(&other, NULL); broker_expect("200 OK", false); assert(strstr(output, "idle"));
|
||||
execute(); assert(s_operation.state == OK && assignments == 1); zero(&s_operation.principal, sizeof(s_operation.principal));
|
||||
execute(); assert(assignments == 1);
|
||||
esp_err_t failures[] = {ESP_ERR_INVALID_STATE, ESP_ERR_NOT_FOUND, ESP_FAIL};
|
||||
for (unsigned i = 0; i < 3; ++i) { broker_error = failures[i]; submit(&admin); execute(); assert(s_operation.state == (i < 2 ? CONFLICT : FAILED)); }
|
||||
broker_error = ESP_OK;
|
||||
puts("PASS Broker typed dispatcher-only execution, replay suppression, login-isolated results, stale/absent target conflicts");
|
||||
submit(&admin); unsigned before = assignments;
|
||||
web_broker_settings_execute(0); web_broker_settings_execute(first); assert(assignments == before && s_operation.state == PENDING);
|
||||
now += 30000000; execute(); assert(s_operation.state == CANCELLED && assignments == before);
|
||||
submit(&admin); web_session_store_invalidate(admin.view.id); execute(); assert(s_operation.state == CANCELLED);
|
||||
admin = mint(&alice); submit(&admin); db_fail = true; execute(); db_fail = false; assert(s_operation.state == CANCELLED);
|
||||
admin = mint(&alice); submit(&admin); stale_user = alice.user_id; execute(); stale_user = 0; assert(s_operation.state == CANCELLED);
|
||||
admin = mint(&alice); now = admin.view.expires_at_us - 1; submit(&admin); now = admin.view.expires_at_us; execute(); assert(s_operation.state == CANCELLED);
|
||||
admin = mint(&alice); submit(&admin); web_cookie_auth_stop(); assert(web_cookie_auth_start() == ESP_OK); execute(); assert(s_operation.state == CANCELLED);
|
||||
admin = mint(&alice); operation_begin(&admin, NULL); broker_expect("200 OK", false); assert(strstr(output, "idle"));
|
||||
assert(assignments == before);
|
||||
puts("PASS Broker deadline/expiry/revocation/currentness failure and auth stop/restart fencing");
|
||||
send_fail = true; submit(&admin); send_fail = false; execute(); assert(s_operation.state == OK);
|
||||
operation_begin(&admin, NULL); broker_expect("200 OK", false); assert(strstr(output, "ok"));
|
||||
s_next_id = UINT32_MAX; operation_begin(&admin, assign_body); broker_expect("503 Service Unavailable", false);
|
||||
puts("PASS Broker lost acknowledgement retained result and nonwrapping operation IDs");
|
||||
}
|
||||
@@ -59,6 +59,7 @@ admin = "--admin" in sys.argv
|
||||
settings = "--settings" in sys.argv
|
||||
serial_settings = "--serial-settings" in sys.argv
|
||||
accounts = "--accounts" in sys.argv
|
||||
broker = "--broker" in sys.argv
|
||||
display = "--display" in sys.argv
|
||||
if display:
|
||||
HEADERS["nvs_flash.h"] = '#pragma once\n#include "esp_err.h"\nesp_err_t nvs_flash_init(void);\n'
|
||||
@@ -247,6 +248,7 @@ with tempfile.TemporaryDirectory(prefix="web-cookie-auth-") as directory:
|
||||
*(["-DHOST_ACCOUNTS"] if accounts else []),
|
||||
*(["-DHOST_NETWORK"] if network else []),
|
||||
*(["-DHOST_DISPLAY"] if display else []),
|
||||
*(["-DHOST_BROKER"] if broker 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)
|
||||
|
||||
@@ -19,7 +19,7 @@ static struct httpd_data server = {.config.max_resp_headers = 8};
|
||||
static struct sock_db socket_state;
|
||||
static struct resp_hdr response_headers[8];
|
||||
static char scratch[1024], cookie_values[2][200];
|
||||
#ifdef HOST_NETWORK
|
||||
#if defined(HOST_NETWORK) || defined(HOST_BROKER)
|
||||
static char output[2048];
|
||||
#else
|
||||
static char output[1024];
|
||||
@@ -146,6 +146,9 @@ static void auth_reset(void) {
|
||||
#ifdef HOST_DISPLAY
|
||||
#include "display_settings_test.c"
|
||||
#endif
|
||||
#ifdef HOST_BROKER
|
||||
#include "broker_settings_test.c"
|
||||
#endif
|
||||
|
||||
int main(void) {
|
||||
assert(store_tests() == 0); auth_reset();
|
||||
@@ -308,6 +311,9 @@ int main(void) {
|
||||
#endif
|
||||
#ifdef HOST_DISPLAY
|
||||
display_settings_tests();
|
||||
#endif
|
||||
#ifdef HOST_BROKER
|
||||
broker_settings_tests();
|
||||
#endif
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
'use strict';
|
||||
const assert = require('node:assert/strict');
|
||||
module.exports = async ({test, browser, adminBrowser, tick, json, session, failure, deferred, token, html}) => {
|
||||
const path = '/api/settings/broker', op = path + '-operation';
|
||||
const row = (id, type = 1) => ({id, type, name_hex:'3c696d673e', pending:12, high_water:4096, dropped:'18446744073709551615'});
|
||||
const fixture = (extra = {}) => ({generation:7, writer:8, clients:[row(8),row(9,3)], ...extra});
|
||||
const reply = (state='pending', id=42, status=200, action='assign') => new Response(JSON.stringify({id,action,state}), {status});
|
||||
const n = (b,id) => b.nodes['broker-'+id];
|
||||
const posts = b => b.calls.filter(c => c.url === op && c.method === 'POST');
|
||||
const gets = b => b.calls.filter(c => c.url === op && c.method === 'GET');
|
||||
const reads = b => b.calls.filter(c => c.url === path);
|
||||
async function open(value=fixture()) {
|
||||
const b=await adminBrowser(); b.click('select-settings'); await tick();
|
||||
b.queues[path].push(json(value)); b.click('settings-broker'); await tick(); return b;
|
||||
}
|
||||
function select(b,id=9) { n(b,'target').value=String(id); n(b,'target').change(); }
|
||||
async function submit(b) { select(b); b.window.confirm=()=>true; b.queues[op].push(reply('pending',42,202)); b.click('broker-assign'); await tick(); }
|
||||
await test('Broker admin-only full page, label/value style, bounded safe rows; view/selection do not mutate or close either terminal', async()=>{
|
||||
for(const id of ['settings-broker','broker-values','broker-target','broker-assign','broker-refresh','broker-result']) assert.ok(html.includes('id="'+id+'"'));
|
||||
const u=browser(); u.start(); await tick(); u.click('settings-broker'); await tick(); assert.equal(reads(u).length,0);
|
||||
const b=await open(); assert.equal(b.nodes['broker-settings'].hidden,false); assert.equal(b.nodes['display-settings'].hidden,true);
|
||||
assert.match(n(b,'values').textContent,/<img>/); assert.match(n(b,'values').textContent,/18446744073709551615/);
|
||||
select(b); assert.equal(posts(b).length,0); assert.equal(n(b,'assign').disabled,false);
|
||||
const count=b.calls.length; b.click('settings-broker'); await tick(); assert.equal(b.calls.length,count);
|
||||
for(let i=0;i<2;++i){ b.sockets[i].emit('message',{data:Uint8Array.of(0,255,i).buffer}); assert.deepEqual(b.terminals[i].writes.at(-1),[0,255,i]); b.terminals[i].input('blocked'); assert.equal(b.sockets[i].sent.length,0); }
|
||||
assert.ok(b.sockets.every(s=>!s.closed));
|
||||
});
|
||||
await test('Broker snapshot rejects malformed/missing/duplicate/oversized rows and exhausted generation disables assignments',async()=>{
|
||||
for(const v of [null,{},fixture({generation:0}),fixture({extra:1}),fixture({writer:123}),fixture({clients:[row(8),row(8)]}),fixture({clients:[{...row(8),name_hex:'zz'}]}),fixture({clients:[{...row(8),dropped:'18446744073709551616'}]}),fixture({clients:[{...row(8),pending:4097}]}),fixture({clients:Array.from({length:9},(_,i)=>row(i+8))})]){
|
||||
const b=await open(v); select(b); assert.ok(n(b,'assign').disabled); assert.match(n(b,'detail').textContent,/unavailable|invalid/);
|
||||
}
|
||||
const b=await open(fixture({generation:4294967295})); select(b); assert.ok(n(b,'assign').disabled);
|
||||
const empty=await open(fixture({writer:0,clients:[]})); assert.match(n(empty,'detail').textContent,/0 connected/);
|
||||
const c=await open(); c.queues[path].push(new Response('x'.repeat(2049))); c.click('broker-refresh'); await tick(); select(c); assert.ok(n(c,'assign').disabled);
|
||||
});
|
||||
await test('Broker explicit confirmation captures target and lease generation, cancel/current-writer/selection never POST; refresh clears selection',async()=>{
|
||||
const b=await open(); select(b,8); b.click('broker-assign'); await tick(); assert.equal(posts(b).length,0);
|
||||
select(b); let prompt=''; b.window.confirm=text=>{prompt=text;return false;}; b.click('broker-assign'); await tick(); assert.equal(posts(b).length,0); assert.match(prompt,/9 \/ SSH/); assert.match(prompt,/Current writer: 8/);
|
||||
await submit(b); assert.deepEqual(JSON.parse(posts(b)[0].body),{action:'assign',generation:7,target:9}); assert.equal(posts(b)[0].headers['X-CSRF-Token'],token);
|
||||
b.click('broker-assign'); await tick(); assert.equal(posts(b).length,1);
|
||||
b.queues[op].push(reply('ok')); b.queues[path].push(json(fixture({generation:8,writer:9}))); b.fire(1000); await tick();
|
||||
assert.equal(n(b,'target').value,''); assert.ok(n(b,'assign').disabled); assert.match(n(b,'operation-detail').textContent,/completed/); assert.equal(posts(b).length,1);
|
||||
});
|
||||
await test('Broker stale/failed/cancelled completion refreshes without retry; stale target removal clears explicit selection',async()=>{
|
||||
for(const state of ['conflict','failed','cancelled']) {
|
||||
const b=await open(); await submit(b); b.queues[op].push(reply(state)); b.queues[path].push(json(fixture({clients:[row(8)]}))); b.fire(1000); await tick();
|
||||
assert.equal(posts(b).length,1); assert.equal(reads(b).length,2); assert.equal(n(b,'target').value,''); assert.ok(n(b,'assign').disabled);
|
||||
if(state==='conflict') assert.match(n(b,'operation-detail').textContent,/No lease change/);
|
||||
}
|
||||
});
|
||||
await test('Broker polling bounded to ten requests/fifteen seconds, including stalled session checks',async()=>{
|
||||
const b=await open(); await submit(b);
|
||||
for(let i=0;i<10;++i){b.queues[op].push(reply());b.fire(1000);await tick();}
|
||||
assert.equal(gets(b).length,10);assert.equal(posts(b).length,1);assert.match(n(b,'operation-detail').textContent,/Automatic checking stopped/);
|
||||
const c=await open();await submit(c);const d=deferred();c.queues['/api/session'].push(d.promise);c.fire(1000);await tick();c.elapse(15000);c.fire(15000);await tick();d.resolve(session({role:'admin',username:'alice'}));await tick();assert.equal(gets(c).length,0);
|
||||
});
|
||||
await test('Broker lost acknowledgement/replaced result/invalid state preserve uncertainty and never replay',async()=>{
|
||||
const b=await open();select(b);b.window.confirm=()=>true;b.queues[op].push(()=>{throw Error('lost');});b.click('broker-assign');await tick();
|
||||
assert.match(n(b,'operation-detail').textContent,/unknown/);assert.ok(n(b,'assign').disabled);
|
||||
b.queues[op].push(reply('ok'));b.queues[path].push(json(fixture()));b.click('broker-result');await tick();assert.match(n(b,'operation-detail').textContent,/acknowledgement was lost/);
|
||||
for(const result of [reply('pending',43),reply('loaded_defaults'),reply('ok',42,202),reply('ok',42,200,'reset'),reply('idle',42)]){
|
||||
const c=await open();await submit(c);c.queues[op].push(result);c.fire(1000);await tick();assert.equal(posts(c).length,1);assert.match(n(c,'operation-detail').textContent,/unknown/);assert.ok(![...c.timers.values()].some(t=>t.ms===1000||t.ms===15000));
|
||||
}
|
||||
});
|
||||
await test('Broker navigation aborts stale read/POST/results, no automatic resubmit on return; endpoint401 closes both terminals',async()=>{
|
||||
for(const stage of ['read','post','result']){
|
||||
const b=await open();const d=deferred();
|
||||
if(stage==='read'){b.queues[path].push(d.promise);b.click('broker-refresh');}
|
||||
else if(stage==='post'){select(b);b.window.confirm=()=>true;b.queues[op].push(d.promise);b.click('broker-assign');}
|
||||
else{await submit(b);b.queues[op].push(d.promise);b.fire(1000);}
|
||||
await tick();b.click('settings-display');await tick();d.resolve(failure(401));await tick();assert.deepEqual(b.redirects,[]);assert.ok(b.sockets.every(s=>!s.closed));
|
||||
const count=posts(b).length;b.queues[path].push(json(fixture()));b.click('settings-broker');await tick();assert.equal(posts(b).length,count);assert.equal(n(b,'target').value,'');
|
||||
}
|
||||
const b=await open();b.queues[path].push(failure(401));b.click('broker-refresh');await tick();assert.deepEqual(b.redirects,['/login']);assert.ok(b.sockets.every(s=>s.closed));
|
||||
});
|
||||
await test('Broker pagehide/expiry/logout clear selected identities and stop checks without cancellation claims',async()=>{
|
||||
for(const action of ['pagehide','expiry','logout']){
|
||||
const b=await open();await submit(b);
|
||||
if(action==='pagehide')b.emit('pagehide');else if(action==='expiry')b.window.sakSessionExpired();else{b.queues['/api/logout'].push(new Response(null,{status:204}));b.click('sign-out');}
|
||||
await tick();assert.equal(n(b,'target').value,'');assert.equal(n(b,'values').textContent,'');assert.equal(posts(b).length,1);assert.ok(![...b.timers.values()].some(t=>t.ms===1000||t.ms===15000));
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -13,7 +13,7 @@ const deferred = () => { let resolve; const promise = new Promise(r => { resolve
|
||||
const tick = async () => { for (let i = 0; i < 6; ++i) await new Promise(r => setImmediate(r)); };
|
||||
function browser({onlyLoader = false, withLoader = false, role = 'user', username = '<img>'} = {}) {
|
||||
const nodes = {}, events = {}, calls = [], redirects = [], timers = new Map(), sockets = [], terminals = [];
|
||||
const queues = {'/api/session': [], '/api/status': [], '/api/ws-ticket': [], '/api/admin/ws-ticket': [], '/api/logout': [], '/api/settings/serial': [], '/api/settings/serial-operation': [], '/api/settings/accounts': [], '/api/settings/account-operation': [], '/api/settings/accounts/generate-password': [], '/api/settings/accounts/keys': [], '/api/settings/network': [], '/api/settings/network-operation': [], '/api/settings/display': [], '/api/settings/display-operation': []};
|
||||
const queues = {'/api/session': [], '/api/status': [], '/api/ws-ticket': [], '/api/admin/ws-ticket': [], '/api/logout': [], '/api/settings/serial': [], '/api/settings/serial-operation': [], '/api/settings/accounts': [], '/api/settings/account-operation': [], '/api/settings/accounts/generate-password': [], '/api/settings/accounts/keys': [], '/api/settings/network': [], '/api/settings/network-operation': [], '/api/settings/display': [], '/api/settings/display-operation': [], '/api/settings/broker': [], '/api/settings/broker-operation': []};
|
||||
const fits = [];
|
||||
let serial = 0, now = Date.now();
|
||||
class Clock extends Date { static now() { return now; } }
|
||||
@@ -1252,5 +1252,6 @@ async function test(name, fn) { await fn(); ++passed; console.log('PASS JS:', na
|
||||
});
|
||||
await require('./network.cjs')({test, browser, adminBrowser, tick, json, session, failure, deferred, token, html});
|
||||
await require('./display.cjs')({test, browser, adminBrowser, tick, json, session, failure, deferred, token, html});
|
||||
await require('./broker.cjs')({test, browser, adminBrowser, tick, json, session, failure, deferred, token, html});
|
||||
console.log(`PASS ${passed} browser behavior groups (production C-rendered JS)`);
|
||||
})().catch(error => { console.error(error); process.exitCode = 1; });
|
||||
|
||||
@@ -45,10 +45,10 @@ def check_layout(html):
|
||||
if cls in classes(node):
|
||||
return node
|
||||
raise AssertionError(cls)
|
||||
for ident in ('settings-values', 'accounts-list', 'account-keys-list', 'network-summary', 'display-values'):
|
||||
for ident in ('settings-values', 'accounts-list', 'account-keys-list', 'network-summary', 'display-values', 'broker-values'):
|
||||
assert ids[ident]['tag'] == 'dl'
|
||||
assert 'settings-values' in classes(ids[ident])
|
||||
for ident in ('serial-settings-content', 'account-settings', 'network-settings', 'display-settings'):
|
||||
for ident in ('serial-settings-content', 'account-settings', 'network-settings', 'display-settings', 'broker-settings'):
|
||||
nodes = list(descendants(ids[ident]))
|
||||
assert not any(n['tag'] == 'pre' for n in nodes)
|
||||
assert all('connection-detail' in classes(n) for n in nodes if n['tag'] == 'p')
|
||||
@@ -59,9 +59,9 @@ def check_layout(html):
|
||||
ancestor(n, 'settings-edit')
|
||||
except AssertionError:
|
||||
ancestor(n, 'serial-edit')
|
||||
for ident in ('refresh-settings', 'refresh-accounts', 'network-refresh', 'display-refresh'):
|
||||
for ident in ('refresh-settings', 'refresh-accounts', 'network-refresh', 'display-refresh', 'broker-refresh'):
|
||||
assert ids[ident]['text'] == 'Refresh'
|
||||
for ident in ('serial-result', 'account-result', 'network-result', 'display-result'):
|
||||
for ident in ('serial-result', 'account-result', 'network-result', 'display-result', 'broker-result'):
|
||||
assert ids[ident]['text'] == 'Check Operation Result'
|
||||
for ident in ('network-boot', 'network-enabled', 'account-password-saved'):
|
||||
assert 'settings-check' in classes(ids[ident]['parent'])
|
||||
@@ -90,7 +90,7 @@ def check_layout(html):
|
||||
):
|
||||
assert rule in css, rule
|
||||
assert '.settings-edit textarea{font:inherit;width:100%;min-width:0;' in css
|
||||
print('PASS HTML layout: parsed structure, shared styles, labels, wrapping, checkbox sizing and action order across all four settings views')
|
||||
print('PASS HTML layout: parsed structure, shared styles, labels, wrapping, checkbox sizing and action order across all five settings views')
|
||||
|
||||
|
||||
def check_browser_layout(html, tmp, executable):
|
||||
|
||||
Reference in New Issue
Block a user