diff --git a/PLANS.md b/PLANS.md index 92ffc04..265ad44 100644 --- a/PLANS.md +++ b/PLANS.md @@ -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. diff --git a/include/network_credential_store.h b/include/network_credential_store.h new file mode 100644 index 0000000..8012430 --- /dev/null +++ b/include/network_credential_store.h @@ -0,0 +1,8 @@ +#ifndef NETWORK_CREDENTIAL_STORE_H +#define NETWORK_CREDENTIAL_STORE_H +#include +#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 diff --git a/include/network_credentials.h b/include/network_credentials.h new file mode 100644 index 0000000..0a1af0a --- /dev/null +++ b/include/network_credentials.h @@ -0,0 +1,25 @@ +#ifndef NETWORK_CREDENTIALS_H +#define NETWORK_CREDENTIALS_H + +#include +#include +#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 diff --git a/include/network_state.h b/include/network_state.h new file mode 100644 index 0000000..a37967c --- /dev/null +++ b/include/network_state.h @@ -0,0 +1,21 @@ +#ifndef NETWORK_STATE_H +#define NETWORK_STATE_H + +#include +#include + +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 diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 409a607..94d0187 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -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 ) diff --git a/src/network_credential_store.c b/src/network_credential_store.c new file mode 100644 index 0000000..5e472cf --- /dev/null +++ b/src/network_credential_store.c @@ -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; } diff --git a/src/network_credentials.c b/src/network_credentials.c new file mode 100644 index 0000000..41387fa --- /dev/null +++ b/src/network_credentials.c @@ -0,0 +1,10 @@ +#include "network_credentials.h" +#include +#include +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); } diff --git a/src/network_state.c b/src/network_state.c new file mode 100644 index 0000000..6792968 --- /dev/null +++ b/src/network_state.c @@ -0,0 +1,33 @@ +#include "network_state.h" + +#include + +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; +} diff --git a/test/host/Makefile b/test/host/Makefile index abefb5f..0bc2d32 100644 --- a/test/host/Makefile +++ b/test/host/Makefile @@ -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 diff --git a/test/host/test_network_foundation.c b/test/host/test_network_foundation.c new file mode 100644 index 0000000..1ce459b --- /dev/null +++ b/test/host/test_network_foundation.c @@ -0,0 +1,34 @@ +#include +#include +#include +#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; +}