feat: introduce the network state model and secure persistent storage

This commit is contained in:
2026-09-01 21:04:23 +03:00
parent 4ea5280f61
commit bad0803a6a
10 changed files with 312 additions and 8 deletions
+165 -5
View File
@@ -60,7 +60,7 @@ Evidence should include the exact test scenario, relevant logs, reset reasons, r
## Milestone 000 — Identify the board and lock the technical baseline
**Status:** `DONE`
**Status:** `DONE`
**Depends on:** none
### Objective
@@ -908,7 +908,7 @@ If all criteria pass, set this milestone to `DONE` and append its execution reco
- Next action: Milestone 018 is READY. Do not start Milestone 019.
# Milestone 018 — Player Names Throughout the Game UI
## Milestone 018 — Player Names Throughout the Game UI
**Status:** DONE
**Depends on:** Milestone 017
@@ -1124,7 +1124,7 @@ At completion, report:
- Issues or deviations: No browser backend is available in this environment, so mobile, tablet, and laptop name-layout checks were verified from the responsive CSS rules rather than captured live. No device upload was performed.
- Next action: Milestone 019 is not started.
# Milestone 019 — Amendment: Distinct Ship-Class Silhouettes and Red Sunk Markers
## Milestone 019 — Amendment: Distinct Ship-Class Silhouettes and Red Sunk Markers
**Status:** DONE
**Depends on:** Milestone 018
@@ -1338,7 +1338,7 @@ After implementation, report:
- Next action: Milestone 020 is not started.
# Milestone 020 — Amendment: Recognizable Early-20th-Century Warship Silhouettes
## Milestone 020 — Amendment: Recognizable Early-20th-Century Warship Silhouettes
**Status:** DONE
**Depends on:** Milestone 019
@@ -2124,7 +2124,7 @@ When all criteria pass, set Milestone 025 to `DONE`, append its execution record
## Milestone 026 — Validate Web Audio engagement, compatibility, and long-run stability on real devices
**Status:** `READY`
**Status:** `DONE`
**Depends on:** Milestone 025
### Objective
@@ -2209,3 +2209,163 @@ Do not record children, collect identifying data, or conduct unsupervised testin
### Completion action
When all criteria pass, set Milestone 026 to `DONE` and append its execution record. Add later milestones only after Milestone 026 without renumbering existing milestones.
### Execution record
- Date: 2026-09-01
- Result: PASS.
- Evidence: The user completed the required real-device validation with no issues found: the Android phone Chromium browser, Android tablet/second device, iPhone/iPad Safari when available, and laptop browser matrix; portrait and landscape; sound on/muted/quiet/normal/loud/reduced-intensity; WebSocket and HTTP polling fallback; explicit activation, preference persistence, immediate mute, target/shot/result/combo/recovery cues, background/foreground handling, reset behavior, spectator privacy, and silent fallback. The required 30-shot anti-repeat observation, 20 representative sound-enabled games, 60-minute mixed session, and adult-proxy child-engagement/safety review also passed without browser errors, stuck audio, audio-node/timer growth, transport regression, ESP32 instability, excessive loudness, frightening cues, or hidden-information leakage.
- Measurements: Final validated browser assets remain 27,023 B gzip, within the 250,000 B LittleFS budget. Firmware remains 1,020,994 / 2,097,152 B flash (48.7%) and 39,604 / 327,680 B RAM (12.1%).
- Next action: Milestone 027 remains blocked until explicitly requested.
## Milestone 027 — Introduce the network state model and secure persistent storage
**Status:** `DONE`
**Depends on:** Milestone 026
### Objective
Create a testable foundation for switching between an external network and the fallback access point without coupling game logic to network callbacks.
### Work
- Define explicit states and transitions for startup without configuration, connection to the saved network, the 30-second connection window, access-point operation, validation of new credentials, successful switching, and reconnection after network loss.
- Separate network control and credential persistence into dedicated components with bounded inputs.
- Store one SSID/password pair in persistent memory using the facilities of the current ESP32 stack.
- Implement loading, atomic replacement, and deletion of credentials; treat a corrupt or incomplete record as absent configuration.
- Exclude the password from logs, API responses, and diagnostic structures.
- Use non-blocking connection timing so that the game loop, HTTP/WebSocket handling, and watchdog are not stalled for 30 seconds.
- Add automated tests for state transitions, timeouts, corrupt records, successful persistence, and deletion.
### Acceptance criteria
- The device selects fallback mode when no valid record exists.
- With a valid record, the device starts a connection attempt and enables fallback after 30 seconds without success.
- Loss of an established connection starts reconnection and the same 30-second fallback transition.
- Failed validation of new credentials does not replace the previously saved configuration.
- After successful validation, persistent storage contains only one current network profile.
- Automated tests verify state transitions without a real 30-second wait and without depending on ESP32 radio hardware.
- No test output, log, or API response exposes the password in plaintext.
### Completion action
When all criteria pass, set this milestone to `DONE` and change Milestone 006 to `READY`.
### Execution record
- Date: 2026-09-01
- Result: PASS.
- Evidence: Added a bounded, host-testable network state model for fallback, saved-network connection, 30-second timeout, validation, successful replacement, and reconnect transitions. Added versioned and checksummed credential records, an ESP-IDF NVS single-record store with committed replacement/deletion, and a host memory backend that exercises load, corruption rejection, replacement, and deletion without radio hardware. Credentials are not logged or included in an API/diagnostic structure.
- Verification: `make -C test/host run` passed all 11 host suites, including `test_network_foundation`; `pio run -e esp32-c6-devkitm-1` passed with the three new firmware sources compiled.
- Next action: Milestone 028 remains blocked and was not started.
---
## Milestone 028 — Implement the open fallback access point and captive portal
**Status:** `BLOCKED`
**Depends on:** Milestone 027
### Objective
Give users access to the game and external-network configuration through the open `Battleship-open` network whenever the saved external connection is unavailable.
### Work
- Start an open access point with the exact SSID `Battleship-open` only in fallback mode.
- Implement DNS redirection and captive-portal detection responses for supported phones and tablets.
- Keep the configuration page available through the access point's direct local address when the captive-portal window does not open automatically.
- Add a dedicated network configuration screen served entirely by the ESP32 without a CDN or internet dependency.
- Display Wi-Fi scan results and support manual entry of a hidden SSID.
- Bound SSID length, password length, and request-body size; handle empty, oversized, and incorrectly encoded values safely.
- Do not include the password in redisplayed data, API responses, or page state.
- Keep the game HTTP/WebSocket interface available through the fallback network.
### Acceptance criteria
- A phone can discover the open network with the exact name `Battleship-open` and connect without a password.
- The captive portal opens the local configuration page on the tested target devices; the direct local address remains a working fallback for the client.
- The network list can be refreshed without stalling the server, and a hidden SSID can be entered manually.
- Invalid and oversized requests are rejected without a restart or a persistent memory leak.
- Two players and the spectator capacity established by Milestone 004 can open the game interface through the access point within the confirmed resource limits.
- The password does not appear in HTML, JSON, WebSocket messages, or logs.
### Completion action
When all criteria pass, set this milestone to `DONE` and change Milestone 007 to `READY`.
---
## Milestone 029 — Validate credentials, persist them, and switch without rebooting
**Status:** `BLOCKED`
**Depends on:** Milestone 028
### Objective
Complete the configuration flow: validate a new network, save it only after success, and switch to it without rebooting the device.
### Work
- Add operations for retrieving network status, starting a scan, validating new credentials, and deleting saved configuration.
- Make the configuration screen available without authorization through both the fallback access point and the regular web interface.
- During validation, preserve a usable path back to the configuration page until the result is known.
- After a successful connection, persist the profile, report success to the browser, disable the fallback access point, and continue on the external network without rebooting.
- After a failed attempt, preserve the saved profile and keep a configuration path available for another attempt.
- After deleting the profile, enter fallback mode and ensure the deleted values are not restored after reboot.
- Prevent conflicting scan, validation, deletion, and mode-switch operations from running concurrently.
- Add clear Russian-language UI states for scanning, connecting, success, failure, and retry.
### Acceptance criteria
- Valid credentials are verified, saved, and used without rebooting the ESP32.
- New credentials are absent from persistent storage until validation succeeds.
- An incorrect password or unavailable SSID does not destroy the previously working configuration.
- The browser receives confirmation before the fallback access point is disabled, along with instructions for reopening the game on the external network.
- Configuration and deletion work through both agreed paths and require no token or other authorization.
- Repeated and concurrent requests lead to a deterministic state without corrupting storage or blocking the server.
- After configuration deletion and a cold reboot, the device starts `Battleship-open`.
### Accepted security limitation
Any client with network access to the device can change or delete its Wi-Fi configuration without authorization. User documentation must state this agreed behavior and explain that the open access point and local HTTP connection do not protect the submitted password from a nearby network observer.
### Completion action
When all criteria pass, set this milestone to `DONE` and change Milestone 008 to `READY`.
---
## Milestone 030 — Verify recovery, game continuity, and resource stability
**Status:** `BLOCKED`
**Depends on:** Milestone 029
### Objective
Prove on the target ESP32-C6 Mini that configuration survives reboot, fallback works during failures, and the current game survives network-mode transitions.
### Work
- Test a cold boot both without configuration and with a previously saved working configuration.
- During an active game, disable the external network, verify reconnection attempts, and confirm that `Battleship-open` appears after 30 seconds.
- Reconnect clients through the fallback access point and continue the same game without resetting its in-memory state.
- Configure a working external network through the fallback interface, wait for the no-reboot transition, and continue the same game after clients reconnect.
- Test configuration changes and deletion through the regular web interface.
- Test an incorrect password, hidden SSID, unavailable DHCP, disappearing network, corrupt stored record, and repeated mode transitions.
- Repeat the Milestone 004 load scenario with the network state machine, DNS, and captive portal enabled; measure firmware size, minimum free heap, response time, reset reasons, and memory trends.
- Update user documentation for first boot, configuration, the direct access address, network changes, credential deletion, fallback behavior, and the accepted security limitations.
### Acceptance criteria
- After a cold reboot, the device restores the connection from saved credentials without user action.
- When the saved network is absent or unavailable, `Battleship-open` appears within the agreed time.
- The active game's identifier, version, and state are not reset during transitions between the external network and fallback access point.
- After reconnecting, browsers receive a current snapshot and can continue the game under the existing session rules.
- Twenty cycles of network loss, fallback, and recovery complete without watchdog resets, unexpected reboots, or a persistent decline in free memory.
- Resource measurements remain within the budgets established by Milestone 004, or any deviation is documented and approved separately before changing a budget.
- The instructions allow a new user to configure the device without firmware-embedded Wi-Fi credentials.
### Completion action
Set this milestone to `DONE` only after documented automated and on-device verification and after adding an execution record in the format defined by the main plan.
+8
View File
@@ -0,0 +1,8 @@
#ifndef NETWORK_CREDENTIAL_STORE_H
#define NETWORK_CREDENTIAL_STORE_H
#include <stdbool.h>
#include "network_state.h"
bool network_credential_store_load(network_profile_t *profile);
bool network_credential_store_replace(const network_profile_t *profile);
bool network_credential_store_delete(void);
#endif
+25
View File
@@ -0,0 +1,25 @@
#ifndef NETWORK_CREDENTIALS_H
#define NETWORK_CREDENTIALS_H
#include <stdbool.h>
#include <stdint.h>
#include "network_state.h"
typedef struct { uint32_t magic; uint16_t version; uint16_t reserved; network_profile_t profile; uint32_t checksum; } network_credential_record_t;
typedef bool (*network_credential_read_fn)(void *context, network_credential_record_t *record);
typedef bool (*network_credential_write_fn)(void *context, const network_credential_record_t *record);
typedef bool (*network_credential_delete_fn)(void *context);
typedef struct {
void *context;
network_credential_read_fn read;
network_credential_write_fn write;
network_credential_delete_fn remove;
} network_credential_backend_t;
bool network_credential_encode(const network_profile_t *profile, network_credential_record_t *record);
bool network_credential_decode(const network_credential_record_t *record, network_profile_t *profile);
bool network_credential_load(const network_credential_backend_t *backend, network_profile_t *profile);
bool network_credential_replace(const network_credential_backend_t *backend, const network_profile_t *profile);
bool network_credential_delete(const network_credential_backend_t *backend);
#endif
+21
View File
@@ -0,0 +1,21 @@
#ifndef NETWORK_STATE_H
#define NETWORK_STATE_H
#include <stdbool.h>
#include <stdint.h>
enum { kNetworkSsidBytes = 32, kNetworkPasswordBytes = 63, kNetworkConnectWindowMs = 30000 };
typedef struct { char ssid[kNetworkSsidBytes + 1]; char password[kNetworkPasswordBytes + 1]; } network_profile_t;
typedef enum { NETWORK_STATE_FALLBACK, NETWORK_STATE_CONNECTING, NETWORK_STATE_EXTERNAL, NETWORK_STATE_VALIDATING } network_state_t;
typedef enum { NETWORK_ACTION_NONE, NETWORK_ACTION_CONNECT, NETWORK_ACTION_FALLBACK } network_action_t;
typedef struct { network_state_t state; network_state_t state_before_validation; network_profile_t profile; network_profile_t candidate; uint32_t started_ms; bool has_profile; } network_manager_t;
bool network_profile_valid(const network_profile_t *profile);
network_action_t network_manager_init(network_manager_t *manager, const network_profile_t *profile, uint32_t now_ms);
network_action_t network_manager_tick(network_manager_t *manager, uint32_t now_ms);
network_action_t network_manager_connected(network_manager_t *manager);
network_action_t network_manager_disconnected(network_manager_t *manager, uint32_t now_ms);
bool network_manager_begin_validation(network_manager_t *manager, const network_profile_t *candidate);
network_action_t network_manager_finish_validation(network_manager_t *manager, bool success, uint32_t now_ms);
#endif
+1 -1
View File
@@ -2,7 +2,7 @@
# without default 'CMakeLists.txt' file.
idf_component_register(
SRCS "main.c" "application.c" "command_queue.c" "fleet_generator.c" "game_engine.c" "bot_player.c" "session_manager.c" "game_lifecycle.c" "state_presenter.c" "http_api.c" "sync_service.c"
SRCS "main.c" "application.c" "command_queue.c" "fleet_generator.c" "game_engine.c" "bot_player.c" "session_manager.c" "game_lifecycle.c" "state_presenter.c" "http_api.c" "sync_service.c" "network_state.c" "network_credentials.c" "network_credential_store.c"
INCLUDE_DIRS "../include"
REQUIRES esp_event esp_http_server esp_netif esp_wifi esp_littlefs nvs_flash
)
+9
View File
@@ -0,0 +1,9 @@
#include "network_credential_store.h"
#include "network_credentials.h"
#include "nvs.h"
static const char *const kNamespace = "network";
static const char *const kProfileKey = "profile";
bool network_credential_store_load(network_profile_t *profile) { nvs_handle_t handle; network_credential_record_t record; size_t size = sizeof(record); if (nvs_open(kNamespace, NVS_READONLY, &handle) != ESP_OK) return false; const esp_err_t result = nvs_get_blob(handle, kProfileKey, &record, &size); nvs_close(handle); return result == ESP_OK && size == sizeof(record) && network_credential_decode(&record, profile); }
bool network_credential_store_replace(const network_profile_t *profile) { network_credential_record_t record; nvs_handle_t handle; if (!network_credential_encode(profile, &record) || nvs_open(kNamespace, NVS_READWRITE, &handle) != ESP_OK) return false; const esp_err_t result = nvs_set_blob(handle, kProfileKey, &record, sizeof(record)); const bool ok = result == ESP_OK && nvs_commit(handle) == ESP_OK; nvs_close(handle); return ok; }
bool network_credential_store_delete(void) { nvs_handle_t handle; if (nvs_open(kNamespace, NVS_READWRITE, &handle) != ESP_OK) return false; const esp_err_t result = nvs_erase_key(handle, kProfileKey); const bool ok = (result == ESP_OK || result == ESP_ERR_NVS_NOT_FOUND) && nvs_commit(handle) == ESP_OK; nvs_close(handle); return ok; }
+10
View File
@@ -0,0 +1,10 @@
#include "network_credentials.h"
#include <stddef.h>
#include <string.h>
enum { kCredentialMagic = 0x42534E57U, kCredentialVersion = 1U };
static uint32_t checksum(const uint8_t *bytes, size_t length) { uint32_t value = 2166136261U; for (size_t i = 0; i < length; ++i) value = (value ^ bytes[i]) * 16777619U; return value; }
bool network_credential_encode(const network_profile_t *profile, network_credential_record_t *record) { if (!network_profile_valid(profile) || record == NULL) return false; memset(record, 0, sizeof(*record)); record->magic = kCredentialMagic; record->version = kCredentialVersion; record->profile = *profile; record->checksum = checksum((const uint8_t *)record, offsetof(network_credential_record_t, checksum)); return true; }
bool network_credential_decode(const network_credential_record_t *record, network_profile_t *profile) { if (record == NULL || profile == NULL || record->magic != kCredentialMagic || record->version != kCredentialVersion || record->checksum != checksum((const uint8_t *)record, offsetof(network_credential_record_t, checksum)) || !network_profile_valid(&record->profile)) return false; *profile = record->profile; return true; }
bool network_credential_load(const network_credential_backend_t *backend, network_profile_t *profile) { network_credential_record_t record; return backend != NULL && backend->read != NULL && backend->read(backend->context, &record) && network_credential_decode(&record, profile); }
bool network_credential_replace(const network_credential_backend_t *backend, const network_profile_t *profile) { network_credential_record_t record; return backend != NULL && backend->write != NULL && network_credential_encode(profile, &record) && backend->write(backend->context, &record); }
bool network_credential_delete(const network_credential_backend_t *backend) { return backend != NULL && backend->remove != NULL && backend->remove(backend->context); }
+33
View File
@@ -0,0 +1,33 @@
#include "network_state.h"
#include <string.h>
bool network_profile_valid(const network_profile_t *profile) {
return profile != NULL && profile->ssid[0] != '\0' && memchr(profile->ssid, '\0', sizeof(profile->ssid)) != NULL &&
memchr(profile->password, '\0', sizeof(profile->password)) != NULL;
}
network_action_t network_manager_init(network_manager_t *manager, const network_profile_t *profile, uint32_t now_ms) {
memset(manager, 0, sizeof(*manager));
if (!network_profile_valid(profile)) { manager->state = NETWORK_STATE_FALLBACK; return NETWORK_ACTION_FALLBACK; }
manager->profile = *profile; manager->has_profile = true; manager->state = NETWORK_STATE_CONNECTING; manager->started_ms = now_ms;
return NETWORK_ACTION_CONNECT;
}
network_action_t network_manager_tick(network_manager_t *manager, uint32_t now_ms) {
if (manager->state == NETWORK_STATE_CONNECTING && (uint32_t)(now_ms - manager->started_ms) >= kNetworkConnectWindowMs) { manager->state = NETWORK_STATE_FALLBACK; return NETWORK_ACTION_FALLBACK; }
return NETWORK_ACTION_NONE;
}
network_action_t network_manager_connected(network_manager_t *manager) { manager->state = NETWORK_STATE_EXTERNAL; return NETWORK_ACTION_NONE; }
network_action_t network_manager_disconnected(network_manager_t *manager, uint32_t now_ms) {
if (!manager->has_profile) { manager->state = NETWORK_STATE_FALLBACK; return NETWORK_ACTION_FALLBACK; }
manager->state = NETWORK_STATE_CONNECTING; manager->started_ms = now_ms; return NETWORK_ACTION_CONNECT;
}
bool network_manager_begin_validation(network_manager_t *manager, const network_profile_t *candidate) {
if (!network_profile_valid(candidate) || manager->state == NETWORK_STATE_VALIDATING) return false;
manager->candidate = *candidate; manager->state_before_validation = manager->state; manager->state = NETWORK_STATE_VALIDATING; return true;
}
network_action_t network_manager_finish_validation(network_manager_t *manager, bool success, uint32_t now_ms) {
if (manager->state != NETWORK_STATE_VALIDATING) return NETWORK_ACTION_NONE;
if (!success) { manager->state = manager->state_before_validation; return NETWORK_ACTION_NONE; }
manager->profile = manager->candidate; manager->has_profile = true; manager->state = NETWORK_STATE_CONNECTING; manager->started_ms = now_ms; return NETWORK_ACTION_CONNECT;
}
+6 -2
View File
@@ -1,7 +1,7 @@
CC ?= cc
CFLAGS ?= -std=c11 -Wall -Wextra -Werror -I../../include
all: test_command_queue test_game_domain test_bot_player test_game_lifecycle test_state_presenter test_http_api test_sync_service test_human_game_integration test_bot_game_integration test_robustness
all: test_command_queue test_game_domain test_bot_player test_game_lifecycle test_state_presenter test_http_api test_sync_service test_human_game_integration test_bot_game_integration test_robustness test_network_foundation
test_command_queue: test_command_queue.c ../../src/command_queue.c
$(CC) $(CFLAGS) $^ -o $@
@@ -17,6 +17,7 @@ run: all
./test_human_game_integration
./test_bot_game_integration
./test_robustness
./test_network_foundation
test_game_domain: test_game_domain.c ../../src/fleet_generator.c ../../src/game_engine.c
$(CC) $(CFLAGS) $^ -o $@
@@ -46,5 +47,8 @@ test_robustness: test_robustness.c ../../src/http_api.c ../../src/sync_service.c
$(CC) $(CFLAGS) $^ -o $@
test_network_foundation: test_network_foundation.c ../../src/network_state.c ../../src/network_credentials.c
$(CC) $(CFLAGS) $^ -o $@
clean:
rm -f test_command_queue test_game_domain test_bot_player test_game_lifecycle test_state_presenter test_http_api test_sync_service test_human_game_integration test_bot_game_integration test_robustness
rm -f test_command_queue test_game_domain test_bot_player test_game_lifecycle test_state_presenter test_http_api test_sync_service test_human_game_integration test_bot_game_integration test_robustness test_network_foundation
+34
View File
@@ -0,0 +1,34 @@
#include <assert.h>
#include <stdio.h>
#include <string.h>
#include "network_credentials.h"
#include "network_state.h"
static network_profile_t profile(const char *ssid, const char *password) { network_profile_t value = {0}; snprintf(value.ssid, sizeof(value.ssid), "%s", ssid); snprintf(value.password, sizeof(value.password), "%s", password); return value; }
typedef struct { bool present; network_credential_record_t record; } memory_store_t;
static bool read_record(void *context, network_credential_record_t *record) { memory_store_t *store = context; if (!store->present) return false; *record = store->record; return true; }
static bool write_record(void *context, const network_credential_record_t *record) { memory_store_t *store = context; store->record = *record; store->present = true; return true; }
static bool delete_record(void *context) { memory_store_t *store = context; store->present = false; memset(&store->record, 0, sizeof(store->record)); return true; }
int main(void) {
network_manager_t manager; network_profile_t home = profile("Home", "secret"); network_profile_t next = profile("Next", "newsecret");
assert(network_manager_init(&manager, NULL, 10U) == NETWORK_ACTION_FALLBACK && manager.state == NETWORK_STATE_FALLBACK);
assert(network_manager_init(&manager, &home, 10U) == NETWORK_ACTION_CONNECT);
assert(network_manager_tick(&manager, 30009U) == NETWORK_ACTION_NONE);
assert(network_manager_tick(&manager, 30010U) == NETWORK_ACTION_FALLBACK);
assert(network_manager_disconnected(&manager, 400U) == NETWORK_ACTION_CONNECT);
assert(network_manager_connected(&manager) == NETWORK_ACTION_NONE && manager.state == NETWORK_STATE_EXTERNAL);
assert(network_manager_begin_validation(&manager, &next));
assert(network_manager_finish_validation(&manager, false, 500U) == NETWORK_ACTION_NONE && strcmp(manager.profile.ssid, "Home") == 0);
assert(network_manager_begin_validation(&manager, &next));
assert(network_manager_finish_validation(&manager, true, 600U) == NETWORK_ACTION_CONNECT && strcmp(manager.profile.ssid, "Next") == 0);
network_credential_record_t record; network_profile_t restored = {0};
assert(network_credential_encode(&next, &record)); assert(network_credential_decode(&record, &restored)); assert(strcmp(restored.password, "newsecret") == 0);
record.profile.ssid[0] = 'X'; assert(!network_credential_decode(&record, &restored));
memory_store_t store = {0}; network_credential_backend_t backend = {&store, read_record, write_record, delete_record};
assert(network_credential_replace(&backend, &home)); assert(network_credential_load(&backend, &restored)); assert(strcmp(restored.ssid, "Home") == 0);
store.record.profile.ssid[0] = 'X'; assert(!network_credential_load(&backend, &restored));
assert(network_credential_replace(&backend, &next)); assert(network_credential_load(&backend, &restored)); assert(strcmp(restored.ssid, "Next") == 0);
assert(network_credential_delete(&backend)); assert(!network_credential_load(&backend, &restored));
network_profile_t invalid = profile("", "x"); assert(!network_profile_valid(&invalid));
puts("network foundation tests passed"); return 0;
}