Add firmware slices 1–2: skeleton and Wi-Fi provisioning
Implement ESP-IDF walking skeleton with SoftAP, health API, and host tests, then Slice 2 ISecureStore/NvsSecureStore, STA join, POST /api/wifi, and the provisioning web UI. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,13 @@
|
||||
idf_component_register(
|
||||
SRCS
|
||||
"src/FirmwareVersion.cpp"
|
||||
"src/HealthStatus.cpp"
|
||||
"src/HealthStatusJson.cpp"
|
||||
"src/Secret.cpp"
|
||||
"src/WifiSsid.cpp"
|
||||
"src/WifiCredentials.cpp"
|
||||
"src/WifiProvisionJson.cpp"
|
||||
INCLUDE_DIRS "include"
|
||||
)
|
||||
|
||||
target_compile_features(${COMPONENT_LIB} PUBLIC cxx_std_23)
|
||||
@@ -0,0 +1,69 @@
|
||||
/**
|
||||
* @file FirmwareVersion.hpp
|
||||
* @brief Strong type for the firmware release identifier string.
|
||||
*
|
||||
* DigiRadio firmware — https://github.com/manvalan/DigiRadio
|
||||
*
|
||||
* Copyright 2026 Michele Bigi
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* @author Michele Bigi
|
||||
* @date 2026-07-06
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
namespace core {
|
||||
|
||||
/**
|
||||
* @brief FirmwareVersion — validated firmware release identifier.
|
||||
*
|
||||
* @dname FirmwareVersion
|
||||
* @param version Non-empty semantic version string (e.g. "0.1.0").
|
||||
* @return n/a (type)
|
||||
* @pubstate Owns version_ (immutable after construction). No public data
|
||||
* members.
|
||||
*
|
||||
* Wraps the release string so API endpoints never pass a bare char*.
|
||||
*
|
||||
* @author Michele Bigi
|
||||
* @date 2026-07-06
|
||||
*/
|
||||
class FirmwareVersion {
|
||||
public:
|
||||
/**
|
||||
* @brief FirmwareVersion — construct from a non-empty version string.
|
||||
*
|
||||
* @dname FirmwareVersion
|
||||
* @param version Non-empty semantic version (e.g. "0.1.0").
|
||||
* @pubstate writes version_.
|
||||
*
|
||||
* @author Michele Bigi
|
||||
* @date 2026-07-06
|
||||
*/
|
||||
explicit FirmwareVersion(std::string_view version);
|
||||
|
||||
/**
|
||||
* @brief value — read the version string.
|
||||
*
|
||||
* @dname value
|
||||
* @return The stored version string view.
|
||||
* @pubstate reads version_.
|
||||
*
|
||||
* @author Michele Bigi
|
||||
* @date 2026-07-06
|
||||
*/
|
||||
[[nodiscard]] std::string_view value() const noexcept;
|
||||
|
||||
private:
|
||||
std::string version_;
|
||||
};
|
||||
|
||||
} // namespace core
|
||||
@@ -0,0 +1,98 @@
|
||||
/**
|
||||
* @file HealthStatus.hpp
|
||||
* @brief Health-check DTO returned by GET /api/health.
|
||||
*
|
||||
* DigiRadio firmware — https://github.com/manvalan/DigiRadio
|
||||
*
|
||||
* Copyright 2026 Michele Bigi
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* @author Michele Bigi
|
||||
* @date 2026-07-06
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "core/FirmwareVersion.hpp"
|
||||
|
||||
namespace core {
|
||||
|
||||
/**
|
||||
* @brief HealthState — coarse health indicator for the API.
|
||||
*
|
||||
* @dname HealthState
|
||||
* @return n/a (type)
|
||||
* @pubstate n/a
|
||||
*
|
||||
* @author Michele Bigi
|
||||
* @date 2026-07-06
|
||||
*/
|
||||
enum class HealthState {
|
||||
Ok,
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief HealthStatus — immutable health-check response payload.
|
||||
*
|
||||
* @dname HealthStatus
|
||||
* @param firmware Release identifier included in the response.
|
||||
* @return n/a (type)
|
||||
* @pubstate Owns state_ and firmware_. Factory ok() builds the nominal
|
||||
* response. No public data members.
|
||||
*
|
||||
* Pure domain type; serialisation lives in HealthStatusJson.hpp.
|
||||
*
|
||||
* @author Michele Bigi
|
||||
* @date 2026-07-06
|
||||
*/
|
||||
class HealthStatus {
|
||||
public:
|
||||
/**
|
||||
* @brief ok — build the nominal health response.
|
||||
*
|
||||
* @dname ok
|
||||
* @param firmware Active firmware version to report.
|
||||
* @return HealthStatus with HealthState::Ok.
|
||||
* @pubstate none
|
||||
*
|
||||
* @author Michele Bigi
|
||||
* @date 2026-07-06
|
||||
*/
|
||||
[[nodiscard]] static HealthStatus ok(FirmwareVersion firmware);
|
||||
|
||||
/**
|
||||
* @brief state — read the health indicator.
|
||||
*
|
||||
* @dname state
|
||||
* @return Current HealthState.
|
||||
* @pubstate reads state_.
|
||||
*
|
||||
* @author Michele Bigi
|
||||
* @date 2026-07-06
|
||||
*/
|
||||
[[nodiscard]] HealthState state() const noexcept;
|
||||
|
||||
/**
|
||||
* @brief firmware — read the reported firmware version.
|
||||
*
|
||||
* @dname firmware
|
||||
* @return The firmware version carried in this DTO.
|
||||
* @pubstate reads firmware_.
|
||||
*
|
||||
* @author Michele Bigi
|
||||
* @date 2026-07-06
|
||||
*/
|
||||
[[nodiscard]] const FirmwareVersion& firmware() const noexcept;
|
||||
|
||||
private:
|
||||
explicit HealthStatus(HealthState state, FirmwareVersion firmware);
|
||||
|
||||
HealthState state_;
|
||||
FirmwareVersion firmware_;
|
||||
};
|
||||
|
||||
} // namespace core
|
||||
@@ -0,0 +1,42 @@
|
||||
/**
|
||||
* @file HealthStatusJson.hpp
|
||||
* @brief JSON serialisation for HealthStatus (pure core, no ESP-IDF).
|
||||
*
|
||||
* DigiRadio firmware — https://github.com/manvalan/DigiRadio
|
||||
*
|
||||
* Copyright 2026 Michele Bigi
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* @author Michele Bigi
|
||||
* @date 2026-07-06
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "core/HealthStatus.hpp"
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace core {
|
||||
|
||||
/**
|
||||
* @brief serializeHealthStatusJson — render HealthStatus as JSON.
|
||||
*
|
||||
* @dname serializeHealthStatusJson
|
||||
* @param status Health DTO to serialise.
|
||||
* @return JSON object string, e.g. {"status":"ok","fw":"0.1.0"}.
|
||||
* @pubstate none
|
||||
*
|
||||
* Deterministic, allocation-on-stack-then-heap for the returned string.
|
||||
* Used by the HTTP shell; tested on the host without hardware.
|
||||
*
|
||||
* @author Michele Bigi
|
||||
* @date 2026-07-06
|
||||
*/
|
||||
[[nodiscard]] std::string serializeHealthStatusJson(const HealthStatus& status);
|
||||
|
||||
} // namespace core
|
||||
@@ -0,0 +1,108 @@
|
||||
/**
|
||||
* @file ISecureStore.hpp
|
||||
* @brief Abstract secure persistence for credentials and lists.
|
||||
*
|
||||
* DigiRadio firmware — https://github.com/manvalan/DigiRadio
|
||||
*
|
||||
* Copyright 2026 Michele Bigi
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* @author Michele Bigi
|
||||
* @date 2026-07-06
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "core/StoreError.hpp"
|
||||
#include "core/WifiCredentials.hpp"
|
||||
|
||||
#include <expected>
|
||||
|
||||
namespace core {
|
||||
|
||||
/**
|
||||
* @brief ISecureStore — persistence boundary for secrets at rest.
|
||||
*
|
||||
* @dname ISecureStore
|
||||
* @return n/a (type)
|
||||
* @pubstate Implementations own NVS/flash handles in the shell. The pure
|
||||
* core and host tests use fakes; never touch real keys here.
|
||||
*
|
||||
* Slice 2 stores Wi-Fi credentials. Station list and user credentials
|
||||
* arrive in later slices on the same interface.
|
||||
*
|
||||
* @author Michele Bigi
|
||||
* @date 2026-07-06
|
||||
*/
|
||||
class ISecureStore {
|
||||
public:
|
||||
/**
|
||||
* @brief ~ISecureStore — virtual destructor for interface.
|
||||
*
|
||||
* @dname ~ISecureStore
|
||||
* @pubstate n/a
|
||||
*
|
||||
* @author Michele Bigi
|
||||
* @date 2026-07-06
|
||||
*/
|
||||
virtual ~ISecureStore() = default;
|
||||
|
||||
/**
|
||||
* @brief hasWifiCredentials — check whether STA creds are stored.
|
||||
*
|
||||
* @dname hasWifiCredentials
|
||||
* @return true when loadWifiCredentials would succeed.
|
||||
* @pubstate reads backing storage via implementation.
|
||||
*
|
||||
* @author Michele Bigi
|
||||
* @date 2026-07-06
|
||||
*/
|
||||
[[nodiscard]] virtual bool hasWifiCredentials() const = 0;
|
||||
|
||||
/**
|
||||
* @brief saveWifiCredentials — persist validated STA credentials.
|
||||
*
|
||||
* @dname saveWifiCredentials
|
||||
* @param creds Domain credentials; password stays wrapped in Secret.
|
||||
* @return Ok on success, or StoreError::IoFailed.
|
||||
* @pubstate writes backing storage via implementation.
|
||||
*
|
||||
* @author Michele Bigi
|
||||
* @date 2026-07-06
|
||||
*/
|
||||
[[nodiscard]] virtual std::expected<void, StoreError>
|
||||
saveWifiCredentials(const WifiCredentials& creds) = 0;
|
||||
|
||||
/**
|
||||
* @brief loadWifiCredentials — read stored STA credentials.
|
||||
*
|
||||
* @dname loadWifiCredentials
|
||||
* @return WifiCredentials on success, or StoreError::NotFound /
|
||||
* StoreError::InvalidData / StoreError::IoFailed.
|
||||
* @pubstate reads backing storage via implementation.
|
||||
*
|
||||
* @author Michele Bigi
|
||||
* @date 2026-07-06
|
||||
*/
|
||||
[[nodiscard]] virtual std::expected<WifiCredentials, StoreError>
|
||||
loadWifiCredentials() const = 0;
|
||||
|
||||
/**
|
||||
* @brief clearWifiCredentials — erase stored STA credentials.
|
||||
*
|
||||
* @dname clearWifiCredentials
|
||||
* @return Ok on success, or StoreError::IoFailed.
|
||||
* @pubstate clears backing storage via implementation.
|
||||
*
|
||||
* @author Michele Bigi
|
||||
* @date 2026-07-06
|
||||
*/
|
||||
[[nodiscard]] virtual std::expected<void, StoreError>
|
||||
clearWifiCredentials() = 0;
|
||||
};
|
||||
|
||||
} // namespace core
|
||||
@@ -0,0 +1,39 @@
|
||||
/**
|
||||
* @file ParseError.hpp
|
||||
* @brief Typed errors for untrusted JSON parsing at the boundary.
|
||||
*
|
||||
* DigiRadio firmware — https://github.com/manvalan/DigiRadio
|
||||
*
|
||||
* Copyright 2026 Michele Bigi
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* @author Michele Bigi
|
||||
* @date 2026-07-06
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
namespace core {
|
||||
|
||||
/**
|
||||
* @brief ParseError — failure causes when parsing network input.
|
||||
*
|
||||
* @dname ParseError
|
||||
* @return n/a (type)
|
||||
* @pubstate n/a
|
||||
*
|
||||
* @author Michele Bigi
|
||||
* @date 2026-07-06
|
||||
*/
|
||||
enum class ParseError {
|
||||
InvalidJson,
|
||||
MissingField,
|
||||
InvalidSsid,
|
||||
InvalidPassword,
|
||||
};
|
||||
|
||||
} // namespace core
|
||||
@@ -0,0 +1,130 @@
|
||||
/**
|
||||
* @file Secret.hpp
|
||||
* @brief Opaque wrapper for sensitive strings (passwords, keys).
|
||||
*
|
||||
* DigiRadio firmware — https://github.com/manvalan/DigiRadio
|
||||
*
|
||||
* Copyright 2026 Michele Bigi
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* @author Michele Bigi
|
||||
* @date 2026-07-06
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <utility>
|
||||
|
||||
namespace core {
|
||||
|
||||
/**
|
||||
* @brief Secret — holds sensitive bytes, zeroised on destruction.
|
||||
*
|
||||
* @dname Secret
|
||||
* @return n/a (type)
|
||||
* @pubstate Owns bytes_ (immutable after construction except via move).
|
||||
* No operator<<; never log or serialise this type to plaintext.
|
||||
*
|
||||
* @author Michele Bigi
|
||||
* @date 2026-07-06
|
||||
*/
|
||||
class Secret {
|
||||
public:
|
||||
/**
|
||||
* @brief Secret — construct from a plaintext value at the boundary.
|
||||
*
|
||||
* @dname Secret
|
||||
* @param value Sensitive string moved into protected storage.
|
||||
* @pubstate writes bytes_.
|
||||
*
|
||||
* @author Michele Bigi
|
||||
* @date 2026-07-06
|
||||
*/
|
||||
explicit Secret(std::string value);
|
||||
|
||||
Secret(const Secret&) = delete;
|
||||
Secret& operator=(const Secret&) = delete;
|
||||
|
||||
/**
|
||||
* @brief Secret — move-construct, transferring protected storage.
|
||||
*
|
||||
* @dname Secret
|
||||
* @param other Source secret; zeroised after the move.
|
||||
* @pubstate transfers bytes_ from other.
|
||||
*
|
||||
* @author Michele Bigi
|
||||
* @date 2026-07-06
|
||||
*/
|
||||
Secret(Secret&& other) noexcept;
|
||||
|
||||
/**
|
||||
* @brief operator= — move-assign, transferring protected storage.
|
||||
*
|
||||
* @dname operator=
|
||||
* @param other Source secret; zeroised after the move.
|
||||
* @return Reference to this instance.
|
||||
* @pubstate transfers bytes_ from other.
|
||||
*
|
||||
* @author Michele Bigi
|
||||
* @date 2026-07-06
|
||||
*/
|
||||
Secret& operator=(Secret&& other) noexcept;
|
||||
|
||||
/**
|
||||
* @brief ~Secret — zeroise stored bytes.
|
||||
*
|
||||
* @dname ~Secret
|
||||
* @pubstate clears bytes_ securely.
|
||||
*
|
||||
* @author Michele Bigi
|
||||
* @date 2026-07-06
|
||||
*/
|
||||
~Secret();
|
||||
|
||||
/**
|
||||
* @brief length — byte length of the protected value.
|
||||
*
|
||||
* @dname length
|
||||
* @return Number of stored bytes.
|
||||
* @pubstate reads bytes_.
|
||||
*
|
||||
* @author Michele Bigi
|
||||
* @date 2026-07-06
|
||||
*/
|
||||
[[nodiscard]] std::size_t length() const noexcept;
|
||||
|
||||
/**
|
||||
* @brief usePlaintext — invoke a callable with a borrowed view.
|
||||
*
|
||||
* @dname usePlaintext
|
||||
* @param fn Callable invoked with the secret as string_view.
|
||||
* @return Whatever fn returns.
|
||||
* @pubstate reads bytes_; fn must not retain the view past its scope.
|
||||
*
|
||||
* Shell-only escape hatch for APIs (e.g. esp_wifi_set_config) that
|
||||
* require a transient C string. Never log inside fn.
|
||||
*
|
||||
* @author Michele Bigi
|
||||
* @date 2026-07-06
|
||||
*/
|
||||
template<typename Fn>
|
||||
[[nodiscard]] auto usePlaintext(Fn&& fn) const
|
||||
-> decltype(fn(std::declval<std::string_view>()))
|
||||
{
|
||||
return std::forward<Fn>(fn)(bytes_);
|
||||
}
|
||||
|
||||
private:
|
||||
void zeroize() noexcept;
|
||||
|
||||
std::string bytes_;
|
||||
};
|
||||
|
||||
} // namespace core
|
||||
@@ -0,0 +1,38 @@
|
||||
/**
|
||||
* @file StoreError.hpp
|
||||
* @brief Typed errors for secure-store operations.
|
||||
*
|
||||
* DigiRadio firmware — https://github.com/manvalan/DigiRadio
|
||||
*
|
||||
* Copyright 2026 Michele Bigi
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* @author Michele Bigi
|
||||
* @date 2026-07-06
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
namespace core {
|
||||
|
||||
/**
|
||||
* @brief StoreError — failure causes for ISecureStore operations.
|
||||
*
|
||||
* @dname StoreError
|
||||
* @return n/a (type)
|
||||
* @pubstate n/a
|
||||
*
|
||||
* @author Michele Bigi
|
||||
* @date 2026-07-06
|
||||
*/
|
||||
enum class StoreError {
|
||||
NotFound,
|
||||
IoFailed,
|
||||
InvalidData,
|
||||
};
|
||||
|
||||
} // namespace core
|
||||
@@ -0,0 +1,98 @@
|
||||
/**
|
||||
* @file WifiCredentials.hpp
|
||||
* @brief Domain type for stored Wi-Fi STA credentials.
|
||||
*
|
||||
* DigiRadio firmware — https://github.com/manvalan/DigiRadio
|
||||
*
|
||||
* Copyright 2026 Michele Bigi
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* @author Michele Bigi
|
||||
* @date 2026-07-06
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "core/Secret.hpp"
|
||||
#include "core/WifiSsid.hpp"
|
||||
|
||||
namespace core {
|
||||
|
||||
/**
|
||||
* @brief WifiCredentials — SSID plus protected PSK for STA join.
|
||||
*
|
||||
* @dname WifiCredentials
|
||||
* @return n/a (type)
|
||||
* @pubstate Owns ssid_ and password_. Password is never exposed as a
|
||||
* loggable string; use Secret::usePlaintext in the shell only.
|
||||
*
|
||||
* @author Michele Bigi
|
||||
* @date 2026-07-06
|
||||
*/
|
||||
class WifiCredentials {
|
||||
public:
|
||||
/** Maximum WPA-PSK length accepted at the boundary. */
|
||||
static constexpr std::size_t kMaxPasswordLength = 63;
|
||||
|
||||
/**
|
||||
* @brief isPasswordValid — validate a PSK length at the boundary.
|
||||
*
|
||||
* @dname isPasswordValid
|
||||
* @param raw Untrusted password from network input.
|
||||
* @return true when length is 0 (open) or 8–63 (WPA).
|
||||
* @pubstate none
|
||||
*
|
||||
* Open networks use an empty password; WPA-PSK requires 8–63 chars.
|
||||
*
|
||||
* @author Michele Bigi
|
||||
* @date 2026-07-06
|
||||
*/
|
||||
[[nodiscard]] static bool isPasswordValid(std::string_view raw) noexcept;
|
||||
|
||||
/**
|
||||
* @brief WifiCredentials — construct from validated domain parts.
|
||||
*
|
||||
* @dname WifiCredentials
|
||||
* @param ssid Validated network name.
|
||||
* @param password Protected pre-shared key (may be empty for open).
|
||||
* @pubstate writes ssid_ and password_.
|
||||
*
|
||||
* @author Michele Bigi
|
||||
* @date 2026-07-06
|
||||
*/
|
||||
WifiCredentials(WifiSsid ssid, Secret password);
|
||||
|
||||
/**
|
||||
* @brief ssid — read the network name.
|
||||
*
|
||||
* @dname ssid
|
||||
* @return The stored SSID value object.
|
||||
* @pubstate reads ssid_.
|
||||
*
|
||||
* @author Michele Bigi
|
||||
* @date 2026-07-06
|
||||
*/
|
||||
[[nodiscard]] const WifiSsid& ssid() const noexcept;
|
||||
|
||||
/**
|
||||
* @brief password — borrow the protected PSK.
|
||||
*
|
||||
* @dname password
|
||||
* @return Const reference to the Secret wrapper.
|
||||
* @pubstate reads password_.
|
||||
*
|
||||
* @author Michele Bigi
|
||||
* @date 2026-07-06
|
||||
*/
|
||||
[[nodiscard]] const Secret& password() const noexcept;
|
||||
|
||||
private:
|
||||
WifiSsid ssid_;
|
||||
Secret password_;
|
||||
};
|
||||
|
||||
} // namespace core
|
||||
@@ -0,0 +1,75 @@
|
||||
/**
|
||||
* @file WifiProvisionJson.hpp
|
||||
* @brief Parse and serialise Wi-Fi provisioning JSON at the boundary.
|
||||
*
|
||||
* DigiRadio firmware — https://github.com/manvalan/DigiRadio
|
||||
*
|
||||
* Copyright 2026 Michele Bigi
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* @author Michele Bigi
|
||||
* @date 2026-07-06
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "core/ParseError.hpp"
|
||||
#include "core/WifiCredentials.hpp"
|
||||
|
||||
#include <expected>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
namespace core {
|
||||
|
||||
/**
|
||||
* @brief parseWifiProvisionJson — validate POST body into credentials.
|
||||
*
|
||||
* @dname parseWifiProvisionJson
|
||||
* @param json Untrusted request body, e.g.
|
||||
* {"ssid":"MyNet","password":"secret"}.
|
||||
* @return WifiCredentials on success, or a ParseError.
|
||||
* @pubstate none
|
||||
*
|
||||
* Minimal parser for the Slice 2 provisioning endpoint; rejects malformed
|
||||
* input before any persistence or Wi-Fi driver calls.
|
||||
*
|
||||
* @author Michele Bigi
|
||||
* @date 2026-07-06
|
||||
*/
|
||||
[[nodiscard]] std::expected<WifiCredentials, ParseError>
|
||||
parseWifiProvisionJson(std::string_view json);
|
||||
|
||||
/**
|
||||
* @brief serializeWifiProvisionSavedJson — success response body.
|
||||
*
|
||||
* @dname serializeWifiProvisionSavedJson
|
||||
* @param rebootInSec Seconds until the device reboots into STA mode.
|
||||
* @return JSON object string.
|
||||
* @pubstate none
|
||||
*
|
||||
* @author Michele Bigi
|
||||
* @date 2026-07-06
|
||||
*/
|
||||
[[nodiscard]] std::string
|
||||
serializeWifiProvisionSavedJson(unsigned rebootInSec);
|
||||
|
||||
/**
|
||||
* @brief serializeWifiProvisionErrorJson — rejection response body.
|
||||
*
|
||||
* @dname serializeWifiProvisionErrorJson
|
||||
* @param reason Short machine-readable cause (never a secret).
|
||||
* @return JSON object string.
|
||||
* @pubstate none
|
||||
*
|
||||
* @author Michele Bigi
|
||||
* @date 2026-07-06
|
||||
*/
|
||||
[[nodiscard]] std::string
|
||||
serializeWifiProvisionErrorJson(std::string_view reason);
|
||||
|
||||
} // namespace core
|
||||
@@ -0,0 +1,82 @@
|
||||
/**
|
||||
* @file WifiSsid.hpp
|
||||
* @brief Strong type for a Wi-Fi network name (802.11 SSID).
|
||||
*
|
||||
* DigiRadio firmware — https://github.com/manvalan/DigiRadio
|
||||
*
|
||||
* Copyright 2026 Michele Bigi
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* @author Michele Bigi
|
||||
* @date 2026-07-06
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
namespace core {
|
||||
|
||||
/**
|
||||
* @brief WifiSsid — validated Wi-Fi SSID (1–32 bytes).
|
||||
*
|
||||
* @dname WifiSsid
|
||||
* @return n/a (type)
|
||||
* @pubstate Owns ssid_ (immutable after construction).
|
||||
*
|
||||
* @author Michele Bigi
|
||||
* @date 2026-07-06
|
||||
*/
|
||||
class WifiSsid {
|
||||
public:
|
||||
/** Maximum SSID length per 802.11. */
|
||||
static constexpr std::size_t kMaxLength = 32;
|
||||
|
||||
/**
|
||||
* @brief tryFrom — parse and validate an SSID at the boundary.
|
||||
*
|
||||
* @dname tryFrom
|
||||
* @param raw Untrusted SSID string from network input.
|
||||
* @return WifiSsid on success, or empty optional if invalid.
|
||||
* @pubstate none
|
||||
*
|
||||
* @author Michele Bigi
|
||||
* @date 2026-07-06
|
||||
*/
|
||||
[[nodiscard]] static bool isValid(std::string_view raw) noexcept;
|
||||
|
||||
/**
|
||||
* @brief WifiSsid — construct from an already-validated SSID.
|
||||
*
|
||||
* @dname WifiSsid
|
||||
* @param raw Non-empty SSID up to 32 bytes.
|
||||
* @pubstate writes ssid_.
|
||||
*
|
||||
* @author Michele Bigi
|
||||
* @date 2026-07-06
|
||||
*/
|
||||
explicit WifiSsid(std::string_view raw);
|
||||
|
||||
/**
|
||||
* @brief value — read the SSID string.
|
||||
*
|
||||
* @dname value
|
||||
* @return Stored SSID as a string view.
|
||||
* @pubstate reads ssid_.
|
||||
*
|
||||
* @author Michele Bigi
|
||||
* @date 2026-07-06
|
||||
*/
|
||||
[[nodiscard]] std::string_view value() const noexcept;
|
||||
|
||||
private:
|
||||
std::string ssid_;
|
||||
};
|
||||
|
||||
} // namespace core
|
||||
@@ -0,0 +1,36 @@
|
||||
/**
|
||||
* @file FirmwareVersion.cpp
|
||||
* @brief FirmwareVersion implementation.
|
||||
*
|
||||
* DigiRadio firmware — https://github.com/manvalan/DigiRadio
|
||||
*
|
||||
* Copyright 2026 Michele Bigi
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* @author Michele Bigi
|
||||
* @date 2026-07-06
|
||||
*/
|
||||
|
||||
#include "core/FirmwareVersion.hpp"
|
||||
|
||||
#include <cassert>
|
||||
|
||||
namespace core {
|
||||
|
||||
FirmwareVersion::FirmwareVersion(std::string_view version)
|
||||
: version_(version)
|
||||
{
|
||||
assert(!version.empty());
|
||||
}
|
||||
|
||||
std::string_view FirmwareVersion::value() const noexcept
|
||||
{
|
||||
return version_;
|
||||
}
|
||||
|
||||
} // namespace core
|
||||
@@ -0,0 +1,44 @@
|
||||
/**
|
||||
* @file HealthStatus.cpp
|
||||
* @brief HealthStatus implementation.
|
||||
*
|
||||
* DigiRadio firmware — https://github.com/manvalan/DigiRadio
|
||||
*
|
||||
* Copyright 2026 Michele Bigi
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* @author Michele Bigi
|
||||
* @date 2026-07-06
|
||||
*/
|
||||
|
||||
#include "core/HealthStatus.hpp"
|
||||
|
||||
namespace core {
|
||||
|
||||
HealthStatus HealthStatus::ok(FirmwareVersion firmware)
|
||||
{
|
||||
return HealthStatus(HealthState::Ok, std::move(firmware));
|
||||
}
|
||||
|
||||
HealthStatus::HealthStatus(HealthState state, FirmwareVersion firmware)
|
||||
: state_(state)
|
||||
, firmware_(std::move(firmware))
|
||||
{
|
||||
}
|
||||
|
||||
HealthState HealthStatus::state() const noexcept
|
||||
{
|
||||
return state_;
|
||||
}
|
||||
|
||||
const FirmwareVersion& HealthStatus::firmware() const noexcept
|
||||
{
|
||||
return firmware_;
|
||||
}
|
||||
|
||||
} // namespace core
|
||||
@@ -0,0 +1,53 @@
|
||||
/**
|
||||
* @file HealthStatusJson.cpp
|
||||
* @brief JSON serialisation for HealthStatus.
|
||||
*
|
||||
* DigiRadio firmware — https://github.com/manvalan/DigiRadio
|
||||
*
|
||||
* Copyright 2026 Michele Bigi
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* @author Michele Bigi
|
||||
* @date 2026-07-06
|
||||
*/
|
||||
|
||||
#include "core/HealthStatusJson.hpp"
|
||||
|
||||
namespace core {
|
||||
|
||||
namespace {
|
||||
|
||||
/**
|
||||
* @brief healthStateToken — map HealthState to the API status string.
|
||||
*
|
||||
* @dname healthStateToken
|
||||
* @param state Health indicator to encode.
|
||||
* @return JSON string token without quotes (e.g. ok).
|
||||
* @pubstate none
|
||||
*
|
||||
* @author Michele Bigi
|
||||
* @date 2026-07-06
|
||||
*/
|
||||
[[nodiscard]] const char* healthStateToken(HealthState state) noexcept
|
||||
{
|
||||
switch (state) {
|
||||
case HealthState::Ok:
|
||||
return "ok";
|
||||
}
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
std::string serializeHealthStatusJson(const HealthStatus& status)
|
||||
{
|
||||
return std::string("{\"status\":\"") + healthStateToken(status.state())
|
||||
+ "\",\"fw\":\"" + std::string(status.firmware().value()) + "\"}";
|
||||
}
|
||||
|
||||
} // namespace core
|
||||
@@ -0,0 +1,64 @@
|
||||
/**
|
||||
* @file Secret.cpp
|
||||
* @brief Secret implementation.
|
||||
*
|
||||
* DigiRadio firmware — https://github.com/manvalan/DigiRadio
|
||||
*
|
||||
* Copyright 2026 Michele Bigi
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* @author Michele Bigi
|
||||
* @date 2026-07-06
|
||||
*/
|
||||
|
||||
#include "core/Secret.hpp"
|
||||
|
||||
#include <cstring>
|
||||
|
||||
namespace core {
|
||||
|
||||
Secret::Secret(std::string value)
|
||||
: bytes_(std::move(value))
|
||||
{
|
||||
}
|
||||
|
||||
Secret::Secret(Secret&& other) noexcept
|
||||
: bytes_(std::move(other.bytes_))
|
||||
{
|
||||
other.zeroize();
|
||||
}
|
||||
|
||||
Secret& Secret::operator=(Secret&& other) noexcept
|
||||
{
|
||||
if (this != &other) {
|
||||
zeroize();
|
||||
bytes_ = std::move(other.bytes_);
|
||||
other.zeroize();
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
|
||||
Secret::~Secret()
|
||||
{
|
||||
zeroize();
|
||||
}
|
||||
|
||||
std::size_t Secret::length() const noexcept
|
||||
{
|
||||
return bytes_.size();
|
||||
}
|
||||
|
||||
void Secret::zeroize() noexcept
|
||||
{
|
||||
if (!bytes_.empty()) {
|
||||
std::memset(bytes_.data(), 0, bytes_.size());
|
||||
bytes_.clear();
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace core
|
||||
@@ -0,0 +1,47 @@
|
||||
/**
|
||||
* @file WifiCredentials.cpp
|
||||
* @brief WifiCredentials implementation.
|
||||
*
|
||||
* DigiRadio firmware — https://github.com/manvalan/DigiRadio
|
||||
*
|
||||
* Copyright 2026 Michele Bigi
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* @author Michele Bigi
|
||||
* @date 2026-07-06
|
||||
*/
|
||||
|
||||
#include "core/WifiCredentials.hpp"
|
||||
|
||||
namespace core {
|
||||
|
||||
bool WifiCredentials::isPasswordValid(std::string_view raw) noexcept
|
||||
{
|
||||
if (raw.empty()) {
|
||||
return true;
|
||||
}
|
||||
return raw.size() >= 8 && raw.size() <= kMaxPasswordLength;
|
||||
}
|
||||
|
||||
WifiCredentials::WifiCredentials(WifiSsid ssid, Secret password)
|
||||
: ssid_(std::move(ssid))
|
||||
, password_(std::move(password))
|
||||
{
|
||||
}
|
||||
|
||||
const WifiSsid& WifiCredentials::ssid() const noexcept
|
||||
{
|
||||
return ssid_;
|
||||
}
|
||||
|
||||
const Secret& WifiCredentials::password() const noexcept
|
||||
{
|
||||
return password_;
|
||||
}
|
||||
|
||||
} // namespace core
|
||||
@@ -0,0 +1,93 @@
|
||||
/**
|
||||
* @file WifiProvisionJson.cpp
|
||||
* @brief Wi-Fi provisioning JSON parse/serialise implementation.
|
||||
*
|
||||
* DigiRadio firmware — https://github.com/manvalan/DigiRadio
|
||||
*
|
||||
* Copyright 2026 Michele Bigi
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* @author Michele Bigi
|
||||
* @date 2026-07-06
|
||||
*/
|
||||
|
||||
#include "core/WifiProvisionJson.hpp"
|
||||
|
||||
namespace core {
|
||||
|
||||
namespace {
|
||||
|
||||
/**
|
||||
* @brief extractJsonString — read a quoted string value for a key.
|
||||
*
|
||||
* @dname extractJsonString
|
||||
* @param json Full JSON object text.
|
||||
* @param key Field name without quotes (e.g. ssid).
|
||||
* @return Decoded string view into json, or empty on failure.
|
||||
* @pubstate none
|
||||
*
|
||||
* @author Michele Bigi
|
||||
* @date 2026-07-06
|
||||
*/
|
||||
[[nodiscard]] std::string_view extractJsonString(std::string_view json,
|
||||
std::string_view key)
|
||||
{
|
||||
const std::string needle = std::string("\"") + std::string(key) + "\":\"";
|
||||
const std::size_t start = json.find(needle);
|
||||
if (start == std::string_view::npos) {
|
||||
return {};
|
||||
}
|
||||
const std::size_t valueStart = start + needle.size();
|
||||
const std::size_t valueEnd = json.find('"', valueStart);
|
||||
if (valueEnd == std::string_view::npos) {
|
||||
return {};
|
||||
}
|
||||
return json.substr(valueStart, valueEnd - valueStart);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
std::expected<WifiCredentials, ParseError>
|
||||
parseWifiProvisionJson(std::string_view json)
|
||||
{
|
||||
if (json.find('{') == std::string_view::npos) {
|
||||
return std::unexpected(ParseError::InvalidJson);
|
||||
}
|
||||
|
||||
const std::string_view ssidRaw = extractJsonString(json, "ssid");
|
||||
if (ssidRaw.empty() && json.find("\"ssid\"") == std::string_view::npos) {
|
||||
return std::unexpected(ParseError::MissingField);
|
||||
}
|
||||
if (!WifiSsid::isValid(ssidRaw)) {
|
||||
return std::unexpected(ParseError::InvalidSsid);
|
||||
}
|
||||
|
||||
std::string_view passwordRaw;
|
||||
if (json.find("\"password\"") != std::string_view::npos) {
|
||||
passwordRaw = extractJsonString(json, "password");
|
||||
}
|
||||
if (!WifiCredentials::isPasswordValid(passwordRaw)) {
|
||||
return std::unexpected(ParseError::InvalidPassword);
|
||||
}
|
||||
|
||||
return WifiCredentials(WifiSsid(ssidRaw), Secret(std::string(passwordRaw)));
|
||||
}
|
||||
|
||||
std::string serializeWifiProvisionSavedJson(unsigned rebootInSec)
|
||||
{
|
||||
return std::string("{\"status\":\"saved\",\"reboot_in_sec\":")
|
||||
+ std::to_string(rebootInSec) + "}";
|
||||
}
|
||||
|
||||
std::string serializeWifiProvisionErrorJson(std::string_view reason)
|
||||
{
|
||||
return std::string("{\"status\":\"error\",\"reason\":\"") + std::string(reason)
|
||||
+ "\"}";
|
||||
}
|
||||
|
||||
} // namespace core
|
||||
@@ -0,0 +1,41 @@
|
||||
/**
|
||||
* @file WifiSsid.cpp
|
||||
* @brief WifiSsid implementation.
|
||||
*
|
||||
* DigiRadio firmware — https://github.com/manvalan/DigiRadio
|
||||
*
|
||||
* Copyright 2026 Michele Bigi
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* @author Michele Bigi
|
||||
* @date 2026-07-06
|
||||
*/
|
||||
|
||||
#include "core/WifiSsid.hpp"
|
||||
|
||||
#include <cassert>
|
||||
|
||||
namespace core {
|
||||
|
||||
bool WifiSsid::isValid(std::string_view raw) noexcept
|
||||
{
|
||||
return !raw.empty() && raw.size() <= kMaxLength;
|
||||
}
|
||||
|
||||
WifiSsid::WifiSsid(std::string_view raw)
|
||||
: ssid_(raw)
|
||||
{
|
||||
assert(isValid(raw));
|
||||
}
|
||||
|
||||
std::string_view WifiSsid::value() const noexcept
|
||||
{
|
||||
return ssid_;
|
||||
}
|
||||
|
||||
} // namespace core
|
||||
@@ -0,0 +1,33 @@
|
||||
# Host unit tests for components/core (plain CMake + ctest)
|
||||
|
||||
cmake_minimum_required(VERSION 3.16)
|
||||
project(digiradio_core_tests LANGUAGES CXX)
|
||||
|
||||
set(CMAKE_CXX_STANDARD 23)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
set(CMAKE_CXX_EXTENSIONS ON)
|
||||
|
||||
enable_testing()
|
||||
|
||||
set(CORE_SRC_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../src")
|
||||
|
||||
add_library(digiradio_core STATIC
|
||||
"${CORE_SRC_DIR}/FirmwareVersion.cpp"
|
||||
"${CORE_SRC_DIR}/HealthStatus.cpp"
|
||||
"${CORE_SRC_DIR}/HealthStatusJson.cpp"
|
||||
"${CORE_SRC_DIR}/Secret.cpp"
|
||||
"${CORE_SRC_DIR}/WifiSsid.cpp"
|
||||
"${CORE_SRC_DIR}/WifiCredentials.cpp"
|
||||
"${CORE_SRC_DIR}/WifiProvisionJson.cpp"
|
||||
)
|
||||
target_include_directories(digiradio_core PUBLIC
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/../include"
|
||||
)
|
||||
|
||||
add_executable(health_status_test health_status_test.cpp)
|
||||
target_link_libraries(health_status_test PRIVATE digiradio_core)
|
||||
add_test(NAME health_status_test COMMAND health_status_test)
|
||||
|
||||
add_executable(wifi_provision_test wifi_provision_test.cpp)
|
||||
target_link_libraries(wifi_provision_test PRIVATE digiradio_core)
|
||||
add_test(NAME wifi_provision_test COMMAND wifi_provision_test)
|
||||
@@ -0,0 +1,89 @@
|
||||
/**
|
||||
* @file health_status_test.cpp
|
||||
* @brief Host tests for HealthStatus JSON serialisation.
|
||||
*
|
||||
* DigiRadio firmware — https://github.com/manvalan/DigiRadio
|
||||
*
|
||||
* Copyright 2026 Michele Bigi
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* @author Michele Bigi
|
||||
* @date 2026-07-06
|
||||
*/
|
||||
|
||||
#include "core/FirmwareVersion.hpp"
|
||||
#include "core/HealthStatus.hpp"
|
||||
#include "core/HealthStatusJson.hpp"
|
||||
|
||||
#include <cstdlib>
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
|
||||
namespace {
|
||||
|
||||
/**
|
||||
* @brief expectEqual — assert two strings match.
|
||||
*
|
||||
* @dname expectEqual
|
||||
* @param actual Observed value.
|
||||
* @param expected Expected value.
|
||||
* @return true when equal.
|
||||
* @pubstate none
|
||||
*
|
||||
* @author Michele Bigi
|
||||
* @date 2026-07-06
|
||||
*/
|
||||
[[nodiscard]] bool expectEqual(const std::string& actual,
|
||||
const std::string& expected)
|
||||
{
|
||||
if (actual == expected) {
|
||||
return true;
|
||||
}
|
||||
std::cerr << "expected: " << expected << "\nactual: " << actual << '\n';
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief runHealthStatusJsonTest — verify nominal JSON output.
|
||||
*
|
||||
* @dname runHealthStatusJsonTest
|
||||
* @param none
|
||||
* @return EXIT_SUCCESS or EXIT_FAILURE.
|
||||
* @pubstate none
|
||||
*
|
||||
* @author Michele Bigi
|
||||
* @date 2026-07-06
|
||||
*/
|
||||
[[nodiscard]] int runHealthStatusJsonTest()
|
||||
{
|
||||
const core::HealthStatus status =
|
||||
core::HealthStatus::ok(core::FirmwareVersion("0.1.0"));
|
||||
const std::string json = core::serializeHealthStatusJson(status);
|
||||
if (!expectEqual(json, R"({"status":"ok","fw":"0.1.0"})")) {
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
return EXIT_SUCCESS;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
/**
|
||||
* @brief main — host test entry point.
|
||||
*
|
||||
* @dname main
|
||||
* @param none
|
||||
* @return EXIT_SUCCESS when all tests pass.
|
||||
* @pubstate none
|
||||
*
|
||||
* @author Michele Bigi
|
||||
* @date 2026-07-06
|
||||
*/
|
||||
int main()
|
||||
{
|
||||
return runHealthStatusJsonTest();
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
/**
|
||||
* @file wifi_provision_test.cpp
|
||||
* @brief Host tests for Wi-Fi provisioning JSON parse/serialise.
|
||||
*
|
||||
* DigiRadio firmware — https://github.com/manvalan/DigiRadio
|
||||
*
|
||||
* Copyright 2026 Michele Bigi
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* @author Michele Bigi
|
||||
* @date 2026-07-06
|
||||
*/
|
||||
|
||||
#include "core/ParseError.hpp"
|
||||
#include "core/WifiCredentials.hpp"
|
||||
#include "core/WifiProvisionJson.hpp"
|
||||
|
||||
#include <cstdlib>
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
|
||||
namespace {
|
||||
|
||||
/**
|
||||
* @brief expectEqual — assert two strings match.
|
||||
*
|
||||
* @dname expectEqual
|
||||
* @param actual Observed value.
|
||||
* @param expected Expected value.
|
||||
* @return true when equal.
|
||||
* @pubstate none
|
||||
*
|
||||
* @author Michele Bigi
|
||||
* @date 2026-07-06
|
||||
*/
|
||||
[[nodiscard]] bool expectEqual(const std::string& actual,
|
||||
const std::string& expected)
|
||||
{
|
||||
if (actual == expected) {
|
||||
return true;
|
||||
}
|
||||
std::cerr << "expected: " << expected << "\nactual: " << actual << '\n';
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief runWifiProvisionParseTest — verify nominal JSON parsing.
|
||||
*
|
||||
* @dname runWifiProvisionParseTest
|
||||
* @return EXIT_SUCCESS or EXIT_FAILURE.
|
||||
* @pubstate none
|
||||
*
|
||||
* @author Michele Bigi
|
||||
* @date 2026-07-06
|
||||
*/
|
||||
[[nodiscard]] int runWifiProvisionParseTest()
|
||||
{
|
||||
const auto parsed = core::parseWifiProvisionJson(
|
||||
R"({"ssid":"MyNet","password":"secret12"})");
|
||||
if (!parsed) {
|
||||
std::cerr << "parse failed\n";
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
if (parsed->ssid().value() != "MyNet") {
|
||||
std::cerr << "unexpected ssid\n";
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
bool pwdOk = false;
|
||||
parsed->password().usePlaintext(
|
||||
[&](std::string_view pwd) { pwdOk = (pwd == "secret12"); });
|
||||
if (!pwdOk) {
|
||||
std::cerr << "unexpected password\n";
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
return EXIT_SUCCESS;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief runWifiProvisionRejectTest — verify invalid SSID is rejected.
|
||||
*
|
||||
* @dname runWifiProvisionRejectTest
|
||||
* @return EXIT_SUCCESS or EXIT_FAILURE.
|
||||
* @pubstate none
|
||||
*
|
||||
* @author Michele Bigi
|
||||
* @date 2026-07-06
|
||||
*/
|
||||
[[nodiscard]] int runWifiProvisionRejectTest()
|
||||
{
|
||||
const auto parsed =
|
||||
core::parseWifiProvisionJson(R"({"ssid":"","password":"secret12"})");
|
||||
if (parsed || parsed.error() != core::ParseError::InvalidSsid) {
|
||||
std::cerr << "expected InvalidSsid\n";
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
return EXIT_SUCCESS;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief runWifiProvisionSerialiseTest — verify saved response JSON.
|
||||
*
|
||||
* @dname runWifiProvisionSerialiseTest
|
||||
* @return EXIT_SUCCESS or EXIT_FAILURE.
|
||||
* @pubstate none
|
||||
*
|
||||
* @author Michele Bigi
|
||||
* @date 2026-07-06
|
||||
*/
|
||||
[[nodiscard]] int runWifiProvisionSerialiseTest()
|
||||
{
|
||||
if (!expectEqual(core::serializeWifiProvisionSavedJson(3),
|
||||
R"({"status":"saved","reboot_in_sec":3})")) {
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
return EXIT_SUCCESS;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
/**
|
||||
* @brief main — host test entry point.
|
||||
*
|
||||
* @dname main
|
||||
* @return EXIT_SUCCESS when all tests pass.
|
||||
* @pubstate none
|
||||
*
|
||||
* @author Michele Bigi
|
||||
* @date 2026-07-06
|
||||
*/
|
||||
int main()
|
||||
{
|
||||
if (runWifiProvisionParseTest() != EXIT_SUCCESS) {
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
if (runWifiProvisionRejectTest() != EXIT_SUCCESS) {
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
if (runWifiProvisionSerialiseTest() != EXIT_SUCCESS) {
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
return EXIT_SUCCESS;
|
||||
}
|
||||
Reference in New Issue
Block a user