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.
127 lines
6.0 KiB
Python
127 lines
6.0 KiB
Python
#!/usr/bin/env python3
|
|
"""Actual canonical stop/reboot handlers with deterministic side-effect doubles."""
|
|
from pathlib import Path
|
|
import subprocess
|
|
import tempfile
|
|
|
|
ROOT = Path(__file__).resolve().parents[2]
|
|
|
|
def function(path, name):
|
|
source = path.read_text()
|
|
start = source.index('static int ' + name + '(')
|
|
return source[start:source.index('\n}', start) + 2]
|
|
|
|
header = '\n'.join(line for line in (ROOT / 'src/admin_ssh_console.h').read_text().splitlines()
|
|
if not line.startswith(('#include', '#pragma once')))
|
|
prelude = r'''
|
|
#include <assert.h>
|
|
#include <stdbool.h>
|
|
#include <stddef.h>
|
|
#include <stdint.h>
|
|
#include <stdio.h>
|
|
#include <string.h>
|
|
typedef int esp_err_t;
|
|
enum { ESP_OK=0, ESP_FAIL=-1, ESP_ERR_TIMEOUT=7 };
|
|
typedef struct { int unused; } user_principal_t;
|
|
'''
|
|
fakes = r'''
|
|
static bool remote, web;
|
|
static unsigned stops, reboots, scheduled, waits, rotations, usages;
|
|
static esp_err_t schedule_result, stop_result;
|
|
static admin_ssh_deferred_action_type_t last_action;
|
|
bool admin_ssh_console_dispatch_is_remote(void) { return remote; }
|
|
bool admin_ssh_console_dispatch_is_web(void) { return remote && web; }
|
|
esp_err_t admin_ssh_console_dispatch_defer(admin_ssh_deferred_action_type_t action, uint32_t argument) {
|
|
assert(remote && !argument); ++scheduled; last_action=action; return schedule_result;
|
|
}
|
|
static const char *esp_err_to_name(esp_err_t error) { (void)error; return "fake"; }
|
|
static esp_err_t web_server_stop(void) { ++stops; return stop_result; }
|
|
static esp_err_t web_server_start(void) { assert(false); return ESP_FAIL; }
|
|
static esp_err_t web_server_clear_counters(void) { assert(false); return ESP_FAIL; }
|
|
static esp_err_t web_serial_transport_clear_counters(void) { assert(false); return ESP_FAIL; }
|
|
static bool reboot_busy;
|
|
static esp_err_t web_firmware_update_reserve_reboot(void) { return reboot_busy ? ESP_FAIL : ESP_OK; }
|
|
static void esp_restart(void) { ++reboots; }
|
|
static void vTaskDelay(unsigned delay) { assert(delay==100); ++waits; }
|
|
#define pdMS_TO_TICKS(ms) (ms)
|
|
static void print_usage(void) { ++usages; }
|
|
static int web_diagnostics_command(const char *action) { assert(!strcmp(action, "show")); return 0; }
|
|
static int performance_command(const char *action) { return strcmp(action, "show") ? 1 : 0; }
|
|
static int show_status(void) { assert(false); return 1; }
|
|
static int show_counters(void) { assert(false); return 1; }
|
|
static int show_certificate(void) { assert(false); return 1; }
|
|
static int rotate_certificate(void) { ++rotations; return 0; }
|
|
static int reset_material(void) { assert(false); return 1; }
|
|
static bool force_is_present(int argc, char **argv, int expected) {
|
|
return argc == expected && !strcmp(argv[expected - 1], "--force");
|
|
}
|
|
'''
|
|
tests = r'''
|
|
int main(void) {
|
|
char *removed[]={"web", "credentials", "show", "--force"};
|
|
for (unsigned origin=0; origin<3; ++origin) {
|
|
remote=origin!=0; web=origin==2;
|
|
removed[2]="show";
|
|
assert(command_web(2,removed)==1);
|
|
assert(command_web(3,removed)==1);
|
|
removed[2]="rotate";
|
|
assert(command_web(3,removed)==1);
|
|
assert(command_web(4,removed)==1);
|
|
}
|
|
assert(usages==12 && !stops && !scheduled && !rotations);
|
|
remote=web=false;
|
|
char *diagnostics[]={"web", "diagnostics", "show"};
|
|
assert(command_web(3, diagnostics)==0 && !stops && !scheduled);
|
|
char *performance[]={"web", "performance", "show", "extra"};
|
|
assert(command_web(3, performance)==0 && !stops && !scheduled);
|
|
assert(command_web(4, performance)==1 && !stops && !scheduled);
|
|
performance[2]="invalid";
|
|
assert(command_web(3, performance)==1 && !stops && !scheduled);
|
|
char *stop[]={"web", "stop"};
|
|
remote=web=true;
|
|
assert(command_web(2,stop)==0 && scheduled==1 && !stops && last_action==ADMIN_CONSOLE_DEFER_WEB_STOP);
|
|
schedule_result=ESP_ERR_TIMEOUT;
|
|
assert(command_web(2,stop)==1 && scheduled==2 && !stops);
|
|
schedule_result=ESP_OK;
|
|
web=false; /* SSH preserves its synchronous HTTPS path. */
|
|
assert(command_web(2,stop)==0 && stops==1 && scheduled==2);
|
|
remote=false;
|
|
assert(command_web(2,stop)==0 && stops==2 && scheduled==2);
|
|
stop_result=ESP_FAIL;
|
|
assert(command_web(2,stop)==1 && stops==3);
|
|
remote=true;
|
|
assert(command_reboot(1,NULL)==0 && scheduled==3 && !reboots && last_action==ADMIN_SSH_DEFER_REBOOT);
|
|
web=true;
|
|
assert(command_reboot(1,NULL)==0 && scheduled==4 && !reboots && last_action==ADMIN_SSH_DEFER_REBOOT);
|
|
schedule_result=ESP_FAIL;
|
|
assert(command_reboot(1,NULL)==1 && scheduled==5 && !reboots);
|
|
assert(command_reboot(2,NULL)==1 && scheduled==5 && !reboots);
|
|
remote=false;
|
|
reboot_busy=true;
|
|
assert(command_reboot(1,NULL)==1 && !reboots && !waits && scheduled==5);
|
|
reboot_busy=false;
|
|
assert(command_reboot(1,NULL)==0 && reboots==1 && waits==1 && scheduled==5);
|
|
char *rotate[]={"web", "certificate", "rotate", "--force", "extra"};
|
|
remote=web=true; schedule_result=ESP_OK;
|
|
assert(command_web(4,rotate)==0 && scheduled==6 && !rotations &&
|
|
last_action==ADMIN_CONSOLE_DEFER_WEB_CERTIFICATE_ROTATE);
|
|
schedule_result=ESP_ERR_TIMEOUT;
|
|
assert(command_web(4,rotate)==1 && scheduled==7 && !rotations);
|
|
assert(command_web(3,rotate)==1 && scheduled==7 && !rotations);
|
|
assert(command_web(5,rotate)==1 && scheduled==7 && !rotations);
|
|
web=false;
|
|
assert(command_web(4,rotate)==0 && rotations==1 && scheduled==7);
|
|
remote=false;
|
|
assert(command_web(4,rotate)==0 && rotations==2 && scheduled==7);
|
|
puts("PASS: canonical WEB stop/certificate deferred, exact force required, SSH/UART unchanged, reboot and queue failure isolation");
|
|
}
|
|
'''
|
|
with tempfile.TemporaryDirectory(prefix='console-lifecycle-') as directory:
|
|
tmp = Path(directory)
|
|
(tmp / 'test.c').write_text(prelude + header + fakes +
|
|
function(ROOT / 'src/web_console.c', 'command_web') +
|
|
function(ROOT / 'src/system_console.c', 'command_reboot') + tests)
|
|
subprocess.run(['cc', '-std=c11', '-Wall', '-Wextra', '-Werror',
|
|
str(tmp / 'test.c'), '-o', str(tmp / 'test')], check=True, timeout=30)
|
|
subprocess.run([str(tmp / 'test')], check=True, timeout=10)
|