[READ-ONLY] Mirror of https://github.com/jmrplens/kleidos. Kleidos — Hardware BLE password manager for ESP32-S3 (M5StickC Plus2, StickS3, M5Stack Gray, T-Deck, Cardputer) jmrplens.github.io/kleidos/
0

Configure Feed

Select the types of activity you want to include in your feed.

fix(test): configure clangd and clang-tidy for native test tree (#3)

* fix(test): configure clangd and clang-tidy for native test tree

- Generate compile_commands.json from pio run -e native -t compiledb
so clangd resolves src/ headers in test files without errors.
- Fix .clangd PathMatch regex (relative path, not absolute) and add
-I.pio/build/native/unity_config so unity_config.h resolves.
- Remove overly broad Remove: -I* that was stripping all added paths.
- Add -Wno-format-security and UnusedIncludes: None to test block.
- Add test/.clang-tidy that inherits the root config and:
- Allows test_* functions to use snake_case (FunctionIgnoredRegexp)
- Disables cert-err33-c, cert-dcl50-cpp,
cppcoreguidelines-pro-type-cstyle-cast,
bugprone-argument-comment, bugprone-misplaced-widening-cast,
bugprone-implicit-widening-of-multiplication-result,
misc-unused-using-decls — all false positives in test context.
- Add /* intentionally empty */ comment to all empty setUp()/tearDown()
bodies (28 files) to suppress SonarLint c:S1186.
- Fix test_vault_task: change relative include ../../src/vault/vault_task.h
to vault/vault_task.h (consistent with all other test files; -Isrc
already in compile flags).
- Reorganise test/ into a module-mirroring subtree layout:
test/ble/, test/crypto/, test/hal/, test/ota/, test/platform/,
test/states/, test/totp/, test/ui/, test/vault/, test/web/
(old flat directories removed).
- Add sonar-project.properties exclusion e4: suppress c:S1186 on
test/**/*.cpp at SonarCloud level.

All 468 native unit tests pass (pio test -e native).

* fix(clangd): add -DESP_PLATFORM to global CompileFlags

All Kleidos targets are ESP32/ESP32-S3 (framework = espidf), so
ESP_PLATFORM is always defined at compile time. Without this flag in
the clangd config, the language server was activating #else branches
inside #ifdef ESP_PLATFORM blocks, hiding device code and showing
false diagnostics for src/ files.

The test/** fragment already has Remove: -DESP_PLATFORM, so native
unit-test files continue to compile without the flag.

* revert(clangd): remove incorrect -DESP_PLATFORM from global CompileFlags

Adding -DESP_PLATFORM globally caused 'too many errors' in src/ files
because compile_commands.json was generated from the native env and
lacked all ESP-IDF/Xtensa include paths.

The correct fix is to generate compile_commands.json from a device env:
./scripts/generate_compiledb.sh sticks3

This populates the DB with proper -DESP_PLATFORM and all IDF includes
for every src/ translation unit. The symlink at the project root then
points clangd to the right compilation context.

authored by

José M. Requena Plens and committed by
GitHub
(Jun 8, 2026, 4:29 AM +0200) 4588adc6 5b8a1810

+7692 -5444
+9 -2
.clangd
··· 65 65 # once after a fresh clone so <unity.h> and friends resolve. 66 66 --- 67 67 If: 68 - PathMatch: [.*/test/.*\.(cpp|h)] 68 + PathMatch: test/.*\.(cpp|h) 69 69 CompileFlags: 70 70 Compiler: clang++ 71 71 Add: ··· 74 74 - -I.pio/libdeps/native/Unity/src 75 75 - -I.pio/libdeps/native/ArduinoJson/src 76 76 - -I.pio/libdeps/native/mbedtls/include 77 + - -I.pio/build/native/unity_config 77 78 - -DUNITY_INCLUDE_DOUBLE 78 79 - -DKLEIDOS_BRANDS_TEST_FIXTURE 80 + - -DUNITY_INCLUDE_CONFIG_H 81 + - -Wno-format-security 79 82 Remove: 80 83 - -DESP_PLATFORM 81 84 - -DIDF_VER=* 82 - - -I* 83 85 - -std=gnu* 86 + Diagnostics: 87 + # Suppress include-cleaner "unused include" noise in test files. 88 + # Test files often include headers for documentation purposes or because 89 + # Unity macros indirectly use them. 90 + UnusedIncludes: None
+384 -107
.github/agents/test-writer.agent.md
··· 1 1 --- 2 2 name: Test Writer 3 - description: "Test engineer for ESP32 embedded firmware. Creates native PC unit tests, on-device integration tests, PlatformIO test framework, ButtonInput and platform abstraction seams, VirtualButtonInput, mock patterns for hardware-dependent code, crypto test vectors." 3 + description: "Test engineer for ESP32 embedded firmware. Creates and rewrites native PC unit tests following §24 of the Kleidos style guide. Covers the 10-section §24 file structure (header, includes, constants, state vars, declarations, setUp/tearDown, tests, fake definitions, helper definitions, main), ARRANGE/ACT/ASSERT body convention, static test functions, spy/helper struct pattern, clock-facade hook, platform stubs, mirror-tree directory layout, and 1:1 source↔suite rule. Also handles on-device integration tests, ButtonInput seams, VirtualButtonInput, mock patterns, and crypto test vectors." 4 4 --- 5 5 6 6 # Test Writer — Kleidos 7 7 8 8 You are a test engineer specializing in embedded firmware testing. 9 - You write tests that run both natively on PC and on-device via PlatformIO. 10 - 11 - ## Mission 12 - 13 - Create and maintain tests for Kleidos firmware. Prioritize native (PC) tests 14 - for pure logic, and on-device tests for hardware-dependent behavior. 9 + You write and rewrite tests that run natively on PC via PlatformIO Unity. 15 10 16 11 ## Skills to load 17 12 ··· 22 17 (`snake_case` files, `k`-constants, `s_` statics) and run the 23 18 verify-before-done order, including `pio test -e native`. 24 19 25 - ## Test Infrastructure 20 + ## §24 Unit Test Style — canonical reference 21 + 22 + `docs/code-style-guide.md` §24 is the **single source of truth**. Read it 23 + before writing or editing any test file. Key rules are summarized below as a 24 + quick reference; the full text in §24 takes precedence. 25 + 26 + --- 27 + 28 + ### §24.0 One-to-one source↔test rule 29 + 30 + Each test suite covers **exactly one production source file** in `src/`. The 31 + suite directory name = `test_` + source file stem. 32 + 33 + ``` 34 + src/vault/brute_force_guard.cpp → test/vault/test_brute_force_guard/ 35 + src/crypto/password_generator.cpp → test/crypto/test_password_generator/ 36 + ``` 37 + 38 + **Exceptions** (document in the suite header): 39 + - Architectural/meta tests that verify cross-file structural rules (e.g. 40 + `test_ble_layering`) have no single corresponding source file. 41 + - Fused suites covering a closely related pair with no meaningful individual 42 + boundary may share one suite. 43 + 44 + --- 45 + 46 + ### §24.1 Directory layout — mirror tree 47 + 48 + `test/` mirrors `src/` by module: 49 + 50 + ``` 51 + test/ 52 + vault/test_vault_crypto/test_vault_crypto.cpp 53 + ble/test_keyboard_layouts/test_keyboard_layouts.cpp 54 + platform/test_safe_string/test_safe_string.cpp 55 + support/fake_ble_stack.h ← shared by ≥2 suites 56 + ``` 57 + 58 + PlatformIO suite path in `test_filter` = path relative to `test/`: 59 + `vault/test_vault_crypto`. Run a single suite with 60 + `pio test -e native --filter "vault/test_vault_crypto"`. 61 + 62 + --- 63 + 64 + ### §24.2 File naming 65 + 66 + | Artifact | Pattern | Example | 67 + | --- | --- | --- | 68 + | Module folder | `<module>/` | `vault/` | 69 + | Suite directory | `test_<suite>/` | `test_vault_crypto/` | 70 + | Main source | `test_<suite>.cpp` | `test_vault_crypto.cpp` | 71 + | Local fake header | `fake_<module>.h` | `fake_hid_sink.h` | 72 + | Shared fake | `test/support/fake_<module>.h` | `fake_ble_stack.h` | 26 73 27 - ### Platform Abstraction Testing 74 + --- 28 75 29 - - Firmware logic should depend on project facades (`src/platform/`) instead of 30 - raw RTOS/logging/platform APIs. Tests should preserve that seam. 31 - - When new platform wrappers are added, validate both ESP builds and native 32 - behavior where possible. If native cannot link the ESP facade, keep the raw 33 - dependency isolated and test the portable caller logic with fakes. 34 - - Do not add raw FreeRTOS types to public APIs just to make tests easier; add a 35 - small facade type or adapter instead. 76 + ### §24.3 Internal file structure — 10 sections 77 + 78 + Every `test_<suite>.cpp` has **exactly these 10 sections** in order. Each 79 + section begins with a **titled separator** — a 78-character line of the form 80 + `// --- <Section name> ` followed by dashes to reach 78 chars: 81 + 82 + ```cpp 83 + // --- Includes -------------------------------------------------------------- 84 + ``` 85 + 86 + The 10 fixed section separators are: 87 + 88 + ```cpp 89 + // --- File header ----------------------------------------------------------- 90 + // --- Includes -------------------------------------------------------------- 91 + // --- Constants ------------------------------------------------------------- 92 + // --- State variables ------------------------------------------------------- 93 + // --- Function declarations ------------------------------------------------- 94 + // --- setUp / tearDown ------------------------------------------------------ 95 + // --- Tests ----------------------------------------------------------------- 96 + // --- Fake function definitions --------------------------------------------- 97 + // --- Helper definitions ---------------------------------------------------- 98 + // --- main() ---------------------------------------------------------------- 99 + ``` 36 100 37 - ### Native Tests (PC, no hardware needed) 101 + Sections with no content for a given suite are kept with their separator and a 102 + blank line — never omitted — so the 10-section shape is always visible. 38 103 39 - - Environment: `[env:native]` in `platformio.ini` 40 - - Platform: `native` (compiles for host OS) 41 - - Framework: PlatformIO Unity test framework 42 - - Location: `test/test_*/test_*.cpp` 43 - - Run: `pio test -e native` 44 - - Key pattern: control time via the `platform/Clock.h` test hook 45 - (`setTestMillisProvider`). Firmware reads time through 46 - `kleidos::platform::clock::millis32()` — the IDF-pure clock facade, **not** 47 - Arduino `millis()` (there is no Arduino core). 104 + ``` 105 + 1. File header — 2–3 // lines; no Doxygen @file block 106 + 2. Includes — 4 groups (see below) 107 + 3. Constants — static constexpr kXxx 108 + 4. State variables — spy struct · helper struct · plain statics 109 + 5. Function declarations — fakes first, then arrange helpers 110 + 6. setUp / tearDown — Unity lifecycle + any hooks 111 + 7. Tests — all static void test_() 112 + 8. Fake function definitions 113 + 9. Helper definitions 114 + 10. main() 115 + ``` 48 116 117 + **File header (section 1):** 49 118 ```cpp 50 - // Deterministic fake time for native tests (IDF-pure clock facade) 119 + // Kleidos — Native test: VaultCrypto (AES-256-CBC, PBKDF2, key derivation) 120 + // Build: pio test -e native -f vault/test_vault_crypto 121 + ``` 122 + 123 + **Include order (section 2) — 4 groups, blank line between groups:** 124 + ```cpp 125 + // Group 1 — module(s) under test 126 + #include "vault/vault_crypto.h" 127 + 128 + // Group 2 — other Kleidos headers this test depends on (omit if none) 51 129 #include "platform/clock.h" 52 - namespace clk = kleidos::platform::clock; 130 + 131 + // Group 3 — stdlib + Unity 132 + #include <cstring> 133 + #include <unity.h> 134 + 135 + // Group 4 — local fakes / helpers (omit if none) 136 + #include "fake_hid_sink.h" 137 + ``` 138 + 139 + --- 140 + 141 + ### §24.4 Test function names 142 + 143 + Pattern: `test_<subject>_<condition>_<expected_result>` (three segments, 144 + `lower_snake_case`). 145 + 146 + ```cpp 147 + // ✓ 148 + static void test_holdbutton_early_release_fires_tap() 149 + static void test_pbkdf2_same_pin_and_salt_produces_equal_keys() 150 + 151 + // ✗ too terse 152 + static void test_tap_detected() 153 + ``` 154 + 155 + Two segments are acceptable for trivial constant checks: 156 + `test_conn_params_match_spec_units`. 157 + 158 + **All test functions are `static`** — they have no external linkage. 159 + 160 + --- 161 + 162 + ### §24.5 Test body — ARRANGE / ACT / ASSERT 163 + 164 + Every test body is divided into three labeled blocks. Each label is preceded by 165 + a **blank line** — including the first label after `{`: 166 + 167 + ```cpp 168 + static void test_vault_encrypt_empty_input_returns_false() { 169 + 170 + // ARRANGE 171 + uint8_t key[kAesKeySize] = {}; 172 + uint8_t iv[kIvSize] = {}; 173 + uint8_t cipher[16] = {}; 174 + size_t cipherLen = 0; 175 + 176 + // ACT 177 + bool ok = VaultCrypto::encrypt( key, nullptr, 0, iv, cipher, &cipherLen ); 178 + 179 + // ASSERT 180 + TEST_ASSERT_FALSE( ok ); 181 + TEST_ASSERT_EQUAL( 0, cipherLen ); 182 + } 183 + ``` 53 184 54 - static uint32_t s_fakeMs = 0; 55 - // install: clk::setTestMillisProvider( [] { return s_fakeMs; } ); 56 - // advance: s_fakeMs += 100; 57 - // teardown: clk::clearTestMillisProvider(); 185 + The combined label `// ARRANGE + ACT` (with its preceding blank line) is 186 + allowed when setup and action are a single expression: 187 + 188 + ```cpp 189 + static void test_popup_queue_empty_on_init_returns_null() { 190 + 191 + // ARRANGE + ACT 192 + auto* item = queue.front(); 193 + 194 + // ASSERT 195 + TEST_ASSERT_NULL( item ); 196 + } 58 197 ``` 59 198 60 - > Some older test files still define a vestigial `extern "C" uint32_t millis()` 61 - > stub from the Arduino era. It is dead code — the clock facade never calls an 62 - > external `millis()`. Prefer the `setTestMillisProvider` hook in new tests. 199 + Never omit the blank line before a label, and never omit labels when ARRANGE is 200 + non-trivial. 63 201 64 - ### On-Device Tests 202 + --- 65 203 66 - - Environment: `[env:m5stickc_plus2_debug]` with `-DDEBUG_SERIAL_BUTTONS` 67 - - Serial commands: PRESS/RELEASE/TAP/HOLD A|B [ms], STATE?, HELP 68 - - Automation: `scripts/serial_button_test.py` (pyserial) 204 + ### §24.6 Spy and helper state structs 69 205 70 - ### What to Test Natively 206 + ```cpp 207 + // --- State variables ------------------------------------------------------- 208 + struct { 209 + int callCount = 0; 210 + uint8_t lastEvent = 0; 211 + } spy; // what the fake observed 71 212 72 - - **Pure logic**: HoldButton state machine, credential parsing, FSM transitions 73 - - **Crypto**: AES-256-CBC round-trip, PBKDF2 key derivation, known test vectors 74 - - **Data**: ArduinoJson serialization/deserialization (header-only, 75 - framework-agnostic — not the Arduino framework), vault format validation 76 - - **Platform callers**: logic that consumes `rtos::Ticks`, facade enums, or 77 - app-level adapters without requiring real FreeRTOS 213 + struct { 214 + bool failNext = false; 215 + uint32_t fakeTimeMs = 0; 216 + } helper; // what the test injects into fakes 217 + ``` 78 218 79 - ### What to Test On-Device 219 + Reset both in `setUp()`: 220 + ```cpp 221 + void setUp() { 222 + spy = {}; 223 + helper = {}; 224 + } 225 + ``` 80 226 81 - - Hardware interaction: display output, button hardware, BLE pairing 82 - - Power measurements: deep sleep current, wake timing 83 - - Integration: full credential flow (unlock → select → type) 227 + --- 84 228 85 - ## Test Patterns 229 + ### §24.7 Arrange helpers 86 230 87 - ### ButtonInput Abstraction 231 + Extract repeated multi-step setup into `static` functions prefixed `arrange`. 232 + Declare in section 5, define in section 9. Single-use setup stays inline. 88 233 89 - The project uses a `ButtonInput` abstract interface: 234 + --- 90 235 91 - - `GpioButtonInput` — reads physical GPIO buttons for production 92 - - `VirtualButtonInput` — `setState(bool)` for programmatic testing 236 + ### §24.8 Unused parameters 93 237 94 238 ```cpp 95 - #include "ui/VirtualButtonInput.h" 96 - #include "ui/HoldButton.h" 239 + int main( int /*argc*/, char** /*argv*/ ) // anonymous 240 + bool send( uint8_t /*mod*/, uint8_t sc ) // anonymous override param 241 + void log( [[maybe_unused]] const char* msg ) // conditionally used 242 + ``` 243 + 244 + **Never** use `(void)param;` — it is not idiomatic C++. 245 + 246 + --- 97 247 98 - VirtualButtonInput btn; 99 - HoldButton hold; 248 + ### §24.9 Shared fakes 100 249 101 - // Simulate press 102 - btn.setState(true); 103 - hold.update(btn); 104 - fakeMillis += 2000; 105 - btn.setState(true); // still pressed 106 - hold.update(btn); 107 - TEST_ASSERT_TRUE(hold.checkHold(1500)); 250 + A fake used by only one suite: inline in section 8 of that suite's `.cpp`. 251 + A fake used by ≥2 suites: promoted to `test/support/fake_<module>.h` 252 + (all definitions `inline` or inside a class body; no `main()`). 253 + 254 + --- 255 + 256 + ### §24.10 Platform stubs 257 + 258 + The native build has no ESP-IDF. Stubs that satisfy linker dependencies go in 259 + the suite's `.cpp` (section 8). 260 + 261 + **Prefer the clock facade hook** when the unit under test uses `platform/clock.h`: 262 + 263 + ```cpp 264 + // ✓ preferred — uses clock facade test hook 265 + static uint32_t s_fakeMs = 0; 266 + static uint32_t fakeMillisProvider() { return s_fakeMs; } 267 + // In setUp(): kleidos::platform::clock::setTestMillisProvider( fakeMillisProvider ); 268 + // In tearDown(): kleidos::platform::clock::clearTestMillisProvider(); 108 269 ``` 109 270 110 - ### Crypto Test Vectors 271 + Use `extern "C" uint32_t millis()` only when a production TU calls `millis()` 272 + **directly** (e.g. `HoldButton.cpp`). Always add a comment: 273 + ```cpp 274 + // HoldButton.cpp calls millis() directly; stub satisfies linker. 275 + extern "C" uint32_t millis() { return s_fakeMs; } 276 + ``` 277 + 278 + --- 111 279 112 - Use known-answer tests for crypto validation: 280 + ## Complete §24-compliant template 113 281 114 282 ```cpp 115 - // AES-256-CBC with known key, IV, plaintext → expected ciphertext 116 - const uint8_t key[32] = { /* known test key */ }; 117 - const uint8_t iv[16] = { /* known test IV */ }; 118 - const char* plaintext = "test credential"; 119 - const uint8_t expected_ct[] = { /* expected ciphertext */ }; 283 + // Kleidos — Native test: <Module> (<brief description>) 284 + // Build: pio test -e native -f <module>/test_<suite> 285 + 286 + // --- Includes -------------------------------------------------------------- 287 + #include "<module>/<header>.h" 288 + 289 + #include <cstdint> 290 + #include <unity.h> 291 + 292 + // --- Constants ------------------------------------------------------------- 293 + static constexpr uint32_t kFoo = 42U; 294 + 295 + // --- State variables ------------------------------------------------------- 296 + static uint32_t s_fakeMs = 0; 297 + 298 + struct { 299 + int callCount = 0; 300 + } spy; 301 + 302 + struct { 303 + bool failNext = false; 304 + } helper; 305 + 306 + // --- Function declarations ------------------------------------------------- 307 + // fakes 308 + static uint32_t fakeMillisProvider(); 309 + 310 + // arrange helpers 311 + static void arrangeFilled(); 312 + 313 + // --- setUp / tearDown ------------------------------------------------------ 314 + void setUp() { 315 + s_fakeMs = 0; 316 + spy = {}; 317 + helper = {}; 318 + kleidos::platform::clock::setTestMillisProvider( fakeMillisProvider ); 319 + } 320 + 321 + void tearDown() { 322 + kleidos::platform::clock::clearTestMillisProvider(); 323 + } 324 + 325 + // --- Tests ----------------------------------------------------------------- 326 + static void test_subject_condition_expected_result() { 327 + 328 + // ARRANGE 329 + arrangeFilled(); 330 + 331 + // ACT 332 + bool ok = doThing(); 333 + 334 + // ASSERT 335 + TEST_ASSERT_TRUE( ok ); 336 + TEST_ASSERT_EQUAL( 1, spy.callCount ); 337 + } 338 + 339 + // --- Fake function definitions --------------------------------------------- 340 + static uint32_t fakeMillisProvider() { return s_fakeMs; } 341 + 342 + // --- Helper definitions ---------------------------------------------------- 343 + static void arrangeFilled() { 344 + // ... 345 + } 346 + 347 + // --- main() ---------------------------------------------------------------- 348 + int main( int /*argc*/, char** /*argv*/ ) { 349 + UNITY_BEGIN(); 350 + RUN_TEST( test_subject_condition_expected_result ); 351 + return UNITY_END(); 352 + } 120 353 ``` 121 354 122 - ### Build Source Filter 355 + --- 356 + 357 + ## Test Infrastructure 358 + 359 + ### Native Tests (PC, no hardware) 360 + 361 + - Environment: `[env:native]` in `platformio.ini` 362 + - Framework: PlatformIO + Unity 363 + - Run all: `pio test -e native` 364 + - Run one suite: `pio test -e native --filter "<module>/test_<suite>"` 365 + - Build source filter in `platformio.ini [env:native]` controls which `src/` 366 + files compile for the native build. 367 + 368 + ### What to test natively 369 + 370 + - **Pure logic**: FSM transitions, HoldButton, credential parsing 371 + - **Crypto**: AES-256-CBC round-trip, PBKDF2 derivation, HMAC, known vectors 372 + - **Data**: ArduinoJson serialization (framework-agnostic, not Arduino) 373 + - **Platform callers**: logic that consumes facade types without real FreeRTOS 374 + 375 + ### What to test on-device only 123 376 124 - For native tests, only include testable source files: 377 + - Hardware interaction: display, BLE pairing, physical buttons 378 + - Power measurements: deep sleep current, wake timing 379 + - Integration: full credential flow (unlock → select → type) 380 + 381 + ### ButtonInput abstraction 382 + 383 + ```cpp 384 + #include "ui/input/virtual_button_input.h" 385 + #include "ui/input/hold_button.h" 125 386 126 - ```ini 127 - build_src_filter = -<*> +<ui/VirtualButtonInput.cpp> +<ui/HoldButton.cpp> 387 + VirtualButtonInput btn; 388 + HoldButton hold; 389 + 390 + btn.setState( true ); 391 + hold.update( btn ); 392 + s_fakeMs += 2000; 393 + hold.update( btn ); 394 + TEST_ASSERT_TRUE( hold.checkHold( 1500 ) ); 128 395 ``` 129 396 130 - ## Test Naming Convention 397 + ### Crypto known-answer vectors 131 398 132 - ```text 133 - test/ 134 - test_holdbutton/ 135 - test_holdbutton.cpp # HoldButton state machine tests 136 - test_crypto_native/ 137 - test_crypto_native.cpp # AES-256, PBKDF2 round-trip tests 138 - test_vault_json/ 139 - test_vault_json.cpp # JSON serialization, vault format tests 399 + ```cpp 400 + const uint8_t kKey[32] = { /* known test key */ }; 401 + const uint8_t kIv[16] = { /* known test IV */ }; 140 402 ``` 141 403 404 + --- 405 + 142 406 ## Process 143 407 144 - ### 1. Identify Testable Logic 408 + ### 1. Identify testable logic 145 409 146 - - Read the source module 147 - - Separate pure logic from hardware dependencies 148 - - Identify existing `ButtonInput`-style abstractions or create new ones 149 - - Identify raw platform APIs; if the code would need a fake, it probably needs 150 - a facade under `src/platform/` or a subsystem adapter first 410 + - Read the source module. 411 + - Separate pure logic from hardware dependencies. 412 + - Identify or create `ButtonInput`-style abstractions. 413 + - Identify raw platform calls; if a fake would be needed, the code probably 414 + needs a facade under `src/platform/` first. 151 415 152 - ### 2. Write Tests 416 + ### 2. Write or rewrite tests 153 417 154 - - One `TEST_ASSERT_*` per behavior, not per line 155 - - Test edge cases: empty input, max-size input, timing boundaries 156 - - Use descriptive test names: `test_hold_fires_at_threshold` 418 + - Follow the 10-section §24 structure exactly. 419 + - One `TEST_ASSERT_*` per observable behavior. 420 + - Test edge cases: empty input, max-size, timing boundaries. 421 + - Name every test with the three-segment `test_<subject>_<condition>_<result>` 422 + pattern. 423 + - All test functions are `static`. 424 + - Every body has `// ARRANGE`, `// ACT`, `// ASSERT`. 157 425 158 426 ### 3. Verify 159 427 160 - - Native: `pio test -e native` — must pass with 0 failures 161 - - Debug: `pio run -e m5stickc_plus2_debug` — must compile 162 - - Production: `pio run -e m5stickc_plus2` — must compile (tests excluded) 428 + ``` 429 + pio test -e native # must pass, 0 failures 430 + pio run -e sticks3_debug # must compile (tests excluded) 431 + clang-format -i --style=file <changed files> 432 + ``` 433 + 434 + --- 163 435 164 436 ## Boundaries 165 437 166 - - ✅ Always: Write tests that pass deterministically (no flaky timing) 167 - - ✅ Always: Use VirtualButtonInput for button logic tests 168 - - ✅ Always: Use the `platform/Clock` test hook (`setTestMillisProvider`) for 169 - time-dependent tests 170 - - ✅ Always: Preserve platform abstraction seams instead of exposing native RTOS 171 - or ESP-IDF types to tests 438 + - ✅ Always: Follow §24 structure (10 titled separators, naming, static) 439 + - ✅ Always: ARRANGE/ACT/ASSERT in every non-trivial test body 440 + - ✅ Always: `spy = {}; helper = {};` in `setUp()` 441 + - ✅ Always: `int main( int /*argc*/, char** /*argv*/ )` 442 + - ✅ Always: Clock facade hook over raw `millis()` stub when possible 443 + - ✅ Always: `static constexpr kXxx` for constants (no magic numbers) 444 + - ✅ Always: Write deterministic tests (no flaky timing) 445 + - ✅ Always: Use `VirtualButtonInput` for button logic tests 446 + - ✅ Always: Preserve platform abstraction seams 172 447 - ⚠️ Ask first: Adding new test dependencies or frameworks 173 - - 🚫 Never: Write tests that require hardware to pass in native env 174 - - 🚫 Never: Include credential data in test fixtures (use synthetic test data) 448 + - 🚫 Never: `(void)param;` — use anonymous params or `[[maybe_unused]]` 449 + - 🚫 Never: Test functions without `static` 450 + - 🚫 Never: Tests that require hardware in the native env 451 + - 🚫 Never: Credential data in test fixtures (use synthetic data)
+21
.github/skills/code-quality/SKILL.md
··· 116 116 changes. 117 117 6. `pre-commit run --all-files` as the final pass before committing. 118 118 119 + ## Unit test style (§24) 120 + 121 + When writing or editing **any** test file under `test/`, follow 122 + `docs/code-style-guide.md` §24. Key rules: 123 + 124 + - **Mirror tree**: `test/<module>/test_<suite>/test_<suite>.cpp` mirrors 125 + `src/<module>/<suite>.cpp`. One suite per source file (§24.0). 126 + - **10-section structure** with **titled 78-char separators** `// --- <Name> ---…` 127 + (§24.3): 1 header · 2 includes · 3 constants · 4 state vars · 5 declarations · 128 + 6 setUp/tearDown · 7 tests · 8 fake defs · 9 helper defs · 10 main() 129 + - **All test functions are `static`** (§24.4). 130 + - **Three-part names**: `test_<subject>_<condition>_<result>` (§24.4). 131 + - **ARRANGE / ACT / ASSERT** comments in every non-trivial test body, each 132 + preceded by a **blank line** — including the first inside `{` (§24.5). 133 + - **`spy` / `helper` structs** reset with `= {}` in `setUp()` (§24.6). 134 + - **`int main( int /*argc*/, char** /*argv*/ )`** — never `(void)param;` (§24.8). 135 + - **Clock facade hook** preferred over raw `millis()` stub (§24.10). 136 + 137 + Load the `test-writer` agent (`.claude/agents/test-writer.md`) for the full §24 138 + quick-reference and a complete file template. 139 + 119 140 ## Suppressing findings (only with a documented reason) 120 141 121 142 - clang-tidy: scoped `// NOLINT(check-name)` or `// NOLINTNEXTLINE(...)` with a
+8 -7
.vscode/settings.json
··· 46 46 "files.watcherExclude": { 47 47 "**/.pio/**": true, 48 48 "**/build/**": true, 49 + "**/build/cmake-*/**": true, 49 50 "**/managed_components/**": true, 50 51 "**/.git/**": true 51 52 }, ··· 54 55 "sonarlint.pathToCompileCommands": "${workspaceFolder}/compile_commands.json", 55 56 56 57 // --- CMake Tools (ms-vscode.cmake-tools) --- 57 - // This is a PlatformIO/ESP-IDF project: the root CMakeLists.txt does 58 - // `include($ENV{IDF_PATH}/tools/cmake/project.cmake)` and only configures 59 - // correctly inside the PlatformIO/IDF environment (where IDF_PATH is set). 60 - // CMake Tools must NOT drive the configure itself, otherwise it runs cmake 61 - // without IDF_PATH and fails with "could not find .../tools/cmake/project.cmake" 62 - // and "Unknown CMake command idf_build_set_property". Builds go through 63 - // PlatformIO, so disable all automatic CMake Tools configuration here. 58 + // This is a PlatformIO/ESP-IDF project. CMakePresets.json defines per-device 59 + // presets that set IDF_PATH, IDF_TARGET, the xtensa toolchain and the Python 60 + // venv from PlatformIO's managed ESP-IDF 5.5.4 installation. Select the 61 + // desired preset in the CMake Tools kit selector, then configure/build from 62 + // within VS Code. Auto-configure is kept off so the preset selection comes 63 + // first, avoiding a no-IDF_PATH failure on workspace open. 64 + "cmake.useCMakePresets": "always", 64 65 "cmake.configureOnOpen": false, 65 66 "cmake.configureOnEdit": false, 66 67 "cmake.automaticReconfigure": false,
+383
docs/code-style-guide.md
··· 71 71 - [A file-local module with owned state (`bond_store.cpp`)](#a-file-local-module-with-owned-state-bond_storecpp) 72 72 - [Cross-module access without a global](#cross-module-access-without-a-global) 73 73 - [23. Enforcement](#23-enforcement) 74 + - [24. Unit test conventions](#24-unit-test-conventions) 75 + - [24.0 One-to-one source↔test rule](#240-one-to-one-sourcetest-rule) 76 + - [24.1 Directory layout](#241-directory-layout) 77 + - [24.2 File naming](#242-file-naming) 78 + - [24.3 Internal file structure](#243-internal-file-structure) 79 + - [24.4 Test function names](#244-test-function-names) 80 + - [24.5 Test body — Arrange / Act / Assert](#245-test-body--arrange--act--assert) 81 + - [24.6 Spy and helper state structs](#246-spy-and-helper-state-structs) 82 + - [24.7 Arrange helpers for repeated setup](#247-arrange-helpers-for-repeated-setup) 83 + - [24.8 Unused parameters](#248-unused-parameters) 84 + - [24.9 Shared fakes and support files](#249-shared-fakes-and-support-files) 85 + - [24.10 Platform stubs](#2410-platform-stubs) 74 86 75 87 --- 76 88 ··· 875 887 - No raw `new`/`delete` and no owning raw pointers in application code; stack 876 888 allocation is preferred (firmware: avoid heap fragmentation). *(project 877 889 memory-safety rule)* 890 + - **Unused parameters**: to suppress warnings for a parameter that is never used 891 + in a definition, comment out its name — `void foo( int /*count*/ )`. Never 892 + use the C idiom `(void)param;` inside the function body — it is not idiomatic 893 + C++. For a parameter used only under `#ifdef`, use `[[maybe_unused]]`: 894 + `void log( [[maybe_unused]] const char* msg )`. This rule applies everywhere 895 + in the codebase, including test files. 878 896 879 897 --- 880 898 ··· 983 1001 When this document and the tools disagree, fix the document (or, if the tool is 984 1002 wrong, fix the tool **and** the document in the same change). Code, comments, and 985 1003 identifiers are **English only**. 1004 + 1005 + --- 1006 + 1007 + ## 24. Unit test conventions 1008 + 1009 + Kleidos uses **PlatformIO + Unity** for native (host-PC) unit tests. These 1010 + conventions define how every test suite is laid out so any developer can navigate 1011 + from source to test and back without guessing. 1012 + 1013 + --- 1014 + 1015 + ### 24.0 One-to-one source↔test rule 1016 + 1017 + Each test suite corresponds to **exactly one source file** in `src/`. The suite 1018 + directory name is derived by prefixing the source file stem with `test_`: 1019 + 1020 + | Source file | Suite directory | 1021 + | --- | --- | 1022 + | `src/vault/brute_force_guard.cpp` | `test/vault/test_brute_force_guard/` | 1023 + | `src/crypto/password_generator.cpp` | `test/crypto/test_password_generator/` | 1024 + | `src/hal/shake_detector.cpp` | `test/hal/test_shake_detector/` | 1025 + 1026 + **Do not name a test suite after the module** (e.g. `test_vault`) or after a 1027 + concept that spans many files. Keep the scope to one production source file. 1028 + 1029 + **Exceptions (must be documented in the suite header):** 1030 + 1031 + - *Architectural/meta tests* that verify structural rules across the codebase 1032 + (e.g. `test_ble_layering` checks that BLE layers do not depend upward) have 1033 + no single corresponding source file and are exempt. 1034 + - *Fused suites* that cover a closely-related pair of files that have no 1035 + individually meaningful boundary may share one suite. The header comment must 1036 + list all source files covered. 1037 + 1038 + --- 1039 + 1040 + ### 24.1 Directory layout 1041 + 1042 + Tests live in `test/` as a **mirror tree** of `src/`. Each module folder in 1043 + `test/` matches the top-level module directory in `src/`. Inside each module 1044 + folder, every suite gets its own subdirectory named `test_<suite>/`: 1045 + 1046 + ``` 1047 + src/ 1048 + vault/ 1049 + vault_crypto.h 1050 + vault_crypto.cpp 1051 + ble/ 1052 + profile/ 1053 + hid_typer.h 1054 + platform/ 1055 + clock.h 1056 + 1057 + test/ 1058 + vault/ 1059 + test_vault_crypto/ 1060 + test_vault_crypto.cpp 1061 + ble/ 1062 + test_ble_hid_typer/ 1063 + test_ble_hid_typer.cpp 1064 + fake_hid_sink.h ← local fake, used only here 1065 + platform/ 1066 + test_clock/ 1067 + test_clock.cpp 1068 + support/ 1069 + fake_ble_stack.h ← shared fake, used by multiple suites 1070 + ``` 1071 + 1072 + - Each **suite directory** is named `test_<suite>/` and lives under the 1073 + matching module folder. 1074 + - The suite directory contains exactly one `test_<suite>.cpp` (which defines 1075 + `main()`) plus any local fakes and helper headers that belong only to it. 1076 + - Cross-suite shared fakes and helpers live in `test/support/`. 1077 + 1078 + PlatformIO identifies each suite by its path relative to `test/`. Use the full 1079 + relative path in `test_filter`: `pio test -e native --filter "vault/test_vault_crypto"`. 1080 + To run a whole module: `pio test -e native --filter "ble/*"`. 1081 + 1082 + --- 1083 + 1084 + ### 24.2 File naming 1085 + 1086 + | Artifact | Name pattern | Example | 1087 + | --- | --- | --- | 1088 + | Module folder | `<module>/` | `vault/`, `ble/` | 1089 + | Suite directory | `test_<suite>/` | `test_vault_crypto/` | 1090 + | Main test source | `test_<suite>.cpp` | `test_vault_crypto.cpp` | 1091 + | Local fake header | `fake_<module>.h` | `fake_hid_sink.h` | 1092 + | Local helper header | `helpers_<suite>.h` | `helpers_vault.h` (rare) | 1093 + | Shared fake / helper | `test/support/fake_<module>.h` | `fake_ble_stack.h` | 1094 + 1095 + A local fake used only within one suite may be: 1096 + - kept fully **inline** in the `.cpp` (declaration in section 5, definition in 1097 + section 8) when it is small (< ~30 lines or a simple struct), or 1098 + - extracted to a `fake_<module>.h` file **in the suite directory** when it is 1099 + a class or reused by multiple `.cpp` files compiled together. 1100 + 1101 + A fake used by **two or more** suite directories moves to `test/support/`. 1102 + 1103 + --- 1104 + 1105 + ### 24.3 Internal file structure 1106 + 1107 + Every `test_<suite>.cpp` has **exactly these 10 sections** in order. Each 1108 + section begins with a **titled separator** — a 78-character line of the form 1109 + `// --- <Section name> ` followed by dashes to reach 78 chars: 1110 + 1111 + ```cpp 1112 + // --- Includes -------------------------------------------------------------- 1113 + ``` 1114 + 1115 + The section names and their exact separators are fixed: 1116 + 1117 + ```cpp 1118 + // --- File header ----------------------------------------------------------- 1119 + // --- Includes -------------------------------------------------------------- 1120 + // --- Constants ------------------------------------------------------------- 1121 + // --- State variables ------------------------------------------------------- 1122 + // --- Function declarations ------------------------------------------------- 1123 + // --- setUp / tearDown ------------------------------------------------------ 1124 + // --- Tests ----------------------------------------------------------------- 1125 + // --- Fake function definitions --------------------------------------------- 1126 + // --- Helper definitions ---------------------------------------------------- 1127 + // --- main() ---------------------------------------------------------------- 1128 + ``` 1129 + 1130 + Sections that have no content for a given suite are kept with the separator and 1131 + a blank line — never omitted — so the 10-section shape is always visible. 1132 + 1133 + ``` 1134 + 1. File header — 2–3 // lines (see §24.3.1) 1135 + 2. Includes — 4 groups (see §24.3.2) 1136 + 3. Constants — static constexpr kXxx 1137 + 4. State variables — spy struct · helper struct · plain statics 1138 + 5. Function declarations — fakes first, then arrange helpers 1139 + 6. setUp / tearDown — Unity lifecycle + any hooks 1140 + 7. Tests — all static void test_...() 1141 + 8. Fake function definitions 1142 + 9. Helper definitions 1143 + 10. main() 1144 + ``` 1145 + 1146 + #### 24.3.1 File header 1147 + 1148 + Test files use a **short plain header** (not the full Doxygen `@file` block 1149 + reserved for production headers). Two or three `//` lines suffice: 1150 + 1151 + ```cpp 1152 + // Kleidos — Native test: VaultCrypto (AES-256-CBC, PBKDF2, key derivation) 1153 + // Build: pio test -e native -f vault/test_vault_crypto 1154 + ``` 1155 + 1156 + #### 24.3.2 Include order 1157 + 1158 + Four groups, each separated by a blank line: 1159 + 1160 + ```cpp 1161 + // Group 1 — module(s) under test 1162 + #include "vault/vault_crypto.h" 1163 + 1164 + // Group 2 — other Kleidos headers this test depends on (omit if none) 1165 + #include "platform/clock.h" 1166 + 1167 + // Group 3 — stdlib + Unity 1168 + #include <cstring> 1169 + #include <unity.h> 1170 + 1171 + // Group 4 — local fakes / helpers (omit if none) 1172 + #include "fake_hid_sink.h" 1173 + ``` 1174 + 1175 + --- 1176 + 1177 + ### 24.4 Test function names 1178 + 1179 + ``` 1180 + test_<subject>_<condition>_<expected_result> 1181 + ``` 1182 + 1183 + All three segments use `lower_snake_case`. The subject names the unit or 1184 + behavior under test; the condition names the input state or trigger; the 1185 + expected result names the observable outcome. 1186 + 1187 + ```cpp 1188 + // ✓ three segments — reads like a sentence 1189 + static void test_holdbutton_early_release_fires_tap() 1190 + static void test_pbkdf2_same_pin_and_salt_produces_equal_keys() 1191 + static void test_vault_encrypt_empty_input_returns_false() 1192 + 1193 + // ✗ too terse — condition and result are not clear 1194 + static void test_tap_detected() 1195 + static void test_aes_roundtrip() 1196 + ``` 1197 + 1198 + When the test verifies a trivial constant or compile-time property, two segments 1199 + are acceptable: `test_conn_params_match_spec_units`. 1200 + 1201 + All test functions are **`static`** — they are only called from `main()` in the 1202 + same TU and must not have external linkage. 1203 + 1204 + --- 1205 + 1206 + ### 24.5 Test body — Arrange / Act / Assert 1207 + 1208 + Every test body is divided into three labeled blocks. Each label (`// ARRANGE`, 1209 + `// ACT`, `// ASSERT`) is preceded by a **blank line** so each phase is visually 1210 + distinct — even when it is the first line after `{`: 1211 + 1212 + ```cpp 1213 + static void test_vault_encrypt_empty_input_returns_false() { 1214 + 1215 + // ARRANGE 1216 + uint8_t key[kAesKeySize] = {}; 1217 + uint8_t iv[kIvSize] = {}; 1218 + uint8_t cipher[16] = {}; 1219 + size_t cipherLen = 0; 1220 + 1221 + // ACT 1222 + bool ok = VaultCrypto::encrypt( key, nullptr, 0, iv, cipher, &cipherLen ); 1223 + 1224 + // ASSERT 1225 + TEST_ASSERT_FALSE( ok ); 1226 + TEST_ASSERT_EQUAL( 0, cipherLen ); 1227 + } 1228 + ``` 1229 + 1230 + The combined label `// ARRANGE + ACT` (with its preceding blank line) is allowed 1231 + when setup and action are a single expression: 1232 + 1233 + ```cpp 1234 + static void test_popup_queue_empty_on_init_returns_null() { 1235 + 1236 + // ARRANGE + ACT 1237 + auto* item = queue.front(); 1238 + 1239 + // ASSERT 1240 + TEST_ASSERT_NULL( item ); 1241 + } 1242 + ``` 1243 + 1244 + Never omit the blank line before a label, and never omit the labels themselves 1245 + when ARRANGE is non-trivial or when setup could be confused with assertions. 1246 + 1247 + --- 1248 + 1249 + ### 24.6 Spy and helper state structs 1250 + 1251 + **Spy variables** capture what a fake *observed* (outputs that flowed into it). 1252 + **Helper variables** steer what a fake *does* (inputs the test injects). 1253 + 1254 + Group each into a named `struct` so all accesses use a qualified name and the 1255 + purpose stays unambiguous at the call site: 1256 + 1257 + ```cpp 1258 + // --- State variables ------------------------------------------------------- 1259 + struct { 1260 + int callCount = 0; 1261 + uint8_t lastEvent = 0; 1262 + bool called = false; 1263 + } spy; // what the fake observed 1264 + 1265 + struct { 1266 + bool failNext = false; 1267 + uint32_t fakeTimeMs = 0; 1268 + } helper; // what the test injects into fakes 1269 + ``` 1270 + 1271 + Reset both in `setUp()` using aggregate initialization: 1272 + 1273 + ```cpp 1274 + void setUp() { 1275 + spy = {}; 1276 + helper = {}; 1277 + } 1278 + ``` 1279 + 1280 + Plain `static` variables (outside a struct) are acceptable when only one or two 1281 + values are needed and they clearly belong to neither category. 1282 + 1283 + --- 1284 + 1285 + ### 24.7 Arrange helpers for repeated setup 1286 + 1287 + When the same multi-step setup appears in two or more tests, extract it into a 1288 + static function with the `arrange` prefix. Place its **declaration** in section 5 1289 + (function declarations) and its **definition** in section 9 (helper definitions): 1290 + 1291 + ```cpp 1292 + // Section 5 — declaration 1293 + static void arrangeEmptyLog(); 1294 + static void arrangeLogWithEntries( uint8_t count ); 1295 + 1296 + // ... 1297 + 1298 + // Section 9 — definition 1299 + static void arrangeEmptyLog() { 1300 + simClear(); 1301 + } 1302 + 1303 + static void arrangeLogWithEntries( uint8_t count ) { 1304 + simClear(); 1305 + for ( uint8_t i = 0; i < count; ++i ) { 1306 + helper.fakeTimeMs = static_cast<uint32_t>( i + 1 ) * 100U; 1307 + simRecord( AuditEvent::PIN_OK, i ); 1308 + } 1309 + } 1310 + ``` 1311 + 1312 + Setup that is used in only **one** test stays inlined inside its `// ARRANGE` 1313 + block — do not extract single-use setup. 1314 + 1315 + --- 1316 + 1317 + ### 24.8 Unused parameters 1318 + 1319 + | Situation | Idiom | Example | 1320 + | --- | --- | --- | 1321 + | Parameter never needed in this definition | Anonymous (comment the name) | `int main( int /*argc*/, char** /*argv*/ )` | 1322 + | Parameter conditionally used under `#ifdef` | `[[maybe_unused]]` attribute | `void log( [[maybe_unused]] const char* msg )` | 1323 + | Override that ignores a base-class parameter | Anonymous in the override signature | `bool send( uint8_t /*mod*/, uint8_t sc ) override` | 1324 + 1325 + Never use the C cast idiom `(void)param;` — it is not idiomatic C++. 1326 + 1327 + > This rule applies to **all Kleidos C++**, not only test files — see §19. 1328 + 1329 + --- 1330 + 1331 + ### 24.9 Shared fakes and support files 1332 + 1333 + A fake used by only **one** suite is declared and defined inside that suite's 1334 + `.cpp` (declaration in section 5, definition in section 8). 1335 + 1336 + A fake used by **two or more** suites is promoted to `test/support/` as a 1337 + header-only file (all definitions `inline` or inside a class body). 1338 + `test/support/` is **not** a PlatformIO test suite — it contains no `main()` and 1339 + is never run directly. 1340 + 1341 + ``` 1342 + test/support/ 1343 + fake_ble_stack.h ← FakeBleStack class, fully defined inline 1344 + fake_nvs_store.h ← (future example) 1345 + ``` 1346 + 1347 + --- 1348 + 1349 + ### 24.10 Platform stubs 1350 + 1351 + The native build has no ESP-IDF or FreeRTOS. Stubs that satisfy linker 1352 + dependencies belong in the suite's `.cpp`. Prefer the 1353 + `kleidos::platform::clock::setTestMillisProvider()` hook over a raw 1354 + `extern "C" millis()` stub whenever the unit under test already uses 1355 + `platform/clock.h`: 1356 + 1357 + ```cpp 1358 + // ✓ preferred — uses the clock facade test hook 1359 + static uint32_t s_fakeMs = 0; 1360 + static uint32_t fakeMillisProvider() { return s_fakeMs; } 1361 + // called in setUp(): kleidos::platform::clock::setTestMillisProvider( fakeMillisProvider ); 1362 + 1363 + // ✗ legacy — only valid for TUs that call millis() directly (HoldButton.cpp) 1364 + extern "C" uint32_t millis() { return s_fakeMs; } 1365 + ``` 1366 + 1367 + Document every stub with a one-line comment explaining which production TU 1368 + requires it.
+33 -33
platformio.ini
··· 50 50 -DCORE_DEBUG_LEVEL=1 51 51 -DKLEIDOS_VERSION=\"0.1.0\" 52 52 -DKLEIDOS_LOG_PRINTF_WRAP_STUB 53 + -DESP_PLATFORM 53 54 -ffunction-sections 54 55 -fdata-sections 55 56 -Wl,--gc-sections ··· 178 179 +<ble/connection/connection_manager.cpp> 179 180 +<ble/bond/name_store.cpp> 180 181 test_filter = 181 - test_holdbutton 182 - test_vault_json 183 - test_bruteforce 184 - test_bruteforce_ext 185 - test_totp 186 - test_totp_extended 187 - test_passgen 188 - test_crypto_native 189 - test_keyboard_layouts 190 - test_audit_logic 191 - test_safe_string 192 - test_shake_logic 193 - test_vault_menu 194 - test_vault_v2 195 - test_onboarding_logic 196 - test_brand_registry 197 - test_popup_logic 198 - test_status_notifier 199 - test_vault_detail_logic 200 - test_settings_logic 201 - test_change_pin_logic 202 - test_ota_state_logic 203 - test_rekey_recovery 204 - test_keyboard_input 205 - test_admin_idle_logic 206 - test_vault_task 207 - test_ble_facade_fake 208 - test_ble_layering 209 - test_ble_hid_typer 210 - test_ble_event_bus 211 - test_ble_numeric_comparison 212 - test_ble_connection_manager 213 - test_ble_namestore 182 + ui/test_hold_button 183 + vault/test_vault_store 184 + vault/test_brute_force_guard 185 + totp/test_totp_generator 186 + totp/test_totp_generator_extended 187 + crypto/test_password_generator 188 + crypto/test_vault_crypto 189 + ble/test_keyboard_layouts 190 + vault/test_audit_log 191 + platform/test_safe_string 192 + hal/test_shake_detector 193 + states/test_vault_state 194 + states/test_onboarding_logic 195 + ui/test_brand_registry 196 + ui/test_popup_queue 197 + ui/test_status_notifier 198 + states/test_cred_detail_logic 199 + states/test_settings_nav_logic 200 + states/test_change_pin_logic 201 + ota/test_ota_state_logic 202 + vault/test_vault_recovery_logic 203 + hal/test_keyboard_input 204 + web/test_admin_idle_logic 205 + vault/test_vault_task 206 + ble/test_ble_facade 207 + ble/test_ble_layering 208 + ble/test_hid_typer 209 + ble/test_ble_event_bus 210 + ble/test_ble_numeric_comparison 211 + ble/test_ble_connection_manager 212 + ble/test_name_store 213 + hal/test_display_graphics 214 214 lib_deps = 215 215 throwtheswitch/Unity @ ^2.6.1 216 216 bblanchon/ArduinoJson @ ^7.4.3
+6 -1
sonar-project.properties
··· 60 60 sonar.cfamily.cache.path=.sonar/cache 61 61 62 62 # --- Issue tracking --- 63 - sonar.issue.ignore.multicriteria=e1,e2,e3 63 + sonar.issue.ignore.multicriteria=e1,e2,e3,e4 64 64 65 65 # Ignore "cognitive complexity" in Arduino setup()/loop() patterns 66 66 sonar.issue.ignore.multicriteria.e1.ruleKey=cpp:S3776 ··· 73 73 # Ignore include order issues in Arduino headers 74 74 sonar.issue.ignore.multicriteria.e3.ruleKey=cpp:S1130 75 75 sonar.issue.ignore.multicriteria.e3.resourceKey=src/**/*.h 76 + 77 + # Ignore empty tearDown() functions in Unity test files — intentionally empty 78 + # per the Unity framework contract (required signature, no cleanup needed). 79 + sonar.issue.ignore.multicriteria.e4.ruleKey=c:S1186 80 + sonar.issue.ignore.multicriteria.e4.resourceKey=test/**/*.cpp 76 81 77 82 # --- Links --- 78 83 sonar.links.homepage=https://github.com/jmrplens/kleidos
+43
test/.clang-tidy
··· 1 + # clang-tidy overrides for test/** — native Unity test suite 2 + # 3 + # Inherits all rules from the project root and adds only the minimum 4 + # exceptions required by the Unity test framework and host-only test code. 5 + # 6 + # Checks disabled here are false positives or genuinely inappropriate in a 7 + # test context; production code (src/) is still held to the full rule set. 8 + # 9 + # cert-err33-c 10 + # Return-value checks on fopen/printf/snprintf in test helpers are 11 + # intentionally omitted — these are not production I/O paths. 12 + # cert-dcl50-cpp 13 + # Variadic helpers used in safe_string and format tests. 14 + # cppcoreguidelines-pro-type-cstyle-cast 15 + # Test fixtures use C-style casts for concise byte-level assertions 16 + # (e.g. (uint8_t) literals passed to Unity macros). 17 + # bugprone-argument-comment 18 + # Unity macro invocations use /*= value */ style comments that 19 + # intentionally document positional arguments. 20 + # bugprone-misplaced-widening-cast 21 + # bugprone-implicit-widening-of-multiplication-result 22 + # Constant-expression test-data arithmetic; no runtime overflow risk. 23 + # misc-unused-using-decls 24 + # Test files import using-declarations for readability even when not 25 + # every declaration is referenced in every test function. 26 + 27 + InheritParentConfig: true 28 + 29 + Checks: >- 30 + -cert-err33-c, 31 + -cert-dcl50-cpp, 32 + -cppcoreguidelines-pro-type-cstyle-cast, 33 + -bugprone-argument-comment, 34 + -bugprone-misplaced-widening-cast, 35 + -bugprone-implicit-widening-of-multiplication-result, 36 + -misc-unused-using-decls 37 + 38 + CheckOptions: 39 + # Allow Unity test functions (test_*) to use snake_case. 40 + # All other functions (setUp, tearDown, main, helpers) remain valid under 41 + # the inherited FunctionCase: camelBack rule. 42 + - key: readability-identifier-naming.FunctionIgnoredRegexp 43 + value: '^test_'
+271
test/ble/test_ble_layering/test_ble_layering.cpp
··· 1 + // Kleidos — Native test: BLE layering boundary (§B1 NimBLE encapsulation) 2 + // Build: pio test -e native -f ble/test_ble_layering 3 + // 4 + // Enforces the §B1 contract: NimBLE is an implementation detail of 5 + // `src/ble/`. Code in `src/states/`, `src/ui/`, `src/web/` must reach BLE 6 + // only through `BleFacade.h` — never through `<NimBLE*.h>` or any 7 + // `nimble*` IDF header. The test scans the source tree line-by-line and 8 + // records every forbidden include it finds. Fails on any match. 9 + 10 + // --- Includes -------------------------------------------------------------- 11 + // Group 3: stdlib + Unity 12 + #include <cstdint> 13 + 14 + #include <unity.h> 15 + 16 + // Group 4: scan helpers 17 + #include <algorithm> 18 + #include <cstring> 19 + #include <filesystem> 20 + #include <fstream> 21 + #include <regex> 22 + #include <string> 23 + #include <vector> 24 + 25 + // --- Constants ------------------------------------------------------------- 26 + namespace fs = std::filesystem; 27 + 28 + static const std::vector<std::string> kScanDirs = { 29 + "src/states", 30 + "src/ui", 31 + "src/web", 32 + }; 33 + 34 + static const std::vector<std::string> kAllSourceDirs = { 35 + "src", 36 + }; 37 + 38 + static const std::vector<std::regex> kForbiddenPatterns = { 39 + std::regex( R"(#\s*include\s*[<"]NimBLE[A-Za-z0-9_]*\.h[>"])" ), 40 + std::regex( R"(#\s*include\s*[<"]nimble/[A-Za-z0-9_/]+\.h[>"])" ), 41 + std::regex( R"(#\s*include\s*[<"]host/ble_[A-Za-z0-9_]+\.h[>"])" ), 42 + }; 43 + 44 + static const std::vector<std::regex> kForbiddenNimbleBondOpenPatterns = { 45 + std::regex( R"(\.begin\s*\(\s*"nimble_bond")" ), 46 + std::regex( R"(nvs_open(?:_from_partition)?\s*\([^;]*"nimble_bond")" ), 47 + }; 48 + 49 + // --- State variables ------------------------------------------------------- 50 + struct Violation { 51 + std::string path; 52 + int line; 53 + std::string content; 54 + }; 55 + 56 + static uint32_t s_fakeMs = 0; 57 + 58 + // --- Function declarations ------------------------------------------------- 59 + static bool isSourceFile( const fs::path& p ); 60 + static std::string locateRepoRoot(); 61 + static std::vector<Violation> scan( const std::string& root, const std::vector<std::string>& dirs, 62 + const std::vector<std::regex>& patterns ); 63 + static std::vector<Violation> scanForLiteral( const std::string& root, 64 + const std::vector<std::string>& dirs, 65 + const std::string& literal, 66 + const std::string& allowedPath = {} ); 67 + static void failWithViolations( const char* header, const std::vector<Violation>& violations, 68 + const char* footer ); 69 + 70 + // --- setUp / tearDown ------------------------------------------------------ 71 + void setUp() { /* intentionally empty */ } 72 + void tearDown() { /* intentionally empty */ } 73 + 74 + // --- Tests ----------------------------------------------------------------- 75 + static void test_at_least_one_dir_was_scanned() { 76 + // ARRANGE 77 + const std::string root = locateRepoRoot(); 78 + 79 + // ACT 80 + bool anyDir = false; 81 + for ( const auto& rel : kScanDirs ) { 82 + if ( fs::exists( fs::path( root ) / rel ) ) { 83 + anyDir = true; 84 + break; 85 + } 86 + } 87 + 88 + // ASSERT 89 + TEST_ASSERT_TRUE_MESSAGE( anyDir, "No scan directory exists; check repo root." ); 90 + } 91 + 92 + static void test_no_nimble_includes_in_fsm_ui_web() { 93 + // ARRANGE 94 + const std::string root = locateRepoRoot(); 95 + 96 + // ACT 97 + auto v = scan( root, kScanDirs, kForbiddenPatterns ); 98 + 99 + // ASSERT 100 + if ( !v.empty() ) { 101 + failWithViolations( "Forbidden NimBLE includes found:", v, "Use BleFacade.h instead." ); 102 + } 103 + } 104 + 105 + static void test_ble_nimble_headers_stay_in_host_adapter() { 106 + // ARRANGE 107 + const std::string root = locateRepoRoot(); 108 + 109 + // ACT 110 + auto v = scan( root, { "src/ble" }, kForbiddenPatterns ); 111 + v.erase( std::remove_if( v.begin(), v.end(), 112 + []( const Violation& item ) { 113 + return item.path == "src/ble/stack/nimble_host.cpp"; 114 + } ), 115 + v.end() ); 116 + 117 + // ASSERT 118 + if ( !v.empty() ) { 119 + failWithViolations( "NimBLE headers leaked outside host adapter:", v, 120 + "Route runtime operations through NimbleHost.h." ); 121 + } 122 + } 123 + 124 + static void test_ble_code_does_not_reopen_legacy_bond_blob() { 125 + // ARRANGE 126 + const std::string root = locateRepoRoot(); 127 + 128 + // ACT 129 + auto v = scanForLiteral( root, { "src/ble" }, "ble_bonds" ); 130 + 131 + // ASSERT 132 + if ( !v.empty() ) { 133 + failWithViolations( "Legacy BLE bond blob references found:", v, 134 + "Use NameStore instead of the old app-NVS bond blob." ); 135 + } 136 + } 137 + 138 + static void test_only_namestore_declares_ble_name_namespace() { 139 + // ARRANGE 140 + const std::string root = locateRepoRoot(); 141 + 142 + // ACT 143 + auto v = 144 + scanForLiteral( root, { "src/ble" }, "kleidos_blnames", "src/ble/bond/name_store.cpp" ); 145 + 146 + // ASSERT 147 + if ( !v.empty() ) { 148 + failWithViolations( "Unexpected BLE name namespace references found:", v, 149 + "name_store.cpp must remain the sole owner." ); 150 + } 151 + } 152 + 153 + static void test_firmware_never_opens_nimble_bond_namespace() { 154 + // ARRANGE 155 + const std::string root = locateRepoRoot(); 156 + 157 + // ACT 158 + auto v = scan( root, kAllSourceDirs, kForbiddenNimbleBondOpenPatterns ); 159 + 160 + // ASSERT 161 + if ( !v.empty() ) { 162 + failWithViolations( "Direct nimble_bond NVS opens found:", v, 163 + "NimBLE must own its bond namespace internally." ); 164 + } 165 + } 166 + 167 + // --- Fake function definitions --------------------------------------------- 168 + 169 + // --- Helper definitions ---------------------------------------------------- 170 + static bool isSourceFile( const fs::path& p ) { 171 + static const std::vector<std::string> exts = { ".h", ".hpp", ".cpp", ".c", ".inc" }; 172 + return std::find( exts.begin(), exts.end(), p.extension().string() ) != exts.end(); 173 + } 174 + 175 + static std::string locateRepoRoot() { 176 + fs::path here = fs::current_path(); 177 + for ( int depth = 0; depth < 8; ++depth ) { 178 + if ( fs::exists( here / "platformio.ini" ) ) 179 + return here.string(); 180 + if ( here == here.root_path() ) 181 + break; 182 + here = here.parent_path(); 183 + } 184 + return fs::current_path().string(); 185 + } 186 + 187 + static std::vector<Violation> scan( const std::string& root, const std::vector<std::string>& dirs, 188 + const std::vector<std::regex>& patterns ) { 189 + std::vector<Violation> out; 190 + for ( const auto& rel : dirs ) { 191 + fs::path dir = fs::path( root ) / rel; 192 + if ( !fs::exists( dir ) ) 193 + continue; 194 + for ( auto& entry : fs::recursive_directory_iterator( dir ) ) { 195 + if ( !entry.is_regular_file() || !isSourceFile( entry.path() ) ) 196 + continue; 197 + std::ifstream f( entry.path() ); 198 + std::string line; 199 + int lineNo = 0; 200 + while ( std::getline( f, line ) ) { 201 + ++lineNo; 202 + for ( const auto& re : patterns ) { 203 + if ( std::regex_search( line, re ) ) { 204 + out.push_back( 205 + { fs::relative( entry.path(), root ).string(), lineNo, line } ); 206 + break; 207 + } 208 + } 209 + } 210 + } 211 + } 212 + return out; 213 + } 214 + 215 + static std::vector<Violation> scanForLiteral( const std::string& root, 216 + const std::vector<std::string>& dirs, 217 + const std::string& literal, 218 + const std::string& allowedPath ) { 219 + std::vector<Violation> out; 220 + for ( const auto& rel : dirs ) { 221 + fs::path dir = fs::path( root ) / rel; 222 + if ( !fs::exists( dir ) ) 223 + continue; 224 + for ( auto& entry : fs::recursive_directory_iterator( dir ) ) { 225 + if ( !entry.is_regular_file() || !isSourceFile( entry.path() ) ) 226 + continue; 227 + const std::string relative = fs::relative( entry.path(), root ).string(); 228 + if ( !allowedPath.empty() && relative == allowedPath ) 229 + continue; 230 + std::ifstream f( entry.path() ); 231 + std::string line; 232 + int lineNo = 0; 233 + while ( std::getline( f, line ) ) { 234 + ++lineNo; 235 + if ( line.find( literal ) != std::string::npos ) { 236 + out.push_back( { relative, lineNo, line } ); 237 + } 238 + } 239 + } 240 + } 241 + return out; 242 + } 243 + 244 + static void failWithViolations( const char* header, const std::vector<Violation>& violations, 245 + const char* footer ) { 246 + std::string msg = header; 247 + msg += "\n"; 248 + for ( const auto& it : violations ) { 249 + msg += " "; 250 + msg += it.path; 251 + msg += ":"; 252 + msg += std::to_string( it.line ); 253 + msg += " "; 254 + msg += it.content; 255 + msg += "\n"; 256 + } 257 + msg += footer; 258 + TEST_FAIL_MESSAGE( msg.c_str() ); 259 + } 260 + 261 + // --- main() ---------------------------------------------------------------- 262 + int main( int /*argc*/, char** /*argv*/ ) { 263 + UNITY_BEGIN(); 264 + RUN_TEST( test_at_least_one_dir_was_scanned ); 265 + RUN_TEST( test_no_nimble_includes_in_fsm_ui_web ); 266 + RUN_TEST( test_ble_nimble_headers_stay_in_host_adapter ); 267 + RUN_TEST( test_ble_code_does_not_reopen_legacy_bond_blob ); 268 + RUN_TEST( test_only_namestore_declares_ble_name_namespace ); 269 + RUN_TEST( test_firmware_never_opens_nimble_bond_namespace ); 270 + return UNITY_END(); 271 + }
+297
test/ble/test_keyboard_layouts/test_keyboard_layouts.cpp
··· 1 + // Kleidos — Native test: Keyboard Layout Tables (structural correctness of all 6 tables) 2 + // Build: pio test -e native -f ble/test_keyboard_layouts 3 + 4 + // --- Includes -------------------------------------------------------------- 5 + // Group 1: module under test 6 + #include "ble/keyboard_layouts.h" 7 + 8 + // Group 3: stdlib + Unity 9 + #include <cstdint> 10 + #include <cstdio> 11 + #include <cstring> 12 + 13 + #include <unity.h> 14 + 15 + // Group 4: local fakes/helpers 16 + // pgmspace.h stub for native build 17 + #ifndef PROGMEM 18 + #define PROGMEM 19 + #endif 20 + #ifndef pgm_read_byte 21 + #define pgm_read_byte( addr ) ( *(const uint8_t*)( addr ) ) 22 + #endif 23 + 24 + // --- Constants ------------------------------------------------------------- 25 + // KeyboardLayouts.cpp is linked through the native env's build_src_filter (since B2). 26 + // No #include here — that would cause duplicate symbol definitions for 27 + // LAYOUT_ES/UK/FR/DE/PT/IT. 28 + 29 + static constexpr size_t kNumLayouts = 6U; 30 + 31 + // --- State variables ------------------------------------------------------- 32 + struct LayoutInfo { 33 + const char* name; 34 + const LayoutEntry* table; 35 + }; 36 + 37 + static const LayoutInfo kAllLayouts[kNumLayouts] = { 38 + { "ES", kLayoutEs }, { "UK", kLayoutUk }, { "FR", kLayoutFr }, 39 + { "DE", kLayoutDe }, { "PT", kLayoutPt }, { "IT", kLayoutIt }, 40 + }; 41 + 42 + static uint32_t s_fakeMs = 0; 43 + 44 + // --- Function declarations ------------------------------------------------- 45 + static LayoutEntry readEntry( const LayoutEntry* table, uint8_t idx ); 46 + 47 + // --- setUp / tearDown ------------------------------------------------------ 48 + void setUp() { /* intentionally empty */ } 49 + void tearDown() { /* intentionally empty */ } 50 + 51 + // --- Tests ----------------------------------------------------------------- 52 + static void test_space_has_scancode() { 53 + // ARRANGE - check ASCII 32 (space) in all layouts 54 + for ( size_t i = 0; i < kNumLayouts; i++ ) { 55 + // ACT 56 + LayoutEntry e = readEntry( kAllLayouts[i].table, 32 ); 57 + 58 + // ASSERT 59 + char msg[64]; 60 + snprintf( msg, sizeof( msg ), "Space missing scancode in %s", kAllLayouts[i].name ); 61 + TEST_ASSERT_NOT_EQUAL_MESSAGE( 0, e.scancode, msg ); 62 + TEST_ASSERT_EQUAL_HEX8_MESSAGE( 0x2c, e.scancode, msg ); 63 + } 64 + } 65 + 66 + static void test_digits_have_scancodes() { 67 + // ARRANGE - digits 0-9 must have scancodes in all layouts 68 + for ( size_t i = 0; i < kNumLayouts; i++ ) { 69 + for ( uint8_t c = '0'; c <= '9'; c++ ) { 70 + // ACT 71 + LayoutEntry e = readEntry( kAllLayouts[i].table, c ); 72 + 73 + // ASSERT 74 + char msg[64]; 75 + snprintf( msg, sizeof( msg ), "Digit '%c' missing in %s", c, kAllLayouts[i].name ); 76 + TEST_ASSERT_NOT_EQUAL_MESSAGE( 0, e.scancode, msg ); 77 + } 78 + } 79 + } 80 + 81 + static void test_lowercase_have_scancodes() { 82 + // ARRANGE - lowercase a-z must have scancodes and no shift flag 83 + for ( size_t i = 0; i < kNumLayouts; i++ ) { 84 + for ( uint8_t c = 'a'; c <= 'z'; c++ ) { 85 + // ACT 86 + LayoutEntry e = readEntry( kAllLayouts[i].table, c ); 87 + 88 + // ASSERT 89 + char msg[64]; 90 + snprintf( msg, sizeof( msg ), "Letter '%c' missing in %s", c, kAllLayouts[i].name ); 91 + TEST_ASSERT_NOT_EQUAL_MESSAGE( 0, e.scancode, msg ); 92 + TEST_ASSERT_EQUAL_MESSAGE( 0, e.flags & kKlShift, msg ); 93 + } 94 + } 95 + } 96 + 97 + static void test_uppercase_have_scancodes() { 98 + // ARRANGE - uppercase A-Z must have scancodes and shift flag 99 + for ( size_t i = 0; i < kNumLayouts; i++ ) { 100 + for ( uint8_t c = 'A'; c <= 'Z'; c++ ) { 101 + // ACT 102 + LayoutEntry e = readEntry( kAllLayouts[i].table, c ); 103 + 104 + // ASSERT 105 + char msg[64]; 106 + snprintf( msg, sizeof( msg ), "Letter '%c' missing in %s", c, kAllLayouts[i].name ); 107 + TEST_ASSERT_NOT_EQUAL_MESSAGE( 0, e.scancode, msg ); 108 + TEST_ASSERT_NOT_EQUAL_MESSAGE( 0, e.flags & kKlShift, msg ); 109 + } 110 + } 111 + } 112 + 113 + static void test_case_scancode_match() { 114 + // ARRANGE - upper and lower case letters use same scancode, differ only in shift 115 + for ( size_t i = 0; i < kNumLayouts; i++ ) { 116 + for ( uint8_t lower = 'a'; lower <= 'z'; lower++ ) { 117 + uint8_t upper = lower - 32; 118 + 119 + // ACT 120 + LayoutEntry eLow = readEntry( kAllLayouts[i].table, lower ); 121 + LayoutEntry eUp = readEntry( kAllLayouts[i].table, upper ); 122 + 123 + // ASSERT 124 + char msg[64]; 125 + snprintf( msg, sizeof( msg ), "'%c'/'%c' scancode mismatch in %s", upper, lower, 126 + kAllLayouts[i].name ); 127 + TEST_ASSERT_EQUAL_HEX8_MESSAGE( eLow.scancode, eUp.scancode, msg ); 128 + } 129 + } 130 + } 131 + 132 + static void test_control_chars_empty() { 133 + // ARRANGE - control chars 0-7 and 11-31 must have no scancode 134 + for ( size_t i = 0; i < kNumLayouts; i++ ) { 135 + for ( uint8_t c = 0; c <= 7; c++ ) { 136 + // ACT 137 + LayoutEntry e = readEntry( kAllLayouts[i].table, c ); 138 + 139 + // ASSERT 140 + char msg[64]; 141 + snprintf( msg, sizeof( msg ), "Ctrl char %u has scancode in %s", c, 142 + kAllLayouts[i].name ); 143 + TEST_ASSERT_EQUAL_MESSAGE( 0, e.scancode, msg ); 144 + } 145 + for ( uint8_t c = 11; c <= 31; c++ ) { 146 + // ACT 147 + LayoutEntry e = readEntry( kAllLayouts[i].table, c ); 148 + 149 + // ASSERT 150 + char msg[64]; 151 + snprintf( msg, sizeof( msg ), "Ctrl char %u has scancode in %s", c, 152 + kAllLayouts[i].name ); 153 + TEST_ASSERT_EQUAL_MESSAGE( 0, e.scancode, msg ); 154 + } 155 + } 156 + } 157 + 158 + static void test_special_control_chars() { 159 + // ARRANGE - BS (8), TAB (9), Enter (10) must have standard scancodes in all layouts 160 + for ( size_t i = 0; i < kNumLayouts; i++ ) { 161 + // ACT / ASSERT 162 + LayoutEntry bs = readEntry( kAllLayouts[i].table, 8 ); 163 + TEST_ASSERT_EQUAL_HEX8( 0x2a, bs.scancode ); // Backspace 164 + 165 + LayoutEntry tab = readEntry( kAllLayouts[i].table, 9 ); 166 + TEST_ASSERT_EQUAL_HEX8( 0x2b, tab.scancode ); // Tab 167 + 168 + LayoutEntry enter = readEntry( kAllLayouts[i].table, 10 ); 169 + TEST_ASSERT_EQUAL_HEX8( 0x28, enter.scancode ); // Enter 170 + } 171 + } 172 + 173 + static void test_del_empty() { 174 + // ARRANGE - DEL (127) must have no scancode 175 + for ( size_t i = 0; i < kNumLayouts; i++ ) { 176 + // ACT 177 + LayoutEntry e = readEntry( kAllLayouts[i].table, 127 ); 178 + 179 + // ASSERT 180 + TEST_ASSERT_EQUAL( 0, e.scancode ); 181 + } 182 + } 183 + 184 + static void test_flags_valid_bits() { 185 + // ARRANGE - all flags must use only valid bits (kKlShift | kKlAltgr | kKlDead) 186 + constexpr uint8_t kValidMask = kKlShift | kKlAltgr | kKlDead; 187 + for ( size_t i = 0; i < kNumLayouts; i++ ) { 188 + for ( uint8_t c = 0; c < 128; c++ ) { 189 + // ACT 190 + LayoutEntry e = readEntry( kAllLayouts[i].table, c ); 191 + uint8_t invalid = e.flags & ~kValidMask; 192 + 193 + // ASSERT 194 + char msg[64]; 195 + snprintf( msg, sizeof( msg ), "Invalid flags 0x%02X at %u in %s", e.flags, c, 196 + kAllLayouts[i].name ); 197 + TEST_ASSERT_EQUAL_MESSAGE( 0, invalid, msg ); 198 + } 199 + } 200 + } 201 + 202 + static void test_common_printable_chars() { 203 + // ARRANGE - common printable chars should be typeable in every layout 204 + const char* common = "!@#$%^&*()-_=+[]{}|;:,.<>?/\\\"' "; 205 + 206 + // ACT / ASSERT 207 + for ( size_t i = 0; i < kNumLayouts; i++ ) { 208 + for ( size_t j = 0; common[j] != '\0'; j++ ) { 209 + uint8_t c = static_cast<uint8_t>( common[j] ); 210 + if ( c >= 128 ) 211 + continue; 212 + // Most should have scancodes, but backtick on IT is {0,0} 213 + // so zero-scancode is acceptable for some layouts 214 + readEntry( kAllLayouts[i].table, c ); 215 + } 216 + } 217 + } 218 + 219 + static void test_scancodes_valid_range() { 220 + // ARRANGE - non-zero scancodes must be in valid HID range (0x04–0x65) 221 + for ( size_t i = 0; i < kNumLayouts; i++ ) { 222 + for ( uint8_t c = 0; c < 128; c++ ) { 223 + // ACT 224 + LayoutEntry e = readEntry( kAllLayouts[i].table, c ); 225 + if ( e.scancode == 0 ) 226 + continue; 227 + 228 + // ASSERT 229 + char msg[64]; 230 + snprintf( msg, sizeof( msg ), "Scancode 0x%02X out of range at %u in %s", e.scancode, c, 231 + kAllLayouts[i].name ); 232 + TEST_ASSERT_TRUE_MESSAGE( e.scancode >= 0x04 && e.scancode <= 0x65, msg ); 233 + } 234 + } 235 + } 236 + 237 + static void test_fr_azerty_swaps() { 238 + // ARRANGE - FR AZERTY must have correct A/Q and W/Z key swaps 239 + 240 + // ACT 241 + LayoutEntry a = readEntry( kLayoutFr, 'a' ); 242 + LayoutEntry q = readEntry( kLayoutFr, 'q' ); 243 + 244 + // ASSERT 245 + TEST_ASSERT_EQUAL_HEX8( 0x14, a.scancode ); // Q key 246 + TEST_ASSERT_EQUAL_HEX8( 0x04, q.scancode ); // A key 247 + 248 + // ACT 249 + LayoutEntry w = readEntry( kLayoutFr, 'w' ); 250 + LayoutEntry z = readEntry( kLayoutFr, 'z' ); 251 + 252 + // ASSERT 253 + TEST_ASSERT_EQUAL_HEX8( 0x1d, w.scancode ); // Z key 254 + TEST_ASSERT_EQUAL_HEX8( 0x1a, z.scancode ); // W key 255 + } 256 + 257 + static void test_de_qwertz_swap() { 258 + // ARRANGE - DE QWERTZ must have correct Y/Z key swap 259 + 260 + // ACT 261 + LayoutEntry y = readEntry( kLayoutDe, 'y' ); 262 + LayoutEntry z = readEntry( kLayoutDe, 'z' ); 263 + 264 + // ASSERT 265 + TEST_ASSERT_EQUAL_HEX8( 0x1d, y.scancode ); // Z key 266 + TEST_ASSERT_EQUAL_HEX8( 0x1c, z.scancode ); // Y key 267 + } 268 + 269 + // --- Fake function definitions --------------------------------------------- 270 + 271 + // --- Helper definitions ---------------------------------------------------- 272 + static LayoutEntry readEntry( const LayoutEntry* table, uint8_t idx ) { 273 + LayoutEntry e; 274 + const uint8_t* p = reinterpret_cast<const uint8_t*>( &table[idx] ); 275 + e.scancode = pgm_read_byte( p ); 276 + e.flags = pgm_read_byte( p + 1 ); 277 + return e; 278 + } 279 + 280 + // --- main() ---------------------------------------------------------------- 281 + int main( int /*argc*/, char** /*argv*/ ) { 282 + UNITY_BEGIN(); 283 + RUN_TEST( test_space_has_scancode ); 284 + RUN_TEST( test_digits_have_scancodes ); 285 + RUN_TEST( test_lowercase_have_scancodes ); 286 + RUN_TEST( test_uppercase_have_scancodes ); 287 + RUN_TEST( test_case_scancode_match ); 288 + RUN_TEST( test_control_chars_empty ); 289 + RUN_TEST( test_special_control_chars ); 290 + RUN_TEST( test_del_empty ); 291 + RUN_TEST( test_flags_valid_bits ); 292 + RUN_TEST( test_common_printable_chars ); 293 + RUN_TEST( test_scancodes_valid_range ); 294 + RUN_TEST( test_fr_azerty_swaps ); 295 + RUN_TEST( test_de_qwertz_swap ); 296 + return UNITY_END(); 297 + }
+192
test/crypto/test_password_generator/test_password_generator.cpp
··· 1 + // Kleidos — Native test: Password Generator (length, charset, entropy validation) 2 + // Build: pio test -e native -f crypto/test_password_generator 3 + 4 + // --- File header ----------------------------------------------------------- 5 + // Group 1: module under test 6 + #include "crypto/password_generator.h" 7 + 8 + // Group 3: stdlib + Unity 9 + #include <cmath> 10 + #include <cstdint> 11 + #include <cstring> 12 + 13 + #include <unity.h> 14 + 15 + // --- Constants ------------------------------------------------------------- 16 + static constexpr size_t kBufSize = 65; // 64 chars max + NUL 17 + 18 + // --- State variables ------------------------------------------------------- 19 + 20 + // --- Function declarations ------------------------------------------------- 21 + // HoldButton.cpp calls millis() directly; stub satisfies linker 22 + 23 + // --- setUp / tearDown ------------------------------------------------------ 24 + void setUp() {} 25 + void tearDown() { /* intentionally empty */ } 26 + 27 + // --- Tests ----------------------------------------------------------------- 28 + static void test_generates_correct_length() { 29 + // ARRANGE 30 + char buf[kBufSize] = {}; 31 + 32 + // ACT 33 + PasswordGenerator::generate( buf, 16, 0x0F ); 34 + 35 + // ASSERT 36 + TEST_ASSERT_EQUAL( 16, strlen( buf ) ); 37 + } 38 + 39 + static void test_generates_min_length() { 40 + // ARRANGE 41 + char buf[kBufSize] = {}; 42 + 43 + // ACT (below min → clamped to 8) 44 + PasswordGenerator::generate( buf, 4, 0x0F ); 45 + 46 + // ASSERT 47 + TEST_ASSERT_EQUAL( 8, strlen( buf ) ); 48 + } 49 + 50 + static void test_generates_max_length() { 51 + // ARRANGE 52 + char buf[kBufSize] = {}; 53 + 54 + // ACT 55 + PasswordGenerator::generate( buf, 64, 0x0F ); 56 + 57 + // ASSERT 58 + TEST_ASSERT_EQUAL( 64, strlen( buf ) ); 59 + } 60 + 61 + static void test_only_lowercase() { 62 + // ARRANGE 63 + char buf[kBufSize] = {}; 64 + 65 + // ACT 66 + PasswordGenerator::generate( buf, 64, 0x01 ); 67 + 68 + // ASSERT 69 + for ( size_t i = 0; i < 64; i++ ) { 70 + TEST_ASSERT_TRUE_MESSAGE( buf[i] >= 'a' && buf[i] <= 'z', "Expected lowercase character" ); 71 + } 72 + } 73 + 74 + static void test_only_uppercase() { 75 + // ARRANGE 76 + char buf[kBufSize] = {}; 77 + 78 + // ACT 79 + PasswordGenerator::generate( buf, 64, 0x02 ); 80 + 81 + // ASSERT 82 + for ( size_t i = 0; i < 64; i++ ) { 83 + TEST_ASSERT_TRUE_MESSAGE( buf[i] >= 'A' && buf[i] <= 'Z', "Expected uppercase character" ); 84 + } 85 + } 86 + 87 + static void test_only_digits() { 88 + // ARRANGE 89 + char buf[kBufSize] = {}; 90 + 91 + // ACT 92 + PasswordGenerator::generate( buf, 64, 0x04 ); 93 + 94 + // ASSERT 95 + for ( size_t i = 0; i < 64; i++ ) { 96 + TEST_ASSERT_TRUE_MESSAGE( buf[i] >= '0' && buf[i] <= '9', "Expected digit character" ); 97 + } 98 + } 99 + 100 + static void test_entropy_all_charsets() { 101 + // ARRANGE + ACT (16 chars, all 88-char pool → 16 × log2(88) ≈ 103.4 bits) 102 + float e = PasswordGenerator::entropyBits( 16, 0x0F ); 103 + 104 + // ASSERT 105 + TEST_ASSERT_FLOAT_WITHIN( 1.0f, 103.4f, e ); 106 + } 107 + 108 + static void test_entropy_digits_only() { 109 + // ARRANGE + ACT (6 digits → 6 × log2(10) ≈ 19.9 bits) 110 + float e = PasswordGenerator::entropyBits( 6, 0x04 ); 111 + 112 + // ASSERT 113 + TEST_ASSERT_FLOAT_WITHIN( 0.5f, 19.9f, e ); 114 + } 115 + 116 + static void test_entropy_zero_charsets() { 117 + // ARRANGE + ACT 118 + float e = PasswordGenerator::entropyBits( 16, 0x00 ); 119 + 120 + // ASSERT 121 + TEST_ASSERT_FLOAT_WITHIN( 0.01f, 0.0f, e ); 122 + } 123 + 124 + static void test_null_terminated() { 125 + // ARRANGE 126 + char buf[kBufSize]; 127 + memset( buf, 'X', sizeof( buf ) ); 128 + 129 + // ACT 130 + PasswordGenerator::generate( buf, 16, 0x0F ); 131 + 132 + // ASSERT 133 + TEST_ASSERT_EQUAL( '\0', buf[16] ); 134 + } 135 + 136 + static void test_zero_charsets_defaults_to_all() { 137 + // ARRANGE 138 + char buf[kBufSize] = {}; 139 + 140 + // ACT (zero charsets → should default to ALL, producing 16 chars) 141 + PasswordGenerator::generate( buf, 16, 0x00 ); 142 + 143 + // ASSERT 144 + TEST_ASSERT_EQUAL( 16, strlen( buf ) ); 145 + } 146 + 147 + static void test_only_symbols() { 148 + // ARRANGE 149 + char buf[kBufSize] = {}; 150 + const char* symbols = "!@#$%^&*()-_=+[]{}|;:,.<>?"; 151 + 152 + // ACT 153 + PasswordGenerator::generate( buf, 32, 0x08 ); 154 + 155 + // ASSERT 156 + for ( size_t i = 0; i < 32; i++ ) { 157 + TEST_ASSERT_NOT_NULL_MESSAGE( strchr( symbols, buf[i] ), "Expected symbol character" ); 158 + } 159 + } 160 + 161 + static void test_two_consecutive_different() { 162 + // ARRANGE + ACT (two generates should produce different passwords — statistical) 163 + char buf1[kBufSize], buf2[kBufSize]; 164 + PasswordGenerator::generate( buf1, 32, 0x0F ); 165 + PasswordGenerator::generate( buf2, 32, 0x0F ); 166 + 167 + // ASSERT 168 + TEST_ASSERT_FALSE( strcmp( buf1, buf2 ) == 0 ); 169 + } 170 + 171 + // --- Fake function definitions --------------------------------------------- 172 + 173 + // --- Helper definitions ---------------------------------------------------- 174 + 175 + // --- main() ---------------------------------------------------------------- 176 + int main( int /*argc*/, char** /*argv*/ ) { 177 + UNITY_BEGIN(); 178 + RUN_TEST( test_generates_correct_length ); 179 + RUN_TEST( test_generates_min_length ); 180 + RUN_TEST( test_generates_max_length ); 181 + RUN_TEST( test_only_lowercase ); 182 + RUN_TEST( test_only_uppercase ); 183 + RUN_TEST( test_only_digits ); 184 + RUN_TEST( test_entropy_all_charsets ); 185 + RUN_TEST( test_entropy_digits_only ); 186 + RUN_TEST( test_entropy_zero_charsets ); 187 + RUN_TEST( test_null_terminated ); 188 + RUN_TEST( test_zero_charsets_defaults_to_all ); 189 + RUN_TEST( test_only_symbols ); 190 + RUN_TEST( test_two_consecutive_different ); 191 + return UNITY_END(); 192 + }
+207
test/hal/test_keyboard_input/test_keyboard_input.cpp
··· 1 + // Kleidos — Native test: Keyboard Input (key-event mapping logic) 2 + // Build: pio test -e native -f hal/test_keyboard_input 3 + 4 + // --- File header ----------------------------------------------------------- 5 + // Group 1: module(s) under test 6 + #include "hal/common/keyboard_input.h" 7 + #include "ui/keyboard/key_event_mapper.h" 8 + 9 + // Group 3: stdlib + Unity 10 + #include <cstdint> 11 + #include <cstring> 12 + 13 + #include <unity.h> 14 + 15 + using ui::KeyFrame; 16 + using ui::mapKeyFrame; 17 + 18 + // --- Function declarations ------------------------------------------------- 19 + // (no file-scope constants) 20 + 21 + // --- Function declarations ------------------------------------------------- 22 + // (no file-scope state variables) 23 + 24 + // --- setUp / tearDown ------------------------------------------------------ 25 + void setUp( void ) {} 26 + void tearDown( void ) {} 27 + 28 + // --- Tests ----------------------------------------------------------------- 29 + static void test_empty_frame_emits_nothing() { 30 + // ARRANGE 31 + KeyFrame f{}; 32 + 33 + // ACT 34 + const char result = mapKeyFrame( f, key_code::kNone ); 35 + 36 + // ASSERT 37 + TEST_ASSERT_EQUAL( key_code::kNone, result ); 38 + } 39 + 40 + static void test_enter_wins_over_word() { 41 + // ARRANGE 42 + KeyFrame f{}; 43 + f.enter = true; 44 + const char w[] = "a"; 45 + f.word = w; 46 + f.wordLen = 1; 47 + 48 + // ACT 49 + const char result = mapKeyFrame( f, key_code::kNone ); 50 + 51 + // ASSERT 52 + TEST_ASSERT_EQUAL( key_code::kEnter, result ); 53 + } 54 + 55 + static void test_backspace_wins_over_word_and_space() { 56 + // ARRANGE 57 + KeyFrame f{}; 58 + f.backspace = true; 59 + f.space = true; 60 + const char w[] = "x"; 61 + f.word = w; 62 + f.wordLen = 1; 63 + 64 + // ACT 65 + const char result = mapKeyFrame( f, key_code::kNone ); 66 + 67 + // ASSERT 68 + TEST_ASSERT_EQUAL( key_code::kBackspace, result ); 69 + } 70 + 71 + static void test_fn_alone_becomes_escape() { 72 + // ARRANGE 73 + KeyFrame f{}; 74 + f.fnAlone = true; 75 + 76 + // ACT 77 + const char result = mapKeyFrame( f, key_code::kNone ); 78 + 79 + // ASSERT 80 + TEST_ASSERT_EQUAL( key_code::kEscape, result ); 81 + } 82 + 83 + static void test_fn_with_enter_does_not_emit_escape() { 84 + // ARRANGE 85 + KeyFrame f{}; 86 + f.enter = true; 87 + f.fnAlone = false; // would be set to false by the driver in this case 88 + 89 + // ACT 90 + const char result = mapKeyFrame( f, key_code::kNone ); 91 + 92 + // ASSERT 93 + TEST_ASSERT_EQUAL( key_code::kEnter, result ); 94 + } 95 + 96 + static void test_arrow_wins_over_fn_and_word() { 97 + // ARRANGE 98 + KeyFrame f{}; 99 + f.down = true; 100 + f.fnAlone = true; 101 + const char w[] = "j"; 102 + f.word = w; 103 + f.wordLen = 1; 104 + 105 + // ACT 106 + const char result = mapKeyFrame( f, key_code::kNone ); 107 + 108 + // ASSERT 109 + TEST_ASSERT_EQUAL( key_code::kDown, result ); 110 + } 111 + 112 + static void test_space_emits_space_char() { 113 + // ARRANGE 114 + KeyFrame f{}; 115 + f.space = true; 116 + 117 + // ACT 118 + const char result = mapKeyFrame( f, key_code::kNone ); 119 + 120 + // ASSERT 121 + TEST_ASSERT_EQUAL( ' ', result ); 122 + } 123 + 124 + static void test_word_emits_first_char() { 125 + // ARRANGE 126 + KeyFrame f{}; 127 + const char w[] = "k"; 128 + f.word = w; 129 + f.wordLen = 1; 130 + 131 + // ACT 132 + const char result = mapKeyFrame( f, key_code::kNone ); 133 + 134 + // ASSERT 135 + TEST_ASSERT_EQUAL( 'k', result ); 136 + } 137 + 138 + static void test_digit_emits_digit() { 139 + // ARRANGE 140 + KeyFrame f{}; 141 + const char w[] = "7"; 142 + f.word = w; 143 + f.wordLen = 1; 144 + 145 + // ACT 146 + const char result = mapKeyFrame( f, key_code::kNone ); 147 + 148 + // ASSERT 149 + TEST_ASSERT_EQUAL( '7', result ); 150 + } 151 + 152 + static void test_held_key_does_not_repeat() { 153 + // ARRANGE 154 + KeyFrame f{}; 155 + const char w[] = "a"; 156 + f.word = w; 157 + f.wordLen = 1; 158 + 159 + // ACT 160 + const char first = mapKeyFrame( f, key_code::kNone ); 161 + const char second = mapKeyFrame( f, first ); 162 + 163 + // ASSERT 164 + TEST_ASSERT_EQUAL( 'a', first ); 165 + TEST_ASSERT_EQUAL( key_code::kNone, second ); 166 + } 167 + 168 + static void test_re_press_after_release_emits_again() { 169 + // ARRANGE 170 + KeyFrame held{}; 171 + const char w[] = "z"; 172 + held.word = w; 173 + held.wordLen = 1; 174 + KeyFrame released{}; 175 + 176 + // ACT 177 + const char first = mapKeyFrame( held, key_code::kNone ); 178 + const char none = mapKeyFrame( released, first ); 179 + const char third = mapKeyFrame( held, none ); 180 + 181 + // ASSERT 182 + TEST_ASSERT_EQUAL( 'z', first ); 183 + TEST_ASSERT_EQUAL( key_code::kNone, none ); 184 + TEST_ASSERT_EQUAL( 'z', third ); 185 + } 186 + 187 + // --- Fake function definitions --------------------------------------------- 188 + 189 + // --- Helper definitions ---------------------------------------------------- 190 + // (no helper / arrange definitions) 191 + 192 + // --- main() ---------------------------------------------------------------- 193 + int main( int /*argc*/, char** /*argv*/ ) { 194 + UNITY_BEGIN(); 195 + RUN_TEST( test_empty_frame_emits_nothing ); 196 + RUN_TEST( test_enter_wins_over_word ); 197 + RUN_TEST( test_backspace_wins_over_word_and_space ); 198 + RUN_TEST( test_fn_alone_becomes_escape ); 199 + RUN_TEST( test_fn_with_enter_does_not_emit_escape ); 200 + RUN_TEST( test_arrow_wins_over_fn_and_word ); 201 + RUN_TEST( test_space_emits_space_char ); 202 + RUN_TEST( test_word_emits_first_char ); 203 + RUN_TEST( test_digit_emits_digit ); 204 + RUN_TEST( test_held_key_does_not_repeat ); 205 + RUN_TEST( test_re_press_after_release_emits_again ); 206 + return UNITY_END(); 207 + }
+174
test/hal/test_shake_detector/test_shake_detector.cpp
··· 1 + // Kleidos — Native test: Shake Detector (magnitude calculation, threshold logic) 2 + // Build: pio test -e native -f hal/test_shake_detector 3 + 4 + // --- File header ----------------------------------------------------------- 5 + // Group 3: stdlib + Unity 6 + #include <cmath> 7 + #include <cstdint> 8 + 9 + #include <unity.h> 10 + 11 + // --- Constants ------------------------------------------------------------- 12 + static constexpr float kDefaultThreshold = 2.5f; 13 + static constexpr uint32_t kCooldownMs = 500U; 14 + 15 + // --- State variables ------------------------------------------------------- 16 + static uint32_t s_fakeMillis = 0; 17 + 18 + // --- Function declarations ------------------------------------------------- 19 + // shake_detector.cpp calls millis() directly; stub satisfies linker 20 + 21 + static float accelMagnitude( float x, float y, float z ); 22 + 23 + // SimShake — simulated detector state used by all tests 24 + struct SimShake { 25 + float threshold = kDefaultThreshold; 26 + bool enabled = true; 27 + uint32_t lastShakeMs = 0U; 28 + 29 + bool check( float ax, float ay, float az ) { 30 + if ( !enabled ) { 31 + return false; 32 + } 33 + if ( s_fakeMillis - lastShakeMs < kCooldownMs ) { 34 + return false; 35 + } 36 + const float mag = accelMagnitude( ax, ay, az ); 37 + if ( mag > threshold ) { 38 + lastShakeMs = s_fakeMillis; 39 + return true; 40 + } 41 + return false; 42 + } 43 + }; 44 + 45 + // --- setUp / tearDown ------------------------------------------------------ 46 + void setUp( void ) { 47 + s_fakeMillis = 0; 48 + } 49 + void tearDown( void ) {} 50 + 51 + // --- Tests ----------------------------------------------------------------- 52 + static void test_magnitude_at_rest() { 53 + // ARRANGE: gravity ~ 1.0 g on Z axis 54 + 55 + // ACT 56 + const float mag = accelMagnitude( 0.0f, 0.0f, 1.0f ); 57 + 58 + // ASSERT 59 + TEST_ASSERT_FLOAT_WITHIN( 0.01f, 1.0f, mag ); 60 + } 61 + 62 + static void test_magnitude_strong_shake() { 63 + // ARRANGE: diagonal 2 g on all axes 64 + 65 + // ACT 66 + const float mag = accelMagnitude( 2.0f, 2.0f, 2.0f ); 67 + 68 + // ASSERT 69 + TEST_ASSERT_FLOAT_WITHIN( 0.1f, 3.46f, mag ); 70 + } 71 + 72 + static void test_threshold_triggers_shake() { 73 + // ARRANGE 74 + SimShake s; 75 + s_fakeMillis = 1000; 76 + 77 + // ACT 78 + const bool result = s.check( 2.0f, 2.0f, 1.0f ); 79 + 80 + // ASSERT 81 + TEST_ASSERT_TRUE( result ); 82 + } 83 + 84 + static void test_threshold_no_trigger_below() { 85 + // ARRANGE 86 + SimShake s; 87 + s_fakeMillis = 1000; 88 + 89 + // ACT 90 + const bool result = s.check( 0.0f, 0.0f, 1.0f ); 91 + 92 + // ASSERT 93 + TEST_ASSERT_FALSE( result ); 94 + } 95 + 96 + static void test_cooldown_prevents_rapid_fire() { 97 + // ARRANGE 98 + SimShake s; 99 + s_fakeMillis = 1000; 100 + 101 + // ACT 102 + const bool first = s.check( 3.0f, 0.0f, 0.0f ); 103 + 104 + s_fakeMillis = 1200; // within cooldown window 105 + const bool second = s.check( 3.0f, 0.0f, 0.0f ); 106 + 107 + s_fakeMillis = 1600; // after cooldown expires 108 + const bool third = s.check( 3.0f, 0.0f, 0.0f ); 109 + 110 + // ASSERT 111 + TEST_ASSERT_TRUE( first ); 112 + TEST_ASSERT_FALSE( second ); 113 + TEST_ASSERT_TRUE( third ); 114 + } 115 + 116 + static void test_disabled_never_triggers() { 117 + // ARRANGE 118 + SimShake s; 119 + s.enabled = false; 120 + s_fakeMillis = 1000; 121 + 122 + // ACT 123 + const bool result = s.check( 10.0f, 10.0f, 10.0f ); 124 + 125 + // ASSERT 126 + TEST_ASSERT_FALSE( result ); 127 + } 128 + 129 + static void test_custom_threshold() { 130 + // ARRANGE 131 + SimShake s; 132 + s.threshold = 5.0f; 133 + s_fakeMillis = 1000; 134 + 135 + // ACT + ASSERT: 3 g does not trigger with a 5 g threshold 136 + TEST_ASSERT_FALSE( s.check( 2.0f, 2.0f, 1.0f ) ); 137 + 138 + // ACT + ASSERT: 6 g exceeds the 5 g threshold 139 + TEST_ASSERT_TRUE( s.check( 4.0f, 4.0f, 2.0f ) ); 140 + } 141 + 142 + static void test_threshold_boundary() { 143 + // ARRANGE 144 + SimShake s; 145 + s.threshold = 2.5f; 146 + s_fakeMillis = 1000; 147 + 148 + // ACT 149 + const bool atThreshold = s.check( 2.5f, 0.0f, 0.0f ); // exactly at (> not >=) 150 + const bool justAbove = s.check( 2.51f, 0.0f, 0.0f ); // just above 151 + 152 + // ASSERT 153 + TEST_ASSERT_FALSE( atThreshold ); 154 + TEST_ASSERT_TRUE( justAbove ); 155 + } 156 + 157 + // --- State variables ------------------------------------------------------- 158 + static float accelMagnitude( float x, float y, float z ) { 159 + return sqrtf( x * x + y * y + z * z ); 160 + } 161 + 162 + // --- main() ---------------------------------------------------------------- 163 + int main( int /*argc*/, char** /*argv*/ ) { 164 + UNITY_BEGIN(); 165 + RUN_TEST( test_magnitude_at_rest ); 166 + RUN_TEST( test_magnitude_strong_shake ); 167 + RUN_TEST( test_threshold_triggers_shake ); 168 + RUN_TEST( test_threshold_no_trigger_below ); 169 + RUN_TEST( test_cooldown_prevents_rapid_fire ); 170 + RUN_TEST( test_disabled_never_triggers ); 171 + RUN_TEST( test_custom_threshold ); 172 + RUN_TEST( test_threshold_boundary ); 173 + return UNITY_END(); 174 + }
+468
test/platform/test_safe_string/test_safe_string.cpp
··· 1 + // Kleidos — Native test: Safe String facade (copy, append, length, format, equals, find) 2 + // Build: pio test -e native -f platform/test_safe_string 3 + 4 + // --- File header ----------------------------------------------------------- 5 + // Group 1: module under test 6 + #include "platform/safe_string.h" 7 + 8 + // Group 3: stdlib + Unity 9 + #include <cstdarg> 10 + #include <cstring> 11 + 12 + #include <unity.h> 13 + 14 + // --- Includes -------------------------------------------------------------- 15 + 16 + // --- Constants ------------------------------------------------------------- 17 + namespace str = kleidos::platform::str; 18 + 19 + // --- Function declarations ------------------------------------------------- 20 + // Mirrors how a module-local printf-style shim forwards captured args to the 21 + // va_list overload of the facade. 22 + static size_t callFormatV( char* dest, size_t destSize, const char* fmt, ... ) 23 + __attribute__( ( __format__( __printf__, 3, 4 ) ) ); 24 + 25 + // --- setUp / tearDown ------------------------------------------------------ 26 + void setUp() {} 27 + void tearDown() { /* intentionally empty */ } 28 + 29 + // --- Tests ----------------------------------------------------------------- 30 + // copy 31 + static void test_copy_basic() { 32 + // ARRANGE 33 + char dest[8] = {}; 34 + 35 + // ACT 36 + const size_t n = str::copy( dest, "abc" ); 37 + 38 + // ASSERT 39 + TEST_ASSERT_EQUAL_STRING( "abc", dest ); 40 + TEST_ASSERT_EQUAL( 3u, n ); 41 + } 42 + 43 + static void test_copy_truncates_and_terminates() { 44 + // ARRANGE 45 + char dest[4] = {}; 46 + 47 + // ACT 48 + const size_t n = str::copy( dest, "abcdef" ); 49 + 50 + // ASSERT (3 chars + NUL in a 4-byte buffer) 51 + TEST_ASSERT_EQUAL_STRING( "abc", dest ); 52 + TEST_ASSERT_EQUAL( 3u, n ); 53 + TEST_ASSERT_EQUAL( '\0', dest[3] ); 54 + } 55 + 56 + static void test_copy_exact_fit() { 57 + // ARRANGE 58 + char dest[4] = {}; 59 + 60 + // ACT 61 + const size_t n = str::copy( dest, "abc" ); 62 + 63 + // ASSERT 64 + TEST_ASSERT_EQUAL_STRING( "abc", dest ); 65 + TEST_ASSERT_EQUAL( 3u, n ); 66 + } 67 + 68 + static void test_copy_empty_source() { 69 + // ARRANGE 70 + char dest[4] = { 'x', 'x', 'x', '\0' }; 71 + 72 + // ACT 73 + const size_t n = str::copy( dest, "" ); 74 + 75 + // ASSERT 76 + TEST_ASSERT_EQUAL_STRING( "", dest ); 77 + TEST_ASSERT_EQUAL( 0u, n ); 78 + } 79 + 80 + static void test_copy_null_source() { 81 + // ARRANGE 82 + char dest[4] = { 'x', 'x', 'x', '\0' }; 83 + 84 + // ACT 85 + const size_t n = str::copy( dest, nullptr ); 86 + 87 + // ASSERT 88 + TEST_ASSERT_EQUAL_STRING( "", dest ); 89 + TEST_ASSERT_EQUAL( 0u, n ); 90 + } 91 + 92 + static void test_copy_explicit_size() { 93 + // ARRANGE 94 + char dest[16] = {}; 95 + 96 + // ACT 97 + const size_t n = str::copy( dest, 4, "abcdef" ); 98 + 99 + // ASSERT 100 + TEST_ASSERT_EQUAL_STRING( "abc", dest ); 101 + TEST_ASSERT_EQUAL( 3u, n ); 102 + } 103 + 104 + static void test_copy_zero_size_is_noop() { 105 + // ARRANGE 106 + char dest[2] = { 'y', '\0' }; 107 + 108 + // ACT 109 + const size_t n = str::copy( dest, 0, "abc" ); 110 + 111 + // ASSERT 112 + TEST_ASSERT_EQUAL( 0u, n ); 113 + TEST_ASSERT_EQUAL( 'y', dest[0] ); // untouched 114 + } 115 + 116 + // append 117 + static void test_append_basic() { 118 + // ARRANGE 119 + char dest[8] = "ab"; 120 + 121 + // ACT 122 + const size_t n = str::append( dest, "cd" ); 123 + 124 + // ASSERT 125 + TEST_ASSERT_EQUAL_STRING( "abcd", dest ); 126 + TEST_ASSERT_EQUAL( 4u, n ); 127 + } 128 + 129 + static void test_append_truncates() { 130 + // ARRANGE 131 + char dest[5] = "abc"; 132 + 133 + // ACT 134 + const size_t n = str::append( dest, "XYZ" ); 135 + 136 + // ASSERT (only one char fits) 137 + TEST_ASSERT_EQUAL_STRING( "abcX", dest ); 138 + TEST_ASSERT_EQUAL( 4u, n ); 139 + } 140 + 141 + static void test_append_to_full_buffer() { 142 + // ARRANGE 143 + char dest[4] = "abc"; 144 + 145 + // ACT 146 + const size_t n = str::append( dest, "Z" ); 147 + 148 + // ASSERT (no room, unchanged) 149 + TEST_ASSERT_EQUAL_STRING( "abc", dest ); 150 + TEST_ASSERT_EQUAL( 3u, n ); 151 + } 152 + 153 + static void test_append_empty_source() { 154 + // ARRANGE 155 + char dest[8] = "ab"; 156 + 157 + // ACT 158 + const size_t n = str::append( dest, "" ); 159 + 160 + // ASSERT 161 + TEST_ASSERT_EQUAL_STRING( "ab", dest ); 162 + TEST_ASSERT_EQUAL( 2u, n ); 163 + } 164 + 165 + // length 166 + static void test_length_basic() { 167 + // ARRANGE + ACT + ASSERT 168 + TEST_ASSERT_EQUAL( 3u, str::length( "abc", 16 ) ); 169 + } 170 + 171 + static void test_length_capped() { 172 + // ARRANGE + ACT + ASSERT 173 + TEST_ASSERT_EQUAL( 2u, str::length( "abcdef", 2 ) ); 174 + } 175 + 176 + static void test_length_null() { 177 + // ARRANGE + ACT + ASSERT 178 + TEST_ASSERT_EQUAL( 0u, str::length( nullptr, 16 ) ); 179 + } 180 + 181 + static void test_length_array_overload() { 182 + // ARRANGE 183 + char buf[8] = "abc"; 184 + 185 + // ASSERT 186 + TEST_ASSERT_EQUAL( 3u, str::length( buf ) ); 187 + } 188 + 189 + static void test_length_unterminated_capped_at_array_size() { 190 + // ARRANGE 191 + char buf[4] = { 'a', 'b', 'c', 'd' }; // no NUL 192 + 193 + // ASSERT 194 + TEST_ASSERT_EQUAL( 4u, str::length( buf ) ); 195 + } 196 + 197 + static void test_length_unbounded_basic() { 198 + // ARRANGE 199 + const char* p = "hello"; 200 + 201 + // ASSERT 202 + TEST_ASSERT_EQUAL( 5u, str::length( p ) ); 203 + } 204 + 205 + static void test_length_unbounded_null() { 206 + // ARRANGE 207 + const char* p = nullptr; 208 + 209 + // ASSERT 210 + TEST_ASSERT_EQUAL( 0u, str::length( p ) ); 211 + } 212 + 213 + // format 214 + static void test_format_basic() { 215 + // ARRANGE 216 + char buf[16] = {}; 217 + 218 + // ACT 219 + const size_t n = str::format( buf, sizeof( buf ), "%d-%s", 42, "x" ); 220 + 221 + // ASSERT 222 + TEST_ASSERT_EQUAL_STRING( "42-x", buf ); 223 + TEST_ASSERT_EQUAL( 4u, n ); 224 + } 225 + 226 + static void test_format_exact_fit() { 227 + // ARRANGE 228 + char buf[4] = {}; 229 + 230 + // ACT 231 + const size_t n = str::format( buf, sizeof( buf ), "%s", "abc" ); 232 + 233 + // ASSERT 234 + TEST_ASSERT_EQUAL_STRING( "abc", buf ); 235 + TEST_ASSERT_EQUAL( 3u, n ); 236 + } 237 + 238 + static void test_format_truncates_and_clamps_return() { 239 + // ARRANGE 240 + char buf[4] = {}; 241 + 242 + // ACT 243 + const size_t n = str::format( buf, sizeof( buf ), "%s", "abcdef" ); 244 + 245 + // ASSERT (3 chars + NUL; return clamped, not snprintf "would write" 6) 246 + TEST_ASSERT_EQUAL_STRING( "abc", buf ); 247 + TEST_ASSERT_EQUAL( 3u, n ); 248 + TEST_ASSERT_EQUAL( '\0', buf[3] ); 249 + } 250 + 251 + static void test_format_null_dest() { 252 + // ARRANGE + ACT 253 + const size_t n = str::format( nullptr, 8, "%d", 1 ); 254 + 255 + // ASSERT 256 + TEST_ASSERT_EQUAL( 0u, n ); 257 + } 258 + 259 + static void test_format_zero_size_is_noop() { 260 + // ARRANGE 261 + char buf[2] = { 'y', '\0' }; 262 + 263 + // ACT 264 + const size_t n = str::format( buf, 0, "%d", 1 ); 265 + 266 + // ASSERT 267 + TEST_ASSERT_EQUAL( 0u, n ); 268 + TEST_ASSERT_EQUAL( 'y', buf[0] ); // untouched 269 + } 270 + 271 + static void test_format_null_fmt() { 272 + // ARRANGE 273 + char buf[4] = { 'x', 'x', 'x', '\0' }; 274 + const char* fmt = nullptr; // non-literal so -Wformat stays quiet 275 + 276 + // ACT 277 + const size_t n = str::format( buf, sizeof( buf ), fmt ); 278 + 279 + // ASSERT 280 + TEST_ASSERT_EQUAL( 0u, n ); 281 + TEST_ASSERT_EQUAL_STRING( "", buf ); 282 + } 283 + 284 + static void test_formatV_forwards_arguments() { 285 + // ARRANGE 286 + char buf[16] = {}; 287 + 288 + // ACT 289 + const size_t n = callFormatV( buf, sizeof( buf ), "%s=%d", "k", 7 ); 290 + 291 + // ASSERT 292 + TEST_ASSERT_EQUAL_STRING( "k=7", buf ); 293 + TEST_ASSERT_EQUAL( 3u, n ); 294 + } 295 + 296 + // equals 297 + static void test_equals_basic() { 298 + // ARRANGE + ACT + ASSERT 299 + TEST_ASSERT_TRUE( str::equals( "abc", "abc" ) ); 300 + TEST_ASSERT_FALSE( str::equals( "abc", "abx" ) ); 301 + TEST_ASSERT_FALSE( str::equals( "abc", "ab" ) ); // prefix is not equal 302 + } 303 + 304 + static void test_equals_null_safety() { 305 + // ARRANGE + ACT + ASSERT 306 + TEST_ASSERT_TRUE( str::equals( nullptr, nullptr ) ); 307 + TEST_ASSERT_FALSE( str::equals( "abc", nullptr ) ); 308 + TEST_ASSERT_FALSE( str::equals( nullptr, "abc" ) ); 309 + } 310 + 311 + static void test_equals_bounded() { 312 + // ARRANGE + ACT + ASSERT 313 + TEST_ASSERT_TRUE( str::equals( "abcZZ", "abcYY", 3 ) ); // first 3 match 314 + TEST_ASSERT_FALSE( str::equals( "abcZZ", "abxYY", 3 ) ); // differ within cap 315 + TEST_ASSERT_TRUE( str::equals( "ab", "ab", 8 ) ); // stop at shared NUL 316 + TEST_ASSERT_FALSE( str::equals( "ab", "abc", 8 ) ); // length differs 317 + } 318 + 319 + static void test_equals_bounded_does_not_overread() { 320 + // ARRANGE (unterminated buffers: comparison must stop at maxLen, never past it) 321 + const char a[3] = { 'x', 'y', 'z' }; // no NUL 322 + const char b[3] = { 'x', 'y', 'z' }; // no NUL 323 + 324 + // ASSERT 325 + TEST_ASSERT_TRUE( str::equals( a, b, 3 ) ); 326 + } 327 + 328 + static void test_equals_ignore_case() { 329 + // ARRANGE + ACT + ASSERT 330 + TEST_ASSERT_TRUE( str::equalsIgnoreCase( "AbC", "aBc" ) ); 331 + TEST_ASSERT_TRUE( str::equalsIgnoreCase( "HELLO", "hello" ) ); 332 + TEST_ASSERT_FALSE( str::equalsIgnoreCase( "abc", "abx" ) ); 333 + TEST_ASSERT_TRUE( str::equalsIgnoreCase( nullptr, nullptr ) ); 334 + TEST_ASSERT_FALSE( str::equalsIgnoreCase( "abc", nullptr ) ); 335 + } 336 + 337 + static void test_equals_ignore_case_only_folds_ascii() { 338 + // ARRANGE (high bytes must be compared verbatim, never locale-folded) 339 + const char a[] = { static_cast<char>( 0xC3 ), static_cast<char>( 0xA9 ), '\0' }; 340 + const char b[] = { static_cast<char>( 0xC3 ), static_cast<char>( 0xA9 ), '\0' }; 341 + const char c[] = { static_cast<char>( 0xC3 ), static_cast<char>( 0x89 ), '\0' }; 342 + 343 + // ASSERT 344 + TEST_ASSERT_TRUE( str::equalsIgnoreCase( a, b ) ); 345 + TEST_ASSERT_FALSE( str::equalsIgnoreCase( a, c ) ); // not folded as A-Z 346 + } 347 + 348 + // startsWith 349 + static void test_starts_with() { 350 + // ARRANGE + ACT + ASSERT 351 + TEST_ASSERT_TRUE( str::startsWith( "cred_login", "cred_" ) ); 352 + TEST_ASSERT_FALSE( str::startsWith( "totp_login", "cred_" ) ); 353 + TEST_ASSERT_TRUE( str::startsWith( "abc", "" ) ); // empty prefix matches 354 + TEST_ASSERT_FALSE( str::startsWith( "ab", "abc" ) ); // str shorter than prefix 355 + TEST_ASSERT_FALSE( str::startsWith( nullptr, "a" ) ); 356 + TEST_ASSERT_FALSE( str::startsWith( "a", nullptr ) ); 357 + } 358 + 359 + static void test_starts_with_ignore_case() { 360 + // ARRANGE + ACT + ASSERT 361 + TEST_ASSERT_TRUE( str::startsWithIgnoreCase( "0xABCD", "0X" ) ); 362 + TEST_ASSERT_TRUE( str::startsWithIgnoreCase( "HTTPS://x", "https://" ) ); 363 + TEST_ASSERT_FALSE( str::startsWithIgnoreCase( "ftp://x", "https://" ) ); 364 + } 365 + 366 + // findChar / findLastChar 367 + static void test_find_char() { 368 + // ARRANGE 369 + const char* s = "a.b.c"; 370 + 371 + // ACT + ASSERT 372 + TEST_ASSERT_EQUAL_PTR( s + 1, str::findChar( s, '.' ) ); 373 + TEST_ASSERT_NULL( str::findChar( s, 'Z' ) ); 374 + const char* nullStr = nullptr; 375 + TEST_ASSERT_NULL( str::findChar( nullStr, 'a' ) ); 376 + TEST_ASSERT_EQUAL_PTR( s + 5, str::findChar( s, '\0' ) ); // terminator, like strchr 377 + 378 + // Mutable overload: returns a writable pointer for in-place splitting. 379 + char buf[] = "user|pass"; 380 + char* sep = str::findChar( buf, '|' ); 381 + TEST_ASSERT_EQUAL_PTR( buf + 4, sep ); 382 + *sep = '\0'; 383 + TEST_ASSERT_EQUAL_STRING( "user", buf ); 384 + char* nullMut = nullptr; 385 + TEST_ASSERT_NULL( str::findChar( nullMut, 'a' ) ); 386 + } 387 + 388 + static void test_find_last_char() { 389 + // ARRANGE 390 + const char* s = "a.b.c"; 391 + 392 + // ACT + ASSERT 393 + TEST_ASSERT_EQUAL_PTR( s + 3, str::findLastChar( s, '.' ) ); 394 + TEST_ASSERT_NULL( str::findLastChar( s, 'Z' ) ); 395 + const char* nullStr = nullptr; 396 + TEST_ASSERT_NULL( str::findLastChar( nullStr, 'a' ) ); 397 + 398 + // Mutable overload: returns a writable pointer to the last match. 399 + char buf[] = "a/b/c"; 400 + char* last = str::findLastChar( buf, '/' ); 401 + TEST_ASSERT_EQUAL_PTR( buf + 3, last ); 402 + char* nullMut = nullptr; 403 + TEST_ASSERT_NULL( str::findLastChar( nullMut, 'a' ) ); 404 + } 405 + 406 + // findSubstring 407 + static void test_find_substring() { 408 + // ARRANGE 409 + const char* s = "host://path"; 410 + 411 + // ACT + ASSERT 412 + TEST_ASSERT_EQUAL_PTR( s + 4, str::findSubstring( s, "://" ) ); 413 + TEST_ASSERT_NULL( str::findSubstring( s, "@" ) ); 414 + TEST_ASSERT_EQUAL_PTR( s, str::findSubstring( s, "" ) ); // empty needle, like strstr 415 + TEST_ASSERT_NULL( str::findSubstring( nullptr, "a" ) ); 416 + TEST_ASSERT_NULL( str::findSubstring( "a", nullptr ) ); 417 + } 418 + 419 + // --- State variables ------------------------------------------------------- 420 + static size_t callFormatV( char* dest, size_t destSize, const char* fmt, ... ) { 421 + va_list args; 422 + va_start( args, fmt ); 423 + const size_t n = str::formatV( dest, destSize, fmt, args ); 424 + va_end( args ); 425 + return n; 426 + } 427 + 428 + // --- main() ---------------------------------------------------------------- 429 + int main( int /*argc*/, char** /*argv*/ ) { 430 + UNITY_BEGIN(); 431 + RUN_TEST( test_copy_basic ); 432 + RUN_TEST( test_copy_truncates_and_terminates ); 433 + RUN_TEST( test_copy_exact_fit ); 434 + RUN_TEST( test_copy_empty_source ); 435 + RUN_TEST( test_copy_null_source ); 436 + RUN_TEST( test_copy_explicit_size ); 437 + RUN_TEST( test_copy_zero_size_is_noop ); 438 + RUN_TEST( test_append_basic ); 439 + RUN_TEST( test_append_truncates ); 440 + RUN_TEST( test_append_to_full_buffer ); 441 + RUN_TEST( test_append_empty_source ); 442 + RUN_TEST( test_length_basic ); 443 + RUN_TEST( test_length_capped ); 444 + RUN_TEST( test_length_null ); 445 + RUN_TEST( test_length_array_overload ); 446 + RUN_TEST( test_length_unterminated_capped_at_array_size ); 447 + RUN_TEST( test_length_unbounded_basic ); 448 + RUN_TEST( test_length_unbounded_null ); 449 + RUN_TEST( test_format_basic ); 450 + RUN_TEST( test_format_exact_fit ); 451 + RUN_TEST( test_format_truncates_and_clamps_return ); 452 + RUN_TEST( test_format_null_dest ); 453 + RUN_TEST( test_format_zero_size_is_noop ); 454 + RUN_TEST( test_format_null_fmt ); 455 + RUN_TEST( test_formatV_forwards_arguments ); 456 + RUN_TEST( test_equals_basic ); 457 + RUN_TEST( test_equals_null_safety ); 458 + RUN_TEST( test_equals_bounded ); 459 + RUN_TEST( test_equals_bounded_does_not_overread ); 460 + RUN_TEST( test_equals_ignore_case ); 461 + RUN_TEST( test_equals_ignore_case_only_folds_ascii ); 462 + RUN_TEST( test_starts_with ); 463 + RUN_TEST( test_starts_with_ignore_case ); 464 + RUN_TEST( test_find_char ); 465 + RUN_TEST( test_find_last_char ); 466 + RUN_TEST( test_find_substring ); 467 + return UNITY_END(); 468 + }
-105
test/test_admin_idle_logic/test_admin_idle_logic.cpp
··· 1 - // Kleidos — Native tests for the admin idle endpoint logic. 2 - 3 - #include "web/admin_idle_logic.h" 4 - 5 - #include <unity.h> 6 - 7 - using web::decideIdleResponse; 8 - using web::IdleResponse; 9 - 10 - extern "C" uint32_t millis() { 11 - return 0; 12 - } 13 - 14 - void setUp() {} 15 - void tearDown() {} 16 - 17 - static constexpr uint32_t TIMEOUT_MS = 300000; // 5 min 18 - static constexpr uint32_t WARN_MS = 30000; // 30 s 19 - 20 - // elapsed=0 → full window remains, banner hidden. 21 - void test_fresh_session_no_banner() { 22 - auto r = decideIdleResponse( 0u, TIMEOUT_MS, WARN_MS ); 23 - TEST_ASSERT_EQUAL_UINT32( 300u, r.remainingS ); 24 - TEST_ASSERT_FALSE( r.showBanner ); 25 - } 26 - 27 - // At the warning threshold, banner arms. 28 - void test_at_warning_threshold_banner_on() { 29 - auto r = decideIdleResponse( 270000u, TIMEOUT_MS, WARN_MS ); 30 - TEST_ASSERT_EQUAL_UINT32( 30u, r.remainingS ); 31 - TEST_ASSERT_TRUE( r.showBanner ); 32 - } 33 - 34 - // Inside the warning window. 35 - void test_inside_warning_window_banner_on() { 36 - auto r = decideIdleResponse( 290000u, TIMEOUT_MS, WARN_MS ); 37 - TEST_ASSERT_EQUAL_UINT32( 10u, r.remainingS ); 38 - TEST_ASSERT_TRUE( r.showBanner ); 39 - } 40 - 41 - // Past the timeout: clamp to zero, banner still on (caller will tear down). 42 - void test_past_timeout_clamped_to_zero() { 43 - auto r = decideIdleResponse( 305000u, TIMEOUT_MS, WARN_MS ); 44 - TEST_ASSERT_EQUAL_UINT32( 0u, r.remainingS ); 45 - TEST_ASSERT_TRUE( r.showBanner ); 46 - } 47 - 48 - // Just before the warning window — banner stays hidden. 49 - void test_just_before_warning_no_banner() { 50 - auto r = decideIdleResponse( 269000u, TIMEOUT_MS, WARN_MS ); 51 - TEST_ASSERT_EQUAL_UINT32( 31u, r.remainingS ); 52 - TEST_ASSERT_FALSE( r.showBanner ); 53 - } 54 - 55 - // Degenerate: warn lead larger than timeout — banner is permanently on. 56 - void test_warn_larger_than_timeout_always_on() { 57 - auto r = decideIdleResponse( 0u, 10000u, 60000u ); 58 - TEST_ASSERT_EQUAL_UINT32( 10u, r.remainingS ); 59 - TEST_ASSERT_TRUE( r.showBanner ); 60 - } 61 - 62 - // H1: "user keeps the page open but never interacts". 63 - // 64 - // Models the post-H1 reality where a connected station no longer pumps 65 - // `lastClientActivity_` — only authenticated /api/* requests do (and 66 - // /api/idle is read-only). With the JS poll firing every ~10 s on a 67 - // timer, the elapsed counter must walk up to the warning threshold and 68 - // then to the timeout without being reset. 69 - // 70 - // Sequence: 0s, 100s, 200s, 269s, 270s, 290s, 300s, 305s. 71 - void test_idle_walk_with_no_user_input_emits_banner_and_times_out() { 72 - constexpr uint32_t kStep[] = { 0, 100000, 200000, 269000, 270000, 290000, 300000, 305000 }; 73 - constexpr uint32_t kRem[] = { 300, 200, 100, 31, 30, 10, 0, 0 }; 74 - constexpr bool kBanner[] = { false, false, false, false, true, true, true, true }; 75 - 76 - for ( size_t i = 0; i < sizeof( kStep ) / sizeof( kStep[0] ); ++i ) { 77 - auto r = decideIdleResponse( kStep[i], TIMEOUT_MS, WARN_MS ); 78 - TEST_ASSERT_EQUAL_UINT32( kRem[i], r.remainingS ); 79 - if ( kBanner[i] ) { 80 - TEST_ASSERT_TRUE( r.showBanner ); 81 - } else { 82 - TEST_ASSERT_FALSE( r.showBanner ); 83 - } 84 - } 85 - } 86 - 87 - // H1: explicit 270 s threshold ⇒ exactly 30 s remaining + banner armed. 88 - void test_banner_arms_at_270s_remaining_30() { 89 - auto r = decideIdleResponse( 270000u, TIMEOUT_MS, WARN_MS ); 90 - TEST_ASSERT_EQUAL_UINT32( 30u, r.remainingS ); 91 - TEST_ASSERT_TRUE( r.showBanner ); 92 - } 93 - 94 - int main( int, char** ) { 95 - UNITY_BEGIN(); 96 - RUN_TEST( test_fresh_session_no_banner ); 97 - RUN_TEST( test_at_warning_threshold_banner_on ); 98 - RUN_TEST( test_inside_warning_window_banner_on ); 99 - RUN_TEST( test_past_timeout_clamped_to_zero ); 100 - RUN_TEST( test_just_before_warning_no_banner ); 101 - RUN_TEST( test_warn_larger_than_timeout_always_on ); 102 - RUN_TEST( test_idle_walk_with_no_user_input_emits_banner_and_times_out ); 103 - RUN_TEST( test_banner_arms_at_270s_remaining_30 ); 104 - return UNITY_END(); 105 - }
-227
test/test_audit_logic/test_audit_logic.cpp
··· 1 - // Kleidos — Native test: Audit Log circular buffer logic 2 - // Tests the circular buffer algorithm without NVS/RTC hardware. 3 - 4 - #include <cstdint> 5 - #include <cstring> 6 - 7 - #include <unity.h> 8 - 9 - // millis() stub 10 - static uint32_t fakeMillis = 0; 11 - extern "C" uint32_t millis() { 12 - return fakeMillis; 13 - } 14 - 15 - // Replicate AuditLog types and constants 16 - enum class AuditEvent : uint8_t { 17 - PIN_OK = 0, 18 - PIN_FAIL = 1, 19 - VAULT_WIPE = 2, 20 - BLE_TYPE = 3, 21 - OTA_UPDATE = 4, 22 - ADMIN_LOGIN = 5, 23 - }; 24 - 25 - struct AuditEntry { 26 - uint32_t timestamp; 27 - uint8_t event; 28 - uint8_t attempts; 29 - }; 30 - 31 - static constexpr uint8_t AUDIT_LOG_SIZE = 16; 32 - 33 - // Simulated NVS storage 34 - static AuditEntry simEntries[AUDIT_LOG_SIZE] = {}; 35 - static uint8_t simIdx = 0; 36 - static uint32_t simTime = 0; 37 - 38 - static void simClear() { 39 - memset( simEntries, 0, sizeof( simEntries ) ); 40 - simIdx = 0; 41 - simTime = 0; 42 - } 43 - 44 - static void simRecord( AuditEvent event, uint8_t attempts = 0 ) { 45 - simEntries[simIdx].timestamp = simTime; 46 - simEntries[simIdx].event = static_cast<uint8_t>( event ); 47 - simEntries[simIdx].attempts = attempts; 48 - simIdx = ( simIdx + 1 ) % AUDIT_LOG_SIZE; 49 - } 50 - 51 - static uint8_t simGetEntries( AuditEntry* out, uint8_t maxEntries ) { 52 - uint8_t count = 0; 53 - for ( uint8_t i = 0; i < AUDIT_LOG_SIZE && count < maxEntries; i++ ) { 54 - uint8_t pos = ( simIdx + i ) % AUDIT_LOG_SIZE; 55 - if ( simEntries[pos].timestamp == 0 && simEntries[pos].event == 0 ) 56 - continue; 57 - out[count++] = simEntries[pos]; 58 - } 59 - return count; 60 - } 61 - 62 - static bool simGetLastEntry( AuditEntry& entry ) { 63 - uint8_t lastIdx = ( simIdx + AUDIT_LOG_SIZE - 1 ) % AUDIT_LOG_SIZE; 64 - if ( simEntries[lastIdx].timestamp == 0 && simEntries[lastIdx].event == 0 ) 65 - return false; 66 - entry = simEntries[lastIdx]; 67 - return true; 68 - } 69 - 70 - // --------------------------------------------------------------------------- 71 - // Tests 72 - // --------------------------------------------------------------------------- 73 - void test_empty_log_returns_zero() { 74 - simClear(); 75 - AuditEntry out[AUDIT_LOG_SIZE]; 76 - uint8_t count = simGetEntries( out, AUDIT_LOG_SIZE ); 77 - TEST_ASSERT_EQUAL( 0, count ); 78 - } 79 - 80 - void test_empty_log_no_last_entry() { 81 - simClear(); 82 - AuditEntry entry; 83 - TEST_ASSERT_FALSE( simGetLastEntry( entry ) ); 84 - } 85 - 86 - void test_single_record() { 87 - simClear(); 88 - simTime = 100; 89 - simRecord( AuditEvent::PIN_OK, 0 ); 90 - 91 - AuditEntry out[AUDIT_LOG_SIZE]; 92 - uint8_t count = simGetEntries( out, AUDIT_LOG_SIZE ); 93 - TEST_ASSERT_EQUAL( 1, count ); 94 - TEST_ASSERT_EQUAL( 100, out[0].timestamp ); 95 - TEST_ASSERT_EQUAL( 0, out[0].event ); 96 - } 97 - 98 - void test_last_entry_returns_most_recent() { 99 - simClear(); 100 - simTime = 100; 101 - simRecord( AuditEvent::PIN_OK ); 102 - simTime = 200; 103 - simRecord( AuditEvent::PIN_FAIL, 1 ); 104 - simTime = 300; 105 - simRecord( AuditEvent::BLE_TYPE ); 106 - 107 - AuditEntry entry; 108 - TEST_ASSERT_TRUE( simGetLastEntry( entry ) ); 109 - TEST_ASSERT_EQUAL( 300, entry.timestamp ); 110 - TEST_ASSERT_EQUAL( static_cast<uint8_t>( AuditEvent::BLE_TYPE ), entry.event ); 111 - } 112 - 113 - void test_entries_oldest_first() { 114 - simClear(); 115 - for ( uint8_t i = 0; i < 5; i++ ) { 116 - simTime = ( i + 1 ) * 100; 117 - simRecord( AuditEvent::PIN_FAIL, i ); 118 - } 119 - 120 - AuditEntry out[AUDIT_LOG_SIZE]; 121 - uint8_t count = simGetEntries( out, AUDIT_LOG_SIZE ); 122 - TEST_ASSERT_EQUAL( 5, count ); 123 - 124 - // Verify oldest first 125 - for ( uint8_t i = 0; i < count - 1; i++ ) { 126 - TEST_ASSERT_TRUE( out[i].timestamp <= out[i + 1].timestamp ); 127 - } 128 - } 129 - 130 - void test_circular_wrap() { 131 - simClear(); 132 - // Fill buffer completely 133 - for ( uint8_t i = 0; i < AUDIT_LOG_SIZE; i++ ) { 134 - simTime = ( i + 1 ) * 10; 135 - simRecord( AuditEvent::PIN_FAIL, i ); 136 - } 137 - 138 - AuditEntry out[AUDIT_LOG_SIZE]; 139 - uint8_t count = simGetEntries( out, AUDIT_LOG_SIZE ); 140 - TEST_ASSERT_EQUAL( AUDIT_LOG_SIZE, count ); 141 - 142 - // Now overflow — oldest (timestamp=10) should be overwritten 143 - simTime = 999; 144 - simRecord( AuditEvent::VAULT_WIPE, 10 ); 145 - 146 - count = simGetEntries( out, AUDIT_LOG_SIZE ); 147 - TEST_ASSERT_EQUAL( AUDIT_LOG_SIZE, count ); 148 - 149 - // First entry should now be timestamp=20 (second original) 150 - TEST_ASSERT_EQUAL( 20, out[0].timestamp ); 151 - 152 - // Last entry should be the new one 153 - TEST_ASSERT_EQUAL( 999, out[AUDIT_LOG_SIZE - 1].timestamp ); 154 - TEST_ASSERT_EQUAL( static_cast<uint8_t>( AuditEvent::VAULT_WIPE ), 155 - out[AUDIT_LOG_SIZE - 1].event ); 156 - } 157 - 158 - void test_max_entries_limit() { 159 - simClear(); 160 - for ( uint8_t i = 0; i < 10; i++ ) { 161 - simTime = ( i + 1 ) * 10; 162 - simRecord( AuditEvent::PIN_OK ); 163 - } 164 - 165 - AuditEntry out[3]; 166 - uint8_t count = simGetEntries( out, 3 ); 167 - TEST_ASSERT_EQUAL( 3, count ); 168 - } 169 - 170 - void test_clear_resets_all() { 171 - simTime = 100; 172 - simRecord( AuditEvent::PIN_OK ); 173 - simRecord( AuditEvent::PIN_FAIL ); 174 - simClear(); 175 - 176 - AuditEntry out[AUDIT_LOG_SIZE]; 177 - uint8_t count = simGetEntries( out, AUDIT_LOG_SIZE ); 178 - TEST_ASSERT_EQUAL( 0, count ); 179 - } 180 - 181 - void test_event_types_stored_correctly() { 182 - simClear(); 183 - AuditEvent events[] = { 184 - AuditEvent::PIN_OK, AuditEvent::PIN_FAIL, AuditEvent::VAULT_WIPE, 185 - AuditEvent::BLE_TYPE, AuditEvent::OTA_UPDATE, AuditEvent::ADMIN_LOGIN, 186 - }; 187 - 188 - for ( size_t i = 0; i < 6; i++ ) { 189 - simTime = ( i + 1 ) * 100; 190 - simRecord( events[i], static_cast<uint8_t>( i ) ); 191 - } 192 - 193 - AuditEntry out[AUDIT_LOG_SIZE]; 194 - uint8_t count = simGetEntries( out, AUDIT_LOG_SIZE ); 195 - TEST_ASSERT_EQUAL( 6, count ); 196 - 197 - for ( size_t i = 0; i < 6; i++ ) { 198 - TEST_ASSERT_EQUAL( static_cast<uint8_t>( events[i] ), out[i].event ); 199 - TEST_ASSERT_EQUAL( i, out[i].attempts ); 200 - } 201 - } 202 - 203 - void test_attempts_field_max() { 204 - simClear(); 205 - simTime = 1; 206 - simRecord( AuditEvent::PIN_FAIL, 255 ); 207 - 208 - AuditEntry entry; 209 - TEST_ASSERT_TRUE( simGetLastEntry( entry ) ); 210 - TEST_ASSERT_EQUAL( 255, entry.attempts ); 211 - } 212 - 213 - // --------------------------------------------------------------------------- 214 - int main( int argc, char** argv ) { 215 - UNITY_BEGIN(); 216 - RUN_TEST( test_empty_log_returns_zero ); 217 - RUN_TEST( test_empty_log_no_last_entry ); 218 - RUN_TEST( test_single_record ); 219 - RUN_TEST( test_last_entry_returns_most_recent ); 220 - RUN_TEST( test_entries_oldest_first ); 221 - RUN_TEST( test_circular_wrap ); 222 - RUN_TEST( test_max_entries_limit ); 223 - RUN_TEST( test_clear_resets_all ); 224 - RUN_TEST( test_event_types_stored_correctly ); 225 - RUN_TEST( test_attempts_field_max ); 226 - return UNITY_END(); 227 - }
+85 -35
test/test_ble_connection_manager/test_ble_connection_manager.cpp test/ble/test_ble_connection_manager/test_ble_connection_manager.cpp
··· 1 + // Kleidos — Native test: ConnectionManager (connection snapshot, MTU, CCCD, RSSI) 2 + // Build: pio test -e native -f ble/test_ble_connection_manager 3 + 4 + // --- Includes -------------------------------------------------------------- 5 + // Group 1: modules under test 1 6 #include "ble/connection/conn_params.h" 2 7 #include "ble/connection/connection_manager.h" 3 8 9 + // Group 3: stdlib + Unity 4 10 #include <cstdint> 5 11 6 12 #include <unity.h> 7 13 8 - static uint32_t fakeMillis = 0; 9 - extern "C" uint32_t millis() { 10 - return fakeMillis; 11 - } 14 + // --- Constants ------------------------------------------------------------- 15 + namespace conn = kleidos::ble::connection; 12 16 13 - namespace conn = kleidos::ble::connection; 17 + // --- State variables ------------------------------------------------------- 18 + static uint32_t s_fakeMs = 0; 14 19 20 + // --- Function declarations ------------------------------------------------- 21 + static void fillAddress( uint8_t addr[6], uint8_t seed ); 22 + 23 + // --- setUp / tearDown ------------------------------------------------------ 15 24 void setUp() { 16 - fakeMillis = 0; 25 + s_fakeMs = 0; 17 26 conn::ConnectionManager::resetForTest(); 18 27 } 19 28 20 - void tearDown() {} 29 + void tearDown() { /* intentionally empty */ } 21 30 22 - static void fillAddress( uint8_t addr[6], uint8_t seed ) { 23 - for ( uint8_t index = 0; index < 6; ++index ) { 24 - addr[index] = static_cast<uint8_t>( seed + index ); 25 - } 26 - } 27 - 31 + // --- Tests ----------------------------------------------------------------- 28 32 static void test_reset_snapshot_defaults_to_disconnected() { 33 + // ARRANGE 29 34 conn::Snapshot snapshot{}; 30 - TEST_ASSERT_TRUE( conn::ConnectionManager::snapshot( snapshot ) ); 35 + 36 + // ACT 37 + const bool ok = conn::ConnectionManager::snapshot( snapshot ); 38 + 39 + // ASSERT 40 + TEST_ASSERT_TRUE( ok ); 31 41 TEST_ASSERT_FALSE( snapshot.connected ); 32 42 TEST_ASSERT_FALSE( snapshot.authenticated ); 33 43 TEST_ASSERT_FALSE( snapshot.bonded ); ··· 40 50 } 41 51 42 52 static void test_conn_params_match_plan_units() { 53 + // ARRANGE - compile-time constants from conn_params.h 54 + 55 + // ACT - values observed directly from header constants 56 + 57 + // ASSERT 43 58 TEST_ASSERT_EQUAL_UINT16( 13, conn::kConnIntervalMinUnits ); 44 59 TEST_ASSERT_EQUAL_UINT16( 26, conn::kConnIntervalMaxUnits ); 45 60 TEST_ASSERT_EQUAL_UINT16( 0, conn::kConnLatency ); ··· 50 65 } 51 66 52 67 static void test_authenticated_peer_updates_snapshot() { 68 + // ARRANGE 53 69 uint8_t addr[6]; 54 70 fillAddress( addr, 0x20 ); 55 71 72 + // ACT 56 73 conn::ConnectionManager::onAuthenticated( addr, 1, 2, 64 ); 57 - 58 74 conn::Snapshot snapshot{}; 59 - TEST_ASSERT_TRUE( conn::ConnectionManager::snapshot( snapshot ) ); 75 + const bool ok = conn::ConnectionManager::snapshot( snapshot ); 76 + 77 + // ASSERT 78 + TEST_ASSERT_TRUE( ok ); 60 79 TEST_ASSERT_TRUE( snapshot.connected ); 61 80 TEST_ASSERT_TRUE( snapshot.authenticated ); 62 81 TEST_ASSERT_TRUE( snapshot.bonded ); ··· 67 86 } 68 87 69 88 static void test_connected_known_peer_is_visible_before_auth() { 89 + // ARRANGE 70 90 uint8_t addr[6]; 71 91 fillAddress( addr, 0x30 ); 72 92 93 + // ACT 73 94 conn::ConnectionManager::onConnected( addr, 1, 2, 80 ); 74 - 75 95 conn::Snapshot snapshot{}; 76 - TEST_ASSERT_TRUE( conn::ConnectionManager::snapshot( snapshot ) ); 96 + const bool ok = conn::ConnectionManager::snapshot( snapshot ); 97 + 98 + // ASSERT 99 + TEST_ASSERT_TRUE( ok ); 77 100 TEST_ASSERT_TRUE( snapshot.connected ); 78 101 TEST_ASSERT_FALSE( snapshot.authenticated ); 79 102 TEST_ASSERT_TRUE( snapshot.bonded ); ··· 86 109 } 87 110 88 111 static void test_auth_preserves_known_slot_from_connect_snapshot() { 112 + // ARRANGE 89 113 uint8_t addr[6]; 90 114 fillAddress( addr, 0x35 ); 91 - 92 115 conn::ConnectionManager::onConnected( addr, 1, 3, 96 ); 93 - conn::ConnectionManager::onAuthenticated( addr, 1, conn::kNoPeerSlot, conn::kDefaultMtu ); 94 116 117 + // ACT 118 + conn::ConnectionManager::onAuthenticated( addr, 1, conn::kNoPeerSlot, conn::kDefaultMtu ); 95 119 conn::Snapshot snapshot{}; 96 - TEST_ASSERT_TRUE( conn::ConnectionManager::snapshot( snapshot ) ); 120 + const bool ok = conn::ConnectionManager::snapshot( snapshot ); 121 + 122 + // ASSERT 123 + TEST_ASSERT_TRUE( ok ); 97 124 TEST_ASSERT_TRUE( snapshot.connected ); 98 125 TEST_ASSERT_TRUE( snapshot.authenticated ); 99 126 TEST_ASSERT_TRUE( snapshot.bonded ); ··· 102 129 } 103 130 104 131 static void test_no_slot_marks_peer_unbonded() { 132 + // ARRANGE 105 133 uint8_t addr[6]; 106 134 fillAddress( addr, 0x40 ); 107 135 136 + // ACT 108 137 conn::ConnectionManager::onAuthenticated( addr, 0, conn::kNoPeerSlot, 0 ); 138 + conn::Snapshot snapshot{}; 139 + const bool ok = conn::ConnectionManager::snapshot( snapshot ); 109 140 110 - conn::Snapshot snapshot{}; 111 - TEST_ASSERT_TRUE( conn::ConnectionManager::snapshot( snapshot ) ); 141 + // ASSERT 142 + TEST_ASSERT_TRUE( ok ); 112 143 TEST_ASSERT_TRUE( snapshot.connected ); 113 144 TEST_ASSERT_TRUE( snapshot.authenticated ); 114 145 TEST_ASSERT_FALSE( snapshot.bonded ); ··· 116 147 } 117 148 118 149 static void test_disconnect_clears_live_link_but_keeps_peer_identity() { 150 + // ARRANGE 119 151 uint8_t addr[6]; 120 152 fillAddress( addr, 0x60 ); 121 - 122 153 conn::ConnectionManager::onAuthenticated( addr, 1, 3, 80 ); 123 154 conn::ConnectionManager::updateRssi( -42 ); 124 155 conn::ConnectionManager::setCccdSubscribed( 0x01, true ); 156 + 157 + // ACT 125 158 conn::ConnectionManager::onDisconnected(); 159 + conn::Snapshot snapshot{}; 160 + const bool ok = conn::ConnectionManager::snapshot( snapshot ); 126 161 127 - conn::Snapshot snapshot{}; 128 - TEST_ASSERT_TRUE( conn::ConnectionManager::snapshot( snapshot ) ); 162 + // ASSERT 163 + TEST_ASSERT_TRUE( ok ); 129 164 TEST_ASSERT_FALSE( snapshot.connected ); 130 165 TEST_ASSERT_FALSE( snapshot.authenticated ); 131 166 TEST_ASSERT_EQUAL_UINT8( 0, snapshot.cccdMask ); ··· 134 169 } 135 170 136 171 static void test_mtu_rssi_and_cccd_updates_are_snapshot_visible() { 172 + // ARRANGE 137 173 uint8_t addr[6]; 138 174 fillAddress( addr, 0x80 ); 175 + conn::ConnectionManager::onAuthenticated( addr, 0, 1, 23 ); 139 176 140 - conn::ConnectionManager::onAuthenticated( addr, 0, 1, 23 ); 177 + // ACT 141 178 conn::ConnectionManager::updateMtu( 96 ); 142 179 conn::ConnectionManager::updateRssi( -55 ); 143 180 conn::ConnectionManager::setCccdSubscribed( conn::kCccdInputReportNotifyMask, true ); 144 181 conn::ConnectionManager::setCccdSubscribed( 0x02, true ); 145 182 conn::ConnectionManager::setCccdSubscribed( conn::kCccdInputReportNotifyMask, false ); 183 + conn::Snapshot snapshot{}; 184 + const bool ok = conn::ConnectionManager::snapshot( snapshot ); 146 185 147 - conn::Snapshot snapshot{}; 148 - TEST_ASSERT_TRUE( conn::ConnectionManager::snapshot( snapshot ) ); 186 + // ASSERT 187 + TEST_ASSERT_TRUE( ok ); 149 188 TEST_ASSERT_EQUAL_UINT16( 96, snapshot.mtu ); 150 189 TEST_ASSERT_EQUAL_INT8( -55, snapshot.rssi ); 151 190 TEST_ASSERT_EQUAL_UINT8( 0x02, snapshot.cccdMask ); ··· 158 197 } 159 198 160 199 static void test_cccd_before_auth_is_counted_and_preserved() { 200 + // ARRANGE 161 201 conn::ConnectionManager::setCccdSubscribed( conn::kCccdInputReportNotifyMask, true ); 162 - 163 202 uint8_t addr[6]; 164 203 fillAddress( addr, 0xA0 ); 204 + 205 + // ACT 165 206 conn::ConnectionManager::onAuthenticated( addr, 1, 0, 23 ); 207 + conn::Snapshot snapshot{}; 208 + const bool ok = conn::ConnectionManager::snapshot( snapshot ); 166 209 167 - conn::Snapshot snapshot{}; 168 - TEST_ASSERT_TRUE( conn::ConnectionManager::snapshot( snapshot ) ); 210 + // ASSERT 211 + TEST_ASSERT_TRUE( ok ); 169 212 TEST_ASSERT_TRUE( snapshot.connected ); 170 213 TEST_ASSERT_TRUE( snapshot.authenticated ); 171 214 TEST_ASSERT_EQUAL_UINT8( conn::kCccdInputReportNotifyMask, snapshot.cccdMask ); ··· 176 219 TEST_ASSERT_EQUAL_UINT16( 1, conn::ConnectionManager::cccdWhileDisconnectedCount() ); 177 220 } 178 221 179 - int main( int argc, char** argv ) { 180 - (void)argc; 181 - (void)argv; 222 + // --- Fake function definitions --------------------------------------------- 223 + 224 + // --- Helper definitions ---------------------------------------------------- 225 + static void fillAddress( uint8_t addr[6], uint8_t seed ) { 226 + for ( uint8_t index = 0; index < 6; ++index ) { 227 + addr[index] = static_cast<uint8_t>( seed + index ); 228 + } 229 + } 182 230 231 + // --- main() ---------------------------------------------------------------- 232 + int main( int /*argc*/, char** /*argv*/ ) { 183 233 UNITY_BEGIN(); 184 234 RUN_TEST( test_reset_snapshot_defaults_to_disconnected ); 185 235 RUN_TEST( test_conn_params_match_plan_units );
+71 -38
test/test_ble_event_bus/test_ble_event_bus.cpp test/ble/test_ble_event_bus/test_ble_event_bus.cpp
··· 1 - // Kleidos — Native test for BleEventBus (B3). 2 - // 3 - // Exercises subscribe / unsubscribe / publish semantics on the host PC. 4 - // FreeRTOS path is excluded; native build uses std::mutex internally. 1 + // Kleidos — Native test: BleEventBus (subscribe / unsubscribe / publish semantics) 2 + // Build: pio test -e native -f ble/test_ble_event_bus 5 3 4 + // --- Includes -------------------------------------------------------------- 5 + // Group 1: module under test 6 + #include "ble/stack/ble_event_bus.h" 7 + 8 + // Group 3: stdlib + Unity 6 9 #include <atomic> 7 10 #include <cstdint> 8 11 9 12 #include <unity.h> 10 13 11 - // millis() stub required by native env's build_src_filter (HoldButton.cpp etc.) 12 - static uint32_t fakeMillis = 0; 13 - extern "C" uint32_t millis() { 14 - return fakeMillis; 15 - } 16 - 17 - #include "ble/stack/ble_event_bus.h" 18 - 14 + // --- Constants ------------------------------------------------------------- 19 15 using namespace kleidos::ble; 20 16 using namespace kleidos::ble::stack; 21 17 22 - namespace { 18 + // --- State variables ------------------------------------------------------- 19 + static uint32_t s_fakeMs = 0; 23 20 24 - BleEvent makeEvt( EventKind k ) { 25 - BleEvent e{}; 26 - e.kind = k; 27 - e.timestampMs = 42; 28 - return e; 29 - } 21 + // --- Function declarations ------------------------------------------------- 22 + static BleEvent makeEvt( EventKind k ); 23 + 24 + // --- setUp / tearDown ------------------------------------------------------ 25 + void setUp() { /* intentionally empty */ } 26 + void tearDown() { /* intentionally empty */ } 30 27 31 - void test_publish_with_no_subscribers_increments_dropped() { 28 + // --- Tests ----------------------------------------------------------------- 29 + static void test_publish_with_no_subscribers_increments_dropped() { 30 + // ARRANGE 32 31 const uint32_t before = BleEventBus::dropped(); 32 + 33 + // ACT 33 34 BleEventBus::publish( makeEvt( EventKind::StateChanged ) ); 35 + 36 + // ASSERT 34 37 TEST_ASSERT_GREATER_THAN_UINT32( before, BleEventBus::dropped() ); 35 38 } 36 39 37 - void test_subscribe_then_publish_invokes_handler() { 40 + static void test_subscribe_then_publish_invokes_handler() { 41 + // ARRANGE 38 42 int hits = 0; 39 43 auto id = BleEventBus::subscribe( [&hits]( const BleEvent& e ) { 40 44 if ( e.kind == EventKind::Connected ) ··· 42 46 } ); 43 47 TEST_ASSERT_NOT_EQUAL( 0, id ); 44 48 49 + // ACT 45 50 BleEventBus::publish( makeEvt( EventKind::Connected ) ); 46 51 BleEventBus::publish( makeEvt( EventKind::Disconnected ) ); 52 + 53 + // ASSERT 47 54 TEST_ASSERT_EQUAL_INT( 1, hits ); 48 - 49 55 BleEventBus::unsubscribe( id ); 50 56 BleEventBus::publish( makeEvt( EventKind::Connected ) ); 51 57 TEST_ASSERT_EQUAL_INT( 1, hits ); // unchanged after unsubscribe 52 58 } 53 59 54 - void test_two_subscribers_both_get_event() { 60 + static void test_two_subscribers_both_get_event() { 61 + // ARRANGE 55 62 int a = 0, b = 0; 56 63 auto idA = BleEventBus::subscribe( [&a]( const BleEvent& ) { ++a; } ); 57 64 auto idB = BleEventBus::subscribe( [&b]( const BleEvent& ) { ++b; } ); ··· 59 66 TEST_ASSERT_NOT_EQUAL( 0, idB ); 60 67 TEST_ASSERT_NOT_EQUAL( idA, idB ); 61 68 69 + // ACT 62 70 BleEventBus::publish( makeEvt( EventKind::TypingStarted ) ); 71 + 72 + // ASSERT 63 73 TEST_ASSERT_EQUAL_INT( 1, a ); 64 74 TEST_ASSERT_EQUAL_INT( 1, b ); 65 - 66 75 BleEventBus::unsubscribe( idA ); 67 76 BleEventBus::unsubscribe( idB ); 68 77 } 69 78 70 - void test_full_bus_rejects_extra_subscriber() { 79 + static void test_full_bus_rejects_extra_subscriber() { 80 + // ARRANGE 71 81 SubscriptionId ids[BleEventBus::kMaxHandlers] = {}; 72 82 for ( uint8_t i = 0; i < BleEventBus::kMaxHandlers; ++i ) { 73 83 ids[i] = BleEventBus::subscribe( []( const BleEvent& ) {} ); 74 84 TEST_ASSERT_NOT_EQUAL( 0, ids[i] ); 75 85 } 76 - // 9th subscribe must fail with id 0. 86 + 87 + // ACT — (kMaxHandlers + 1)th subscribe must fail 77 88 auto extra = BleEventBus::subscribe( []( const BleEvent& ) {} ); 78 - TEST_ASSERT_EQUAL( 0, extra ); 79 89 90 + // ASSERT 91 + TEST_ASSERT_EQUAL( 0, extra ); 80 92 for ( uint8_t i = 0; i < BleEventBus::kMaxHandlers; ++i ) { 81 93 BleEventBus::unsubscribe( ids[i] ); 82 94 } 83 95 } 84 96 85 - void test_unsubscribe_with_stale_id_is_noop() { 97 + static void test_unsubscribe_with_stale_id_is_noop() { 98 + // ARRANGE 86 99 int hits = 0; 87 100 auto id = BleEventBus::subscribe( [&hits]( const BleEvent& ) { ++hits; } ); 88 101 BleEventBus::unsubscribe( id ); 102 + 103 + // ACT 89 104 BleEventBus::unsubscribe( id ); // stale; must not crash or affect bus 90 105 BleEventBus::publish( makeEvt( EventKind::Connected ) ); 106 + 107 + // ASSERT 91 108 TEST_ASSERT_EQUAL_INT( 0, hits ); 92 109 } 93 110 94 - void test_empty_handler_is_rejected() { 111 + static void test_empty_handler_is_rejected() { 112 + // ARRANGE 95 113 EventHandler empty; 96 - auto id = BleEventBus::subscribe( empty ); 114 + 115 + // ACT 116 + auto id = BleEventBus::subscribe( empty ); 117 + 118 + // ASSERT 97 119 TEST_ASSERT_EQUAL( 0, id ); 98 120 } 99 121 100 - void test_publish_does_not_deadlock_when_handler_subscribes() { 122 + static void test_publish_does_not_deadlock_when_handler_subscribes() { 123 + // ARRANGE 101 124 int outerHits = 0; 102 125 int innerHits = 0; 103 126 SubscriptionId inner = 0; 104 - 105 127 auto outer = BleEventBus::subscribe( [&outerHits, &innerHits, &inner]( const BleEvent& ) { 106 128 ++outerHits; 107 129 if ( inner == 0 ) { ··· 110 132 } ); 111 133 TEST_ASSERT_NOT_EQUAL( 0, outer ); 112 134 135 + // ACT — first publish; inner is registered during this callback 113 136 BleEventBus::publish( makeEvt( EventKind::Connected ) ); 137 + 138 + // ASSERT — inner was not in the snapshot taken before publish 114 139 TEST_ASSERT_EQUAL_INT( 1, outerHits ); 115 - TEST_ASSERT_EQUAL_INT( 0, innerHits ); // inner registered after snapshot 140 + TEST_ASSERT_EQUAL_INT( 0, innerHits ); 116 141 142 + // ACT — second publish; inner is now in the subscriber list 117 143 BleEventBus::publish( makeEvt( EventKind::Disconnected ) ); 144 + 145 + // ASSERT 118 146 TEST_ASSERT_EQUAL_INT( 2, outerHits ); 119 147 TEST_ASSERT_EQUAL_INT( 1, innerHits ); 120 - 121 148 BleEventBus::unsubscribe( outer ); 122 149 BleEventBus::unsubscribe( inner ); 123 150 } 124 151 125 - } // namespace 152 + // --- Fake function definitions --------------------------------------------- 126 153 127 - void setUp() {} 128 - void tearDown() {} 154 + // --- Helper definitions ---------------------------------------------------- 155 + static BleEvent makeEvt( EventKind k ) { 156 + BleEvent e{}; 157 + e.kind = k; 158 + e.timestampMs = 42; 159 + return e; 160 + } 129 161 130 - int main( int, char** ) { 162 + // --- main() ---------------------------------------------------------------- 163 + int main( int /*argc*/, char** /*argv*/ ) { 131 164 UNITY_BEGIN(); 132 165 RUN_TEST( test_publish_with_no_subscribers_increments_dropped ); 133 166 RUN_TEST( test_subscribe_then_publish_invokes_handler );
test/test_ble_facade_fake/fake_ble_stack.h test/ble/test_ble_facade/fake_ble_stack.h
+85 -30
test/test_ble_facade_fake/test_ble_facade_fake.cpp test/ble/test_ble_facade/test_ble_facade.cpp
··· 1 - // Kleidos — Native test for FakeBleStack (B1 scaffolding). 2 - // 3 - // Exercises the IBleStack contract end-to-end on the host PC. Once B3 lands, 4 - // FSM-state tests will reuse the same fake to simulate BLE without hardware. 1 + // Kleidos — Native test: FakeBleStack (IBleStack contract end-to-end) 2 + // Build: pio test -e native -f ble/test_ble_facade 3 + 4 + // --- Includes -------------------------------------------------------------- 5 + // Group 1: module under test 6 + #include "fake_ble_stack.h" 5 7 8 + // Group 3: stdlib + Unity 6 9 #include <cstdint> 7 10 8 11 #include <unity.h> 9 12 10 - // millis() stub required by native env's build_src_filter. 11 - static uint32_t fakeMillis = 0; 12 - extern "C" uint32_t millis() { 13 - return fakeMillis; 14 - } 13 + // --- Constants ------------------------------------------------------------- 14 + using namespace kleidos::ble; 15 15 16 - #include "fake_ble_stack.h" 16 + // --- State variables ------------------------------------------------------- 17 + static uint32_t s_fakeMs = 0; 17 18 18 - using namespace kleidos::ble; 19 + // --- Function declarations ------------------------------------------------- 20 + // (no forward declarations needed) 19 21 20 - namespace { 22 + // --- setUp / tearDown ------------------------------------------------------ 23 + void setUp() { /* intentionally empty */ } 24 + void tearDown() { /* intentionally empty */ } 21 25 22 - // --------------------------------------------------------------------------- 23 - void test_initial_state_is_off() { 26 + // --- Tests ----------------------------------------------------------------- 27 + static void test_initial_state_is_off() { 28 + // ARRANGE 24 29 FakeBleStack bs; 30 + 31 + // ACT / ASSERT - pure state observation, no side-effectful ACT 25 32 TEST_ASSERT_EQUAL( static_cast<int>( StackState::OFF ), static_cast<int>( bs.state() ) ); 26 33 TEST_ASSERT_FALSE( bs.isConnected() ); 27 34 TEST_ASSERT_EQUAL( 0, bs.bondedCount() ); 28 35 TEST_ASSERT_FALSE( bs.isNcPending() ); 29 36 } 30 37 31 - void test_start_transitions_to_advertising() { 38 + static void test_start_transitions_to_advertising() { 39 + // ARRANGE 32 40 FakeBleStack bs; 33 - TEST_ASSERT_TRUE( bs.start( StartMode::Bounded30s ) ); 41 + 42 + // ACT 43 + const bool started = bs.start( StartMode::Bounded30s ); 44 + 45 + // ASSERT 46 + TEST_ASSERT_TRUE( started ); 34 47 TEST_ASSERT_EQUAL( static_cast<int>( StackState::ADVERTISING ), 35 48 static_cast<int>( bs.state() ) ); 36 49 // Cannot start twice. 37 50 TEST_ASSERT_FALSE( bs.start( StartMode::Bounded30s ) ); 38 51 } 39 52 40 - void test_nc_accept_path_connects() { 53 + static void test_nc_accept_path_connects() { 54 + // ARRANGE 41 55 FakeBleStack bs; 42 56 bs.start( StartMode::StandbyForever ); 43 57 bs.simulatePeerConnected( "phone1" ); 44 - 45 58 TEST_ASSERT_EQUAL( static_cast<int>( StackState::AUTHENTICATING ), 46 59 static_cast<int>( bs.state() ) ); 47 60 TEST_ASSERT_TRUE( bs.isNcPending() ); 48 61 TEST_ASSERT_EQUAL( 123456u, bs.ncPasskey() ); 49 62 63 + // ACT 50 64 bs.resolveNc( true ); 65 + 66 + // ASSERT 51 67 TEST_ASSERT_FALSE( bs.isNcPending() ); 52 68 TEST_ASSERT_TRUE( bs.ncResolved() ); 53 69 TEST_ASSERT_TRUE( bs.ncAccepted() ); ··· 55 71 TEST_ASSERT_EQUAL( static_cast<int>( StackState::CONNECTED ), static_cast<int>( bs.state() ) ); 56 72 } 57 73 58 - void test_nc_reject_path_returns_to_advertising() { 74 + static void test_nc_reject_path_returns_to_advertising() { 75 + // ARRANGE 59 76 FakeBleStack bs; 60 77 bs.start( StartMode::Bounded30s ); 61 78 bs.simulatePeerConnected(); 79 + 80 + // ACT 62 81 bs.resolveNc( false ); 63 82 83 + // ASSERT 64 84 TEST_ASSERT_FALSE( bs.isConnected() ); 65 85 TEST_ASSERT_FALSE( bs.ncAccepted() ); 66 86 TEST_ASSERT_EQUAL( static_cast<int>( StackState::ADVERTISING ), 67 87 static_cast<int>( bs.state() ) ); 68 88 } 69 89 70 - void test_typing_token_lifecycle() { 90 + static void test_typing_token_lifecycle() { 91 + // ARRANGE 71 92 FakeBleStack bs; 72 93 bs.start( StartMode::Bounded30s ); 73 94 bs.simulatePeerConnected(); 74 95 bs.resolveNc( true ); 75 96 97 + // ACT 76 98 auto t = bs.typeCredential( "user@example.com", "S3cret-Pass!" ); 99 + 100 + // ASSERT — running state 77 101 TEST_ASSERT_NOT_EQUAL( 0, t ); 78 102 TEST_ASSERT_EQUAL( static_cast<int>( TypingStatus::Running ), 79 103 static_cast<int>( bs.pollTyping( t ) ) ); 80 104 TEST_ASSERT_EQUAL( static_cast<int>( StackState::TYPING ), static_cast<int>( bs.state() ) ); 81 105 106 + // ACT — complete typing 82 107 bs.simulateTypingDone( true ); 108 + 109 + // ASSERT — success state 83 110 TEST_ASSERT_EQUAL( static_cast<int>( TypingStatus::Success ), 84 111 static_cast<int>( bs.pollTyping( t ) ) ); 85 112 TEST_ASSERT_EQUAL( static_cast<int>( StackState::CONNECTED ), static_cast<int>( bs.state() ) ); 86 - 87 113 TEST_ASSERT_EQUAL_STRING( "user@example.com", bs.typedUser().c_str() ); 88 114 TEST_ASSERT_EQUAL_STRING( "S3cret-Pass!", bs.typedPass().c_str() ); 89 115 } 90 116 91 - void test_typing_rejects_bad_token() { 117 + static void test_typing_rejects_bad_token() { 118 + // ARRANGE 92 119 FakeBleStack bs; 93 120 bs.start( StartMode::Bounded30s ); 94 121 bs.simulatePeerConnected(); 95 122 bs.resolveNc( true ); 96 123 auto good = bs.typeCredential( "a", "b" ); 124 + 125 + // ACT / ASSERT 97 126 TEST_ASSERT_EQUAL( static_cast<int>( TypingStatus::Error ), 98 127 static_cast<int>( bs.pollTyping( good + 99 ) ) ); 99 128 TEST_ASSERT_EQUAL( static_cast<int>( TypingStatus::Error ), 100 129 static_cast<int>( bs.pollTyping( 0 ) ) ); 101 130 } 102 131 103 - void test_typing_without_connection_fails() { 132 + static void test_typing_without_connection_fails() { 133 + // ARRANGE 104 134 FakeBleStack bs; 105 135 bs.start( StartMode::Bounded30s ); 136 + 137 + // ACT 106 138 auto t = bs.typeCredential( "u", "p" ); 139 + 140 + // ASSERT 107 141 TEST_ASSERT_EQUAL( 0, t ); 108 142 } 109 143 110 - void test_disconnect_during_typing_propagates() { 144 + static void test_disconnect_during_typing_propagates() { 145 + // ARRANGE 111 146 FakeBleStack bs; 112 147 bs.start( StartMode::Bounded30s ); 113 148 bs.simulatePeerConnected(); 114 149 bs.resolveNc( true ); 115 150 auto t = bs.typeCredential( "u", "p" ); 151 + 152 + // ACT 116 153 bs.simulatePeerDisconnected(); 154 + 155 + // ASSERT 117 156 TEST_ASSERT_EQUAL( static_cast<int>( TypingStatus::Disconnected ), 118 157 static_cast<int>( bs.pollTyping( t ) ) ); 119 158 TEST_ASSERT_FALSE( bs.isConnected() ); 120 159 } 121 160 122 - void test_cancel_typing_resets_state() { 161 + static void test_cancel_typing_resets_state() { 162 + // ARRANGE 123 163 FakeBleStack bs; 124 164 bs.start( StartMode::Bounded30s ); 125 165 bs.simulatePeerConnected(); 126 166 bs.resolveNc( true ); 127 167 auto t = bs.typeCredential( "u", "p" ); 168 + 169 + // ACT 128 170 bs.cancelTyping( t ); 171 + 172 + // ASSERT 129 173 TEST_ASSERT_EQUAL( static_cast<int>( TypingStatus::Error ), 130 174 static_cast<int>( bs.pollTyping( t ) ) ); 131 175 TEST_ASSERT_EQUAL( static_cast<int>( StackState::CONNECTED ), static_cast<int>( bs.state() ) ); 132 176 } 133 177 134 - void test_bond_management() { 178 + static void test_bond_management() { 179 + // ARRANGE 135 180 FakeBleStack bs; 136 181 uint8_t a[6] = { 0xDE, 0xAD, 0xBE, 0xEF, 0x01, 0x02 }; 137 182 bs.simulateBondPersisted( 0, a, "phone-A" ); 138 183 bs.simulateBondPersisted( 1, a, "phone-B" ); 184 + 185 + // ACT / ASSERT — bond count 139 186 TEST_ASSERT_EQUAL( 2, bs.bondedCount() ); 140 187 188 + // ACT / ASSERT — slot lookup 141 189 BondInfo info; 142 190 TEST_ASSERT_TRUE( bs.getBond( 0, info ) ); 143 191 TEST_ASSERT_EQUAL_STRING( "phone-A", info.name ); 144 192 TEST_ASSERT_TRUE( info.active ); 145 - 146 193 TEST_ASSERT_FALSE( bs.getBond( 2, info ) ); // empty slot 194 + 195 + // ACT — remove 147 196 TEST_ASSERT_TRUE( bs.removeBond( 0 ) ); 148 197 TEST_ASSERT_EQUAL( 1, bs.bondedCount() ); 149 198 TEST_ASSERT_FALSE( bs.removeBond( 0 ) ); // already gone 150 199 } 151 200 152 - void test_stop_is_idempotent() { 201 + static void test_stop_is_idempotent() { 202 + // ARRANGE 153 203 FakeBleStack bs; 154 204 bs.start( StartMode::Bounded30s ); 205 + 206 + // ACT / ASSERT 155 207 TEST_ASSERT_TRUE( bs.stop() ); 156 208 TEST_ASSERT_TRUE( bs.stop() ); 157 209 TEST_ASSERT_EQUAL( static_cast<int>( StackState::OFF ), static_cast<int>( bs.state() ) ); 158 210 } 159 211 160 - } // namespace 212 + // --- Fake function definitions --------------------------------------------- 213 + 214 + // --- Helper definitions ---------------------------------------------------- 215 + // (no arrange helpers) 161 216 162 - // --------------------------------------------------------------------------- 217 + // --- main() ---------------------------------------------------------------- 163 218 int main( int /*argc*/, char** /*argv*/ ) { 164 219 UNITY_BEGIN(); 165 220 RUN_TEST( test_initial_state_is_off );
test/test_ble_hid_typer/fake_hid_sink.h test/ble/test_hid_typer/fake_hid_sink.h
+174 -68
test/test_ble_hid_typer/test_ble_hid_typer.cpp test/ble/test_hid_typer/test_hid_typer.cpp
··· 1 - // Kleidos — Native tests: HidTyper UTF-8 decoder + per-layout dispatch. 2 - // 3 - // Run with: pio test -e native -f test_ble_hid_typer 1 + // Kleidos — Native test: HidTyper (UTF-8 decoder + per-layout dispatch) 2 + // Build: pio test -e native -f ble/test_hid_typer 4 3 4 + // --- Includes -------------------------------------------------------------- 5 + // Group 1: modules under test 6 + #include "ble/profile/hid_scancodes.h" 7 + #include "ble/profile/hid_typer.h" 8 + #include "ble/profile/latin1_tables.h" 9 + 10 + // Group 4: local fakes/helpers 11 + #include "fake_hid_sink.h" 12 + 13 + // Group 3: stdlib + Unity 5 14 #include <cstdint> 6 15 #include <cstring> 7 16 #include <string> 8 17 9 18 #include <unity.h> 10 19 11 - // millis() stub required by native env's build_src_filter. 12 - static uint32_t fakeMillis = 0; 13 - extern "C" uint32_t millis() { 14 - return fakeMillis; 15 - } 16 - 17 - #include "ble/profile/hid_scancodes.h" 18 - #include "ble/profile/hid_typer.h" 19 - #include "ble/profile/latin1_tables.h" 20 - #include "fake_hid_sink.h" 21 - 20 + // --- Constants ------------------------------------------------------------- 22 21 using kleidos::ble::profile::getLatin1Table; 23 22 using kleidos::ble::profile::HidTyper; 24 23 using kleidos::ble::profile::Latin1Entry; ··· 27 26 using kleidos::ble::profile::TypingStatus; 28 27 namespace P = kleidos::ble::profile; 29 28 30 - // =========================================================================== 31 - // UTF-8 decoder 32 - // =========================================================================== 29 + // --- State variables ------------------------------------------------------- 30 + static uint32_t s_fakeMs = 0; 31 + 32 + // --- Function declarations ------------------------------------------------- 33 + // (no forward declarations needed) 34 + 35 + // --- setUp / tearDown ------------------------------------------------------ 36 + void setUp() { /* intentionally empty */ } 37 + void tearDown() { /* intentionally empty */ } 38 + 39 + // --- Tests ----------------------------------------------------------------- 40 + // UTF-8 decoder tests 33 41 34 42 static void test_decode_ascii() { 43 + // ARRANGE 35 44 uint32_t cp = 0; 45 + 46 + // ACT / ASSERT 36 47 TEST_ASSERT_EQUAL( 1u, HidTyper::decodeUtf8( "A", 1, cp ) ); 37 48 TEST_ASSERT_EQUAL( 0x41u, cp ); 38 49 TEST_ASSERT_EQUAL( 1u, HidTyper::decodeUtf8( "\x7f", 1, cp ) ); ··· 40 51 } 41 52 42 53 static void test_decode_2byte() { 54 + // ARRANGE 43 55 uint32_t cp = 0; 44 - // ñ = U+00F1 = C3 B1 56 + 57 + // ACT / ASSERT - ñ = U+00F1 = C3 B1 45 58 TEST_ASSERT_EQUAL( 2u, HidTyper::decodeUtf8( "\xC3\xB1", 2, cp ) ); 46 59 TEST_ASSERT_EQUAL( 0x00F1u, cp ); 47 60 // ü = U+00FC = C3 BC ··· 50 63 } 51 64 52 65 static void test_decode_3byte() { 66 + // ARRANGE 53 67 uint32_t cp = 0; 54 - // € = U+20AC = E2 82 AC 68 + 69 + // ACT / ASSERT - € = U+20AC = E2 82 AC 55 70 TEST_ASSERT_EQUAL( 3u, HidTyper::decodeUtf8( "\xE2\x82\xAC", 3, cp ) ); 56 71 TEST_ASSERT_EQUAL( 0x20ACu, cp ); 57 72 } 58 73 59 74 static void test_decode_4byte() { 75 + // ARRANGE 60 76 uint32_t cp = 0; 61 - // U+1F600 = F0 9F 98 80 (smiley) 77 + 78 + // ACT / ASSERT - U+1F600 = F0 9F 98 80 (smiley) 62 79 TEST_ASSERT_EQUAL( 4u, HidTyper::decodeUtf8( "\xF0\x9F\x98\x80", 4, cp ) ); 63 80 TEST_ASSERT_EQUAL( 0x1F600u, cp ); 64 81 } 65 82 66 83 static void test_decode_truncated() { 84 + // ARRANGE 67 85 uint32_t cp = 0xDEADu; 68 - // 2-byte leader, only 1 byte available 86 + 87 + // ACT / ASSERT - 2-byte leader, only 1 byte available 69 88 TEST_ASSERT_EQUAL( 0u, HidTyper::decodeUtf8( "\xC3", 1, cp ) ); 70 89 // 3-byte leader, 2 bytes 71 90 TEST_ASSERT_EQUAL( 0u, HidTyper::decodeUtf8( "\xE2\x82", 2, cp ) ); 72 91 } 73 92 74 93 static void test_decode_overlong() { 94 + // ARRANGE 75 95 uint32_t cp = 0xDEADu; 76 - // C0 80 = overlong NUL 96 + 97 + // ACT / ASSERT - C0 80 = overlong NUL 77 98 TEST_ASSERT_EQUAL( 0u, HidTyper::decodeUtf8( "\xC0\x80", 2, cp ) ); 78 99 // E0 80 80 = overlong 79 100 TEST_ASSERT_EQUAL( 0u, HidTyper::decodeUtf8( "\xE0\x80\x80", 3, cp ) ); 80 101 } 81 102 82 103 static void test_decode_surrogate() { 104 + // ARRANGE 83 105 uint32_t cp = 0xDEADu; 84 - // ED A0 80 = U+D800 (surrogate, illegal in UTF-8) 106 + 107 + // ACT / ASSERT - ED A0 80 = U+D800 (surrogate, illegal in UTF-8) 85 108 TEST_ASSERT_EQUAL( 0u, HidTyper::decodeUtf8( "\xED\xA0\x80", 3, cp ) ); 86 109 } 87 110 88 111 static void test_decode_bad_continuation() { 112 + // ARRANGE 89 113 uint32_t cp = 0xDEADu; 90 - // C3 41 = leader followed by ASCII (not continuation) 114 + 115 + // ACT / ASSERT - C3 41 = leader followed by ASCII (not continuation) 91 116 TEST_ASSERT_EQUAL( 0u, HidTyper::decodeUtf8( "\xC3\x41", 2, cp ) ); 92 117 } 93 118 94 - // =========================================================================== 95 - // ASCII typing 96 - // =========================================================================== 119 + // ASCII typing tests 97 120 98 121 static void test_us_letter_lowercase() { 122 + // ARRANGE 99 123 FakeHidSink sink; 100 - auto r = HidTyper::typeUtf8( "a", 1, KbLayout::US, sink ); 124 + 125 + // ACT 126 + auto r = HidTyper::typeUtf8( "a", 1, KbLayout::US, sink ); 127 + 128 + // ASSERT 101 129 TEST_ASSERT_EQUAL( static_cast<int>( TypingStatus::Ok ), static_cast<int>( r.status ) ); 102 130 TEST_ASSERT_EQUAL( 1u, sink.reports.size() ); 103 131 TEST_ASSERT_EQUAL( 0, sink.reports[0].mod ); ··· 105 133 } 106 134 107 135 static void test_us_letter_uppercase() { 136 + // ARRANGE 108 137 FakeHidSink sink; 138 + 139 + // ACT 109 140 HidTyper::typeUtf8( "Z", 1, KbLayout::US, sink ); 141 + 142 + // ASSERT 110 143 TEST_ASSERT_EQUAL( 1u, sink.reports.size() ); 111 144 TEST_ASSERT_EQUAL( P::kHidModLShift, sink.reports[0].mod ); 112 145 TEST_ASSERT_EQUAL( 0x04 + ( 'z' - 'a' ), sink.reports[0].scancode ); 113 146 } 114 147 115 148 static void test_us_punctuation() { 149 + // ARRANGE 116 150 FakeHidSink sink; 151 + 152 + // ACT 117 153 HidTyper::typeUtf8( "!@#", 3, KbLayout::US, sink ); 154 + 155 + // ASSERT 118 156 TEST_ASSERT_EQUAL( 3u, sink.reports.size() ); 119 157 // ! = Shift+1 120 158 TEST_ASSERT_EQUAL( P::kHidModLShift, sink.reports[0].mod ); ··· 128 166 } 129 167 130 168 static void test_us_unsupported_high_byte_is_unsupported_not_dropped() { 169 + // ARRANGE 131 170 FakeHidSink sink; 132 - // ñ on US layout -> no Latin-1 entry -> UnsupportedCharacter 171 + 172 + // ACT - ñ on US layout has no Latin-1 entry 133 173 auto r = HidTyper::typeUtf8( "\xC3\xB1", 2, KbLayout::US, sink ); 174 + 175 + // ASSERT 134 176 TEST_ASSERT_EQUAL( static_cast<int>( TypingStatus::UnsupportedCharacter ), 135 177 static_cast<int>( r.status ) ); 136 178 TEST_ASSERT_EQUAL( 0x00F1u, r.failingCodepoint ); ··· 138 180 } 139 181 140 182 static void test_es_ascii_via_table() { 183 + // ARRANGE 141 184 FakeHidSink sink; 142 - // 'a' on ES layout via LAYOUT_ES table 185 + 186 + // ACT - 'a' on ES layout via LAYOUT_ES table 143 187 HidTyper::typeUtf8( "a", 1, KbLayout::ES, sink ); 188 + 189 + // ASSERT 144 190 TEST_ASSERT_EQUAL( 1u, sink.reports.size() ); 145 191 TEST_ASSERT_EQUAL( 0, sink.reports[0].mod ); 146 192 TEST_ASSERT_EQUAL( 0x04, sink.reports[0].scancode ); 147 193 } 148 194 149 - // =========================================================================== 150 - // Latin-1 — single-key entries (ES, DE) 151 - // =========================================================================== 195 + // Latin-1 single-key entry tests (ES, DE) 152 196 153 197 static void test_es_n_tilde_single_keystroke() { 198 + // ARRANGE 154 199 FakeHidSink sink; 155 - auto r = HidTyper::typeUtf8( "\xC3\xB1", 2, KbLayout::ES, sink ); // ñ 200 + 201 + // ACT - ñ = C3 B1 202 + auto r = HidTyper::typeUtf8( "\xC3\xB1", 2, KbLayout::ES, sink ); 203 + 204 + // ASSERT 156 205 TEST_ASSERT_EQUAL( static_cast<int>( TypingStatus::Ok ), static_cast<int>( r.status ) ); 157 206 TEST_ASSERT_EQUAL( 1u, sink.reports.size() ); 158 207 TEST_ASSERT_EQUAL( 0, sink.reports[0].mod ); ··· 160 209 } 161 210 162 211 static void test_es_inverted_punct() { 212 + // ARRANGE 163 213 FakeHidSink sink; 164 - // ¿ = U+00BF = C2 BF, ¡ = U+00A1 = C2 A1 214 + 215 + // ACT - ¿ = U+00BF = C2 BF, ¡ = U+00A1 = C2 A1 165 216 HidTyper::typeUtf8( "\xC2\xBF\xC2\xA1", 4, KbLayout::ES, sink ); 217 + 218 + // ASSERT 166 219 TEST_ASSERT_EQUAL( 2u, sink.reports.size() ); 167 220 TEST_ASSERT_EQUAL( P::kHidModLShift, sink.reports[0].mod ); 168 221 TEST_ASSERT_EQUAL( 0x2E, sink.reports[0].scancode ); ··· 171 224 } 172 225 173 226 static void test_de_umlauts_direct() { 227 + // ARRANGE 174 228 FakeHidSink sink; 175 - // ä = C3 A4 229 + 230 + // ACT - ä = C3 A4 176 231 HidTyper::typeUtf8( "\xC3\xA4", 2, KbLayout::DE, sink ); 232 + 233 + // ASSERT 177 234 TEST_ASSERT_EQUAL( 1u, sink.reports.size() ); 178 235 TEST_ASSERT_EQUAL( 0, sink.reports[0].mod ); 179 236 TEST_ASSERT_EQUAL( 0x34, sink.reports[0].scancode ); 180 237 sink.reset(); 181 - // ß = C3 9F 238 + 239 + // ACT - ß = C3 9F 182 240 HidTyper::typeUtf8( "\xC3\x9F", 2, KbLayout::DE, sink ); 241 + 242 + // ASSERT 183 243 TEST_ASSERT_EQUAL( 1u, sink.reports.size() ); 184 244 TEST_ASSERT_EQUAL( 0, sink.reports[0].mod ); 185 245 TEST_ASSERT_EQUAL( 0x2D, sink.reports[0].scancode ); 186 246 } 187 247 188 - // =========================================================================== 189 - // Latin-1 — dead-key composed (ES, FR, PT) 190 - // =========================================================================== 248 + // Latin-1 dead-key composed tests (ES, FR, PT) 191 249 192 250 static void test_es_a_acute_two_keystrokes() { 251 + // ARRANGE 193 252 FakeHidSink sink; 194 - // á = C3 A1 = dead acute (0x34) + a (0x04) 253 + 254 + // ACT - á = C3 A1 = dead acute (0x34) + a (0x04) 195 255 auto r = HidTyper::typeUtf8( "\xC3\xA1", 2, KbLayout::ES, sink ); 256 + 257 + // ASSERT 196 258 TEST_ASSERT_EQUAL( static_cast<int>( TypingStatus::Ok ), static_cast<int>( r.status ) ); 197 259 TEST_ASSERT_EQUAL( 2u, sink.reports.size() ); 198 260 TEST_ASSERT_EQUAL( 0, sink.reports[0].mod ); ··· 202 264 } 203 265 204 266 static void test_es_u_umlaut_two_keystrokes_with_shift() { 267 + // ARRANGE 205 268 FakeHidSink sink; 206 - // ü = C3 BC = Shift+0x34 (dead diaeresis) + u (0x18) 269 + 270 + // ACT - ü = C3 BC = Shift+0x34 (dead diaeresis) + u (0x18) 207 271 HidTyper::typeUtf8( "\xC3\xBC", 2, KbLayout::ES, sink ); 272 + 273 + // ASSERT 208 274 TEST_ASSERT_EQUAL( 2u, sink.reports.size() ); 209 275 TEST_ASSERT_EQUAL( P::kHidModLShift, sink.reports[0].mod ); 210 276 TEST_ASSERT_EQUAL( 0x34, sink.reports[0].scancode ); ··· 213 279 } 214 280 215 281 static void test_fr_e_circumflex() { 282 + // ARRANGE 216 283 FakeHidSink sink; 217 - // ê = C3 AA = dead ^ (0x2F) + e (0x08) 284 + 285 + // ACT - ê = C3 AA = dead ^ (0x2F) + e (0x08) 218 286 HidTyper::typeUtf8( "\xC3\xAA", 2, KbLayout::FR, sink ); 287 + 288 + // ASSERT 219 289 TEST_ASSERT_EQUAL( 2u, sink.reports.size() ); 220 290 TEST_ASSERT_EQUAL( 0, sink.reports[0].mod ); 221 291 TEST_ASSERT_EQUAL( 0x2F, sink.reports[0].scancode ); ··· 224 294 } 225 295 226 296 static void test_fr_eu_diaeresis_uses_shift() { 297 + // ARRANGE 227 298 FakeHidSink sink; 228 - // ë = C3 AB = Shift+0x2F (dead ¨) + e (0x08) 299 + 300 + // ACT - ë = C3 AB = Shift+0x2F (dead ¨) + e (0x08) 229 301 HidTyper::typeUtf8( "\xC3\xAB", 2, KbLayout::FR, sink ); 302 + 303 + // ASSERT 230 304 TEST_ASSERT_EQUAL( 2u, sink.reports.size() ); 231 305 TEST_ASSERT_EQUAL( P::kHidModLShift, sink.reports[0].mod ); 232 306 TEST_ASSERT_EQUAL( 0x2F, sink.reports[0].scancode ); 233 307 } 234 308 235 309 static void test_pt_c_cedilla_direct() { 310 + // ARRANGE 236 311 FakeHidSink sink; 237 - // ç = C3 A7 312 + 313 + // ACT - ç = C3 A7 238 314 HidTyper::typeUtf8( "\xC3\xA7", 2, KbLayout::PT, sink ); 315 + 316 + // ASSERT 239 317 TEST_ASSERT_EQUAL( 1u, sink.reports.size() ); 240 318 TEST_ASSERT_EQUAL( 0, sink.reports[0].mod ); 241 319 TEST_ASSERT_EQUAL( 0x33, sink.reports[0].scancode ); 242 320 } 243 321 244 - // =========================================================================== 245 - // € sign on every layout that supports it 246 - // =========================================================================== 322 + // € sign tests 247 323 248 324 static void test_euro_sign_es_de_fr_it_pt() { 325 + // ARRANGE 249 326 const char* euro = "\xE2\x82\xAC"; // U+20AC 250 327 KbLayout layouts[] = { KbLayout::ES, KbLayout::DE, KbLayout::FR, KbLayout::IT, KbLayout::PT }; 328 + 329 + // ACT / ASSERT 251 330 for ( auto l : layouts ) { 252 331 FakeHidSink sink; 253 332 auto r = HidTyper::typeUtf8( euro, 3, l, sink ); ··· 259 338 } 260 339 261 340 static void test_uk_pound_sign() { 341 + // ARRANGE 262 342 FakeHidSink sink; 263 - // £ = U+00A3 = C2 A3 343 + 344 + // ACT - £ = U+00A3 = C2 A3 264 345 HidTyper::typeUtf8( "\xC2\xA3", 2, KbLayout::UK, sink ); 346 + 347 + // ASSERT 265 348 TEST_ASSERT_EQUAL( 1u, sink.reports.size() ); 266 349 TEST_ASSERT_EQUAL( P::kHidModLShift, sink.reports[0].mod ); 267 350 TEST_ASSERT_EQUAL( 0x20, sink.reports[0].scancode ); 268 351 } 269 352 270 - // =========================================================================== 271 - // Error paths 272 - // =========================================================================== 353 + // Error path tests 273 354 274 355 static void test_invalid_utf8_returns_error() { 356 + // ARRANGE 275 357 FakeHidSink sink; 276 - auto r = HidTyper::typeUtf8( 358 + 359 + // ACT 360 + auto r = HidTyper::typeUtf8( 277 361 "a\xC3" 278 362 "b", 279 363 3, KbLayout::US, sink ); 364 + 365 + // ASSERT 280 366 TEST_ASSERT_EQUAL( static_cast<int>( TypingStatus::InvalidUtf8 ), 281 367 static_cast<int>( r.status ) ); 282 368 TEST_ASSERT_EQUAL( 1u, r.bytesConsumed ); // stopped at C3 (index 1) 283 - // Only 'a' was emitted before the failure. 284 369 TEST_ASSERT_EQUAL( 1u, sink.reports.size() ); 285 370 } 286 371 287 372 static void test_not_ready_short_circuits() { 373 + // ARRANGE 288 374 FakeHidSink sink; 289 375 sink.setReady( false ); 376 + 377 + // ACT 290 378 auto r = HidTyper::typeUtf8( "abc", 3, KbLayout::US, sink ); 379 + 380 + // ASSERT 291 381 TEST_ASSERT_EQUAL( static_cast<int>( TypingStatus::NotReady ), static_cast<int>( r.status ) ); 292 382 TEST_ASSERT_EQUAL( 0u, sink.reports.size() ); 293 383 } 294 384 295 385 static void test_send_failed_propagates() { 386 + // ARRANGE 296 387 FakeHidSink sink; 297 388 sink.failAfter( 2 ); // succeed on 1st and 2nd, fail on 3rd 389 + 390 + // ACT 298 391 auto r = HidTyper::typeUtf8( "abc", 3, KbLayout::US, sink ); 392 + 393 + // ASSERT 299 394 TEST_ASSERT_EQUAL( static_cast<int>( TypingStatus::SendFailed ), static_cast<int>( r.status ) ); 300 - TEST_ASSERT_EQUAL( 2u, sink.reports.size() ); // only 2 succeeded 395 + TEST_ASSERT_EQUAL( 2u, sink.reports.size() ); 301 396 } 302 397 303 398 static void test_inter_key_delay_called_between_codepoints() { 399 + // ARRANGE 304 400 FakeHidSink sink; 401 + 402 + // ACT 305 403 HidTyper::typeUtf8( "abc", 3, KbLayout::US, sink ); 306 - // 3 codepoints -> 2 inter-key delays (between them, not before/after) 404 + 405 + // ASSERT - 3 codepoints -> 2 inter-key delays (between them, not before/after) 307 406 TEST_ASSERT_EQUAL( 2, sink.delayCount() ); 308 407 TEST_ASSERT_EQUAL( 3u, sink.reports.size() ); 309 408 } 310 409 311 - // =========================================================================== 312 - // End-to-end: realistic password 313 - // =========================================================================== 410 + // End-to-end and table integrity tests 314 411 315 412 static void test_password_with_spanish_chars() { 413 + // ARRANGE 316 414 FakeHidSink sink; 317 - // "año€" = a, ñ, o, € 415 + 416 + // ACT - "año€" = a, ñ, o, € 318 417 auto r = HidTyper::typeUtf8( "a\xC3\xB1o\xE2\x82\xAC", 7, KbLayout::ES, sink ); 418 + 419 + // ASSERT 319 420 TEST_ASSERT_EQUAL( static_cast<int>( TypingStatus::Ok ), static_cast<int>( r.status ) ); 320 421 TEST_ASSERT_EQUAL( 7u, r.bytesConsumed ); 321 422 // a (0x04) + ñ (0x33) + o (0x12) + € (AltGr+0x08) = 4 reports ··· 328 429 } 329 430 330 431 static void test_no_silent_drop_for_unknown_codepoint() { 432 + // ARRANGE 331 433 FakeHidSink sink; 332 - // U+1F600 (smiley) on any layout -> UnsupportedCharacter, NOT silently dropped. 434 + 435 + // ACT - U+1F600 (smiley) on any layout -> UnsupportedCharacter, NOT silently dropped 333 436 auto r = HidTyper::typeUtf8( "\xF0\x9F\x98\x80", 4, KbLayout::ES, sink ); 437 + 438 + // ASSERT 334 439 TEST_ASSERT_EQUAL( static_cast<int>( TypingStatus::UnsupportedCharacter ), 335 440 static_cast<int>( r.status ) ); 336 441 TEST_ASSERT_EQUAL( 0x1F600u, r.failingCodepoint ); 337 442 TEST_ASSERT_EQUAL( 0u, sink.reports.size() ); 338 443 } 339 444 340 - // =========================================================================== 341 - // Table integrity 342 - // =========================================================================== 343 - 344 445 static void test_all_layout_tables_are_sorted() { 446 + // ARRANGE 345 447 KbLayout layouts[] = { KbLayout::ES, KbLayout::DE, KbLayout::FR, 346 448 KbLayout::IT, KbLayout::PT, KbLayout::UK }; 449 + 450 + // ACT / ASSERT 347 451 for ( auto l : layouts ) { 348 452 Latin1Table t = getLatin1Table( l ); 349 453 for ( size_t i = 1; i < t.count; ++i ) { ··· 354 458 } 355 459 } 356 460 357 - // =========================================================================== 358 - // Runner 359 - // =========================================================================== 461 + // --- Fake function definitions --------------------------------------------- 462 + 463 + // --- Helper definitions ---------------------------------------------------- 464 + // (no arrange helpers) 360 465 466 + // --- main() ---------------------------------------------------------------- 361 467 int main( int /*argc*/, char** /*argv*/ ) { 362 468 UNITY_BEGIN(); 363 469 // UTF-8 decoder
-228
test/test_ble_layering/test_ble_layering.cpp
··· 1 - // Kleidos — Native test: BLE layering boundary. 2 - // 3 - // Enforces the §B1 contract: NimBLE is an implementation detail of 4 - // `src/ble/`. Code in `src/states/`, `src/ui/`, `src/web/` must reach BLE 5 - // only through `BleFacade.h` — never through `<NimBLE*.h>` or any 6 - // `nimble*` IDF header. 7 - // 8 - // The test scans the source tree, line by line, and records every 9 - // forbidden include it finds. The test fails if there is any match. 10 - // 11 - // Run with: pio test -e native -f test_ble_layering 12 - 13 - #include <cstdint> 14 - 15 - #include <unity.h> 16 - 17 - // millis() stub required by native env's build_src_filter. 18 - static uint32_t fakeMillis = 0; 19 - extern "C" uint32_t millis() { 20 - return fakeMillis; 21 - } 22 - 23 - #include <algorithm> 24 - #include <cstring> 25 - #include <filesystem> 26 - #include <fstream> 27 - #include <regex> 28 - #include <string> 29 - #include <vector> 30 - 31 - namespace fs = std::filesystem; 32 - 33 - namespace { 34 - 35 - struct Violation { 36 - std::string path; 37 - int line; 38 - std::string content; 39 - }; 40 - 41 - const std::vector<std::string> kScanDirs = { 42 - "src/states", 43 - "src/ui", 44 - "src/web", 45 - }; 46 - 47 - const std::vector<std::string> kAllSourceDirs = { 48 - "src", 49 - }; 50 - 51 - const std::vector<std::regex> kForbiddenPatterns = { 52 - std::regex( R"(#\s*include\s*[<"]NimBLE[A-Za-z0-9_]*\.h[>"])" ), 53 - std::regex( R"(#\s*include\s*[<"]nimble/[A-Za-z0-9_/]+\.h[>"])" ), 54 - std::regex( R"(#\s*include\s*[<"]host/ble_[A-Za-z0-9_]+\.h[>"])" ), 55 - }; 56 - 57 - const std::vector<std::regex> kForbiddenNimbleBondOpenPatterns = { 58 - std::regex( R"(\.begin\s*\(\s*"nimble_bond")" ), 59 - std::regex( R"(nvs_open(?:_from_partition)?\s*\([^;]*"nimble_bond")" ), 60 - }; 61 - 62 - bool isSourceFile( const fs::path& p ) { 63 - static const std::vector<std::string> exts = { ".h", ".hpp", ".cpp", ".c", ".inc" }; 64 - return std::find( exts.begin(), exts.end(), p.extension().string() ) != exts.end(); 65 - } 66 - 67 - std::string locateRepoRoot() { 68 - // Walk upwards from CWD looking for `platformio.ini`. 69 - fs::path here = fs::current_path(); 70 - for ( int depth = 0; depth < 8; ++depth ) { 71 - if ( fs::exists( here / "platformio.ini" ) ) 72 - return here.string(); 73 - if ( here == here.root_path() ) 74 - break; 75 - here = here.parent_path(); 76 - } 77 - return fs::current_path().string(); // best-effort fallback 78 - } 79 - 80 - std::vector<Violation> scan( const std::string& root, const std::vector<std::string>& dirs, 81 - const std::vector<std::regex>& patterns ) { 82 - std::vector<Violation> out; 83 - for ( const auto& rel : dirs ) { 84 - fs::path dir = fs::path( root ) / rel; 85 - if ( !fs::exists( dir ) ) 86 - continue; 87 - for ( auto& entry : fs::recursive_directory_iterator( dir ) ) { 88 - if ( !entry.is_regular_file() || !isSourceFile( entry.path() ) ) 89 - continue; 90 - std::ifstream f( entry.path() ); 91 - std::string line; 92 - int lineNo = 0; 93 - while ( std::getline( f, line ) ) { 94 - ++lineNo; 95 - for ( const auto& re : patterns ) { 96 - if ( std::regex_search( line, re ) ) { 97 - out.push_back( 98 - { fs::relative( entry.path(), root ).string(), lineNo, line } ); 99 - break; 100 - } 101 - } 102 - } 103 - } 104 - } 105 - return out; 106 - } 107 - 108 - std::vector<Violation> scanForLiteral( const std::string& root, 109 - const std::vector<std::string>& dirs, 110 - const std::string& literal, 111 - const std::string& allowedPath = {} ) { 112 - std::vector<Violation> out; 113 - for ( const auto& rel : dirs ) { 114 - fs::path dir = fs::path( root ) / rel; 115 - if ( !fs::exists( dir ) ) 116 - continue; 117 - for ( auto& entry : fs::recursive_directory_iterator( dir ) ) { 118 - if ( !entry.is_regular_file() || !isSourceFile( entry.path() ) ) 119 - continue; 120 - const std::string relative = fs::relative( entry.path(), root ).string(); 121 - if ( !allowedPath.empty() && relative == allowedPath ) 122 - continue; 123 - std::ifstream f( entry.path() ); 124 - std::string line; 125 - int lineNo = 0; 126 - while ( std::getline( f, line ) ) { 127 - ++lineNo; 128 - if ( line.find( literal ) != std::string::npos ) { 129 - out.push_back( { relative, lineNo, line } ); 130 - } 131 - } 132 - } 133 - } 134 - return out; 135 - } 136 - 137 - void failWithViolations( const char* header, const std::vector<Violation>& violations, 138 - const char* footer ) { 139 - std::string msg = header; 140 - msg += "\n"; 141 - for ( const auto& it : violations ) { 142 - msg += " "; 143 - msg += it.path; 144 - msg += ":"; 145 - msg += std::to_string( it.line ); 146 - msg += " "; 147 - msg += it.content; 148 - msg += "\n"; 149 - } 150 - msg += footer; 151 - TEST_FAIL_MESSAGE( msg.c_str() ); 152 - } 153 - 154 - void test_no_nimble_includes_in_fsm_ui_web() { 155 - std::string root = locateRepoRoot(); 156 - auto v = scan( root, kScanDirs, kForbiddenPatterns ); 157 - if ( !v.empty() ) { 158 - failWithViolations( "Forbidden NimBLE includes found:", v, "Use BleFacade.h instead." ); 159 - } 160 - } 161 - 162 - void test_ble_nimble_headers_stay_in_host_adapter() { 163 - std::string root = locateRepoRoot(); 164 - auto v = scan( root, { "src/ble" }, kForbiddenPatterns ); 165 - v.erase( std::remove_if( v.begin(), v.end(), 166 - []( const Violation& item ) { 167 - return item.path == "src/ble/stack/nimble_host.cpp"; 168 - } ), 169 - v.end() ); 170 - if ( !v.empty() ) { 171 - failWithViolations( "NimBLE headers leaked outside host adapter:", v, 172 - "Route runtime operations through NimbleHost.h." ); 173 - } 174 - } 175 - 176 - void test_ble_code_does_not_reopen_legacy_bond_blob() { 177 - std::string root = locateRepoRoot(); 178 - auto v = scanForLiteral( root, { "src/ble" }, "ble_bonds" ); 179 - if ( !v.empty() ) { 180 - failWithViolations( "Legacy BLE bond blob references found:", v, 181 - "Use NameStore instead of the old app-NVS bond blob." ); 182 - } 183 - } 184 - 185 - void test_only_namestore_declares_ble_name_namespace() { 186 - std::string root = locateRepoRoot(); 187 - auto v = 188 - scanForLiteral( root, { "src/ble" }, "kleidos_blnames", "src/ble/bond/name_store.cpp" ); 189 - if ( !v.empty() ) { 190 - failWithViolations( "Unexpected BLE name namespace references found:", v, 191 - "name_store.cpp must remain the sole owner." ); 192 - } 193 - } 194 - 195 - void test_firmware_never_opens_nimble_bond_namespace() { 196 - std::string root = locateRepoRoot(); 197 - auto v = scan( root, kAllSourceDirs, kForbiddenNimbleBondOpenPatterns ); 198 - if ( !v.empty() ) { 199 - failWithViolations( "Direct nimble_bond NVS opens found:", v, 200 - "NimBLE must own its bond namespace internally." ); 201 - } 202 - } 203 - 204 - void test_at_least_one_dir_was_scanned() { 205 - // Sanity: ensures the test isn't silently a no-op because CWD shifted. 206 - std::string root = locateRepoRoot(); 207 - bool anyDir = false; 208 - for ( const auto& rel : kScanDirs ) { 209 - if ( fs::exists( fs::path( root ) / rel ) ) { 210 - anyDir = true; 211 - break; 212 - } 213 - } 214 - TEST_ASSERT_TRUE_MESSAGE( anyDir, "No scan directory exists; check repo root." ); 215 - } 216 - 217 - } // namespace 218 - 219 - int main( int /*argc*/, char** /*argv*/ ) { 220 - UNITY_BEGIN(); 221 - RUN_TEST( test_at_least_one_dir_was_scanned ); 222 - RUN_TEST( test_no_nimble_includes_in_fsm_ui_web ); 223 - RUN_TEST( test_ble_nimble_headers_stay_in_host_adapter ); 224 - RUN_TEST( test_ble_code_does_not_reopen_legacy_bond_blob ); 225 - RUN_TEST( test_only_namestore_declares_ble_name_namespace ); 226 - RUN_TEST( test_firmware_never_opens_nimble_bond_namespace ); 227 - return UNITY_END(); 228 - }
+77 -31
test/test_ble_namestore/test_ble_namestore.cpp test/ble/test_name_store/test_name_store.cpp
··· 1 + // Kleidos — Native test: NameStore (BLE bond name persistence, CRC, NVS diagnostics) 2 + // Build: pio test -e native -f ble/test_name_store 3 + 4 + // --- Includes -------------------------------------------------------------- 5 + // Group 1: module under test 1 6 #include "ble/bond/name_store.h" 2 7 8 + // Group 3: stdlib + Unity 3 9 #include <cstdint> 4 10 #include <cstdio> 5 11 #include <cstring> 6 12 7 13 #include <unity.h> 8 14 15 + // --- Constants ------------------------------------------------------------- 9 16 using kleidos::ble::kMaxBondedHosts; 10 17 using kleidos::ble::kNameMaxBytes; 11 18 using kleidos::ble::kNameRecordActiveFlag; 12 19 using kleidos::ble::NameRecord; 13 20 using kleidos::ble::bond::NameStore; 14 21 15 - static uint32_t fakeMillis = 0; 16 - extern "C" uint32_t millis() { 17 - return fakeMillis; 18 - } 22 + // --- State variables ------------------------------------------------------- 23 + static uint32_t s_fakeMs = 0; 24 + 25 + // --- Function declarations ------------------------------------------------- 26 + static void makeAddress( uint8_t addr[6], uint8_t seed ); 19 27 28 + // --- setUp / tearDown ------------------------------------------------------ 20 29 void setUp() { 21 - fakeMillis = 0; 30 + s_fakeMs = 0; 22 31 NameStore::resetForTest(); 23 32 } 24 33 25 - void tearDown() {} 26 - 27 - static void makeAddress( uint8_t addr[6], uint8_t seed ) { 28 - for ( uint8_t byteIndex = 0; byteIndex < 6; ++byteIndex ) { 29 - addr[byteIndex] = static_cast<uint8_t>( seed + byteIndex ); 30 - } 31 - } 34 + void tearDown() { /* intentionally empty */ } 32 35 36 + // --- Tests ----------------------------------------------------------------- 33 37 static void test_record_layout_matches_plan() { 38 + // ARRANGE - compile-time constants from name_store.h 39 + 40 + // ACT - values observed directly from header constants 41 + 42 + // ASSERT 34 43 TEST_ASSERT_EQUAL_UINT8( 4, kMaxBondedHosts ); 35 44 TEST_ASSERT_EQUAL_UINT16( 24, kNameMaxBytes ); 36 45 TEST_ASSERT_EQUAL_size_t( 44, sizeof( NameRecord ) ); 37 46 } 38 47 39 48 static void test_remember_bond_creates_valid_default_name() { 49 + // ARRANGE 40 50 uint8_t addr[6]; 41 51 makeAddress( addr, 0x10 ); 42 52 43 - TEST_ASSERT_TRUE( NameStore::rememberBond( addr, 0, nullptr, 1234 ) ); 53 + // ACT 54 + const bool ok = NameStore::rememberBond( addr, 0, nullptr, 1234 ); 55 + 56 + // ASSERT 57 + TEST_ASSERT_TRUE( ok ); 44 58 TEST_ASSERT_TRUE( NameStore::dirty() ); 45 59 TEST_ASSERT_EQUAL_UINT8( 1, NameStore::activeCount() ); 46 60 ··· 54 68 } 55 69 56 70 static void test_existing_peer_updates_same_slot() { 71 + // ARRANGE 57 72 uint8_t addr[6]; 58 73 makeAddress( addr, 0x20 ); 74 + NameStore::rememberBond( addr, 1, "Phone", 10 ); 75 + 76 + // ACT 77 + const bool ok = NameStore::rememberBond( addr, 1, "Phone Pro", 20 ); 59 78 60 - TEST_ASSERT_TRUE( NameStore::rememberBond( addr, 1, "Phone", 10 ) ); 61 - TEST_ASSERT_TRUE( NameStore::rememberBond( addr, 1, "Phone Pro", 20 ) ); 79 + // ASSERT 80 + TEST_ASSERT_TRUE( ok ); 62 81 TEST_ASSERT_EQUAL_UINT8( 1, NameStore::activeCount() ); 63 82 64 83 NameRecord record{}; ··· 69 88 } 70 89 71 90 static void test_rename_truncates_and_keeps_crc_valid() { 91 + // ARRANGE 72 92 uint8_t addr[6]; 73 93 makeAddress( addr, 0x30 ); 94 + NameStore::rememberBond( addr, 0, "Old", 100 ); 74 95 75 - TEST_ASSERT_TRUE( NameStore::rememberBond( addr, 0, "Old", 100 ) ); 76 - TEST_ASSERT_TRUE( 77 - NameStore::renameBond( 0, "This name is definitely longer than twenty three bytes" ) ); 96 + // ACT 97 + const bool ok = 98 + NameStore::renameBond( 0, "This name is definitely longer than twenty three bytes" ); 78 99 100 + // ASSERT 101 + TEST_ASSERT_TRUE( ok ); 79 102 NameRecord record{}; 80 103 TEST_ASSERT_TRUE( NameStore::getBond( 0, record ) ); 81 104 TEST_ASSERT_TRUE( NameStore::isValidRecord( record ) ); ··· 84 107 } 85 108 86 109 static void test_forget_clears_matching_address() { 110 + // ARRANGE 87 111 uint8_t firstAddr[6]; 88 112 uint8_t secondAddr[6]; 89 113 makeAddress( firstAddr, 0x40 ); 90 114 makeAddress( secondAddr, 0x50 ); 115 + NameStore::rememberBond( firstAddr, 0, "One", 1 ); 116 + NameStore::rememberBond( secondAddr, 0, "Two", 2 ); 117 + 118 + // ACT 119 + const bool ok = NameStore::forgetBond( firstAddr, 0 ); 91 120 92 - TEST_ASSERT_TRUE( NameStore::rememberBond( firstAddr, 0, "One", 1 ) ); 93 - TEST_ASSERT_TRUE( NameStore::rememberBond( secondAddr, 0, "Two", 2 ) ); 94 - TEST_ASSERT_TRUE( NameStore::forgetBond( firstAddr, 0 ) ); 121 + // ASSERT 122 + TEST_ASSERT_TRUE( ok ); 95 123 TEST_ASSERT_EQUAL_UINT8( 1, NameStore::activeCount() ); 96 124 97 125 NameRecord record{}; ··· 101 129 } 102 130 103 131 static void test_full_table_rejects_fifth_host() { 132 + // ARRANGE 104 133 for ( uint8_t slotIndex = 0; slotIndex < kMaxBondedHosts; ++slotIndex ) { 105 134 uint8_t addr[6]; 106 135 makeAddress( addr, static_cast<uint8_t>( 0x60 + slotIndex * 8 ) ); ··· 109 138 TEST_ASSERT_TRUE( NameStore::rememberBond( addr, 0, name, slotIndex ) ); 110 139 } 111 140 141 + // ACT 112 142 uint8_t extraAddr[6]; 113 143 makeAddress( extraAddr, 0xA0 ); 114 - TEST_ASSERT_FALSE( NameStore::rememberBond( extraAddr, 0, "Extra", 99 ) ); 144 + const bool ok = NameStore::rememberBond( extraAddr, 0, "Extra", 99 ); 145 + 146 + // ASSERT 147 + TEST_ASSERT_FALSE( ok ); 115 148 TEST_ASSERT_EQUAL_UINT8( kMaxBondedHosts, NameStore::activeCount() ); 116 149 } 117 150 118 151 static void test_crc_detects_corruption() { 152 + // ARRANGE 119 153 uint8_t addr[6]; 120 154 makeAddress( addr, 0xB0 ); 121 - TEST_ASSERT_TRUE( NameStore::rememberBond( addr, 0, "Laptop", 77 ) ); 122 - 155 + NameStore::rememberBond( addr, 0, "Laptop", 77 ); 123 156 NameRecord record{}; 124 157 TEST_ASSERT_TRUE( NameStore::getBond( 0, record ) ); 125 158 TEST_ASSERT_TRUE( NameStore::isValidRecord( record ) ); 159 + 160 + // ACT 126 161 record.name[0] = 'X'; 162 + 163 + // ASSERT 127 164 TEST_ASSERT_FALSE( NameStore::isValidRecord( record ) ); 128 165 } 129 166 130 167 static void test_ram_mutations_do_not_touch_nvs_diagnostics() { 168 + // ARRANGE 131 169 uint8_t addr[6]; 132 170 makeAddress( addr, 0xC0 ); 133 - 134 171 TEST_ASSERT_EQUAL_UINT32( 0, NameStore::nvsReadOpenCount() ); 135 172 TEST_ASSERT_EQUAL_UINT32( 0, NameStore::nvsWriteOpenCount() ); 136 173 TEST_ASSERT_EQUAL_UINT32( 0, NameStore::nvsRejectedCount() ); 137 174 138 - TEST_ASSERT_TRUE( NameStore::rememberBond( addr, 0, "Phone", 1 ) ); 139 - TEST_ASSERT_TRUE( NameStore::renameBond( 0, "Phone 2" ) ); 140 - TEST_ASSERT_TRUE( NameStore::forgetBond( addr, 0 ) ); 175 + // ACT 176 + NameStore::rememberBond( addr, 0, "Phone", 1 ); 177 + NameStore::renameBond( 0, "Phone 2" ); 178 + NameStore::forgetBond( addr, 0 ); 141 179 180 + // ASSERT 142 181 TEST_ASSERT_EQUAL_UINT32( 0, NameStore::nvsReadOpenCount() ); 143 182 TEST_ASSERT_EQUAL_UINT32( 0, NameStore::nvsWriteOpenCount() ); 144 183 TEST_ASSERT_EQUAL_UINT32( 0, NameStore::nvsRejectedCount() ); 145 184 } 146 185 147 - int main( int argc, char** argv ) { 148 - (void)argc; 149 - (void)argv; 186 + // --- Fake function definitions --------------------------------------------- 187 + 188 + // --- Helper definitions ---------------------------------------------------- 189 + static void makeAddress( uint8_t addr[6], uint8_t seed ) { 190 + for ( uint8_t byteIndex = 0; byteIndex < 6; ++byteIndex ) { 191 + addr[byteIndex] = static_cast<uint8_t>( seed + byteIndex ); 192 + } 193 + } 150 194 195 + // --- main() ---------------------------------------------------------------- 196 + int main( int /*argc*/, char** /*argv*/ ) { 151 197 UNITY_BEGIN(); 152 198 RUN_TEST( test_record_layout_matches_plan ); 153 199 RUN_TEST( test_remember_bond_creates_valid_default_name );
+52 -12
test/test_ble_numeric_comparison/test_ble_numeric_comparison.cpp test/ble/test_ble_numeric_comparison/test_ble_numeric_comparison.cpp
··· 1 + // Kleidos — Native test: NumericComparison (NC passkey state machine) 2 + // Build: pio test -e native -f ble/test_ble_numeric_comparison 3 + 4 + // --- Includes -------------------------------------------------------------- 5 + // Group 1: module under test 1 6 #include "ble/security/numeric_comparison.h" 2 7 8 + // Group 3: stdlib + Unity 3 9 #include <cstdint> 4 10 5 11 #include <unity.h> 6 12 7 - static uint32_t fakeMillis = 0; 8 - extern "C" uint32_t millis() { 9 - return fakeMillis; 10 - } 11 - 13 + // --- Constants ------------------------------------------------------------- 12 14 namespace nc = kleidos::ble::security::nc; 13 15 16 + // --- State variables ------------------------------------------------------- 17 + static uint32_t s_fakeMs = 0; 18 + 19 + // --- Function declarations ------------------------------------------------- 20 + // (no forward declarations needed) 21 + 22 + // --- setUp / tearDown ------------------------------------------------------ 14 23 void setUp() { 15 - fakeMillis = 0; 24 + s_fakeMs = 0; 16 25 nc::resetForTest(); 17 26 } 18 27 19 - void tearDown() {} 28 + void tearDown() { /* intentionally empty */ } 20 29 30 + // --- Tests ----------------------------------------------------------------- 21 31 static void test_initial_state_is_clear() { 32 + // ARRANGE - fresh state from setUp 33 + 34 + // ACT - observe state directly 35 + 36 + // ASSERT 22 37 TEST_ASSERT_FALSE( nc::pending() ); 23 38 TEST_ASSERT_EQUAL_UINT32( 0, nc::passkey() ); 24 39 ··· 29 44 } 30 45 31 46 static void test_arm_request_publishes_coherent_snapshot() { 32 - nc::armRequestAt( 654321, 1234 ); 47 + // ARRANGE - fresh state from setUp 33 48 49 + // ACT 50 + nc::armRequestAt( 654321, 1234 ); 34 51 const nc::Snapshot snapshot = nc::snapshot(); 52 + 53 + // ASSERT 35 54 TEST_ASSERT_TRUE( snapshot.pending ); 36 55 TEST_ASSERT_EQUAL_UINT32( 654321, snapshot.passkey ); 37 56 TEST_ASSERT_EQUAL_UINT32( 1234, snapshot.requestMs ); ··· 39 58 } 40 59 41 60 static void test_clear_drops_prompt_state() { 61 + // ARRANGE 42 62 nc::armRequestAt( 111222, 100 ); 63 + 64 + // ACT 43 65 nc::clear(); 44 66 67 + // ASSERT 45 68 TEST_ASSERT_FALSE( nc::pending() ); 46 69 TEST_ASSERT_EQUAL_UINT32( 0, nc::passkey() ); 47 70 } 48 71 49 72 static void test_timeout_uses_unsigned_elapsed_time() { 73 + // ARRANGE 50 74 nc::armRequestAt( 333444, 1000 ); 51 75 76 + // ACT / ASSERT - boundary at requestMs + timeoutMs 52 77 TEST_ASSERT_FALSE( nc::timedOutAt( 500, 1499 ) ); 53 78 TEST_ASSERT_FALSE( nc::timedOutAt( 500, 1500 ) ); 54 79 TEST_ASSERT_TRUE( nc::timedOutAt( 500, 1501 ) ); 55 80 } 56 81 57 82 static void test_timeout_survives_millis_wrap() { 83 + // ARRANGE 58 84 nc::armRequestAt( 444555, 0xFFFFFFF0U ); 59 85 86 + // ACT / ASSERT - elapsed wraps through zero 60 87 TEST_ASSERT_FALSE( nc::timedOutAt( 40, 0x00000010U ) ); 61 88 TEST_ASSERT_TRUE( nc::timedOutAt( 40, 0x00000030U ) ); 62 89 } 63 90 64 91 static void test_second_request_replaces_first_atomically() { 92 + // ARRANGE 65 93 nc::armRequestAt( 100001, 10 ); 66 - nc::armRequestAt( 200002, 20 ); 67 94 95 + // ACT 96 + nc::armRequestAt( 200002, 20 ); 68 97 const nc::Snapshot snapshot = nc::snapshot(); 98 + 99 + // ASSERT 69 100 TEST_ASSERT_TRUE( snapshot.pending ); 70 101 TEST_ASSERT_EQUAL_UINT32( 200002, snapshot.passkey ); 71 102 TEST_ASSERT_EQUAL_UINT32( 20, snapshot.requestMs ); 72 103 } 73 104 74 105 static void test_runtime_accept_and_reject_consume_pending_state() { 106 + // ARRANGE + ACT — accept path 75 107 nc::armRequestAt( 123456, 77 ); 76 108 nc::acceptRuntime(); 109 + 110 + // ASSERT 77 111 TEST_ASSERT_FALSE( nc::pending() ); 78 112 113 + // ARRANGE + ACT — reject path 79 114 nc::armRequestAt( 654321, 88 ); 80 115 nc::rejectRuntime(); 116 + 117 + // ASSERT 81 118 TEST_ASSERT_FALSE( nc::pending() ); 82 119 } 83 120 84 - int main( int argc, char** argv ) { 85 - (void)argc; 86 - (void)argv; 121 + // --- Fake function definitions --------------------------------------------- 87 122 123 + // --- Helper definitions ---------------------------------------------------- 124 + // (no arrange helpers) 125 + 126 + // --- main() ---------------------------------------------------------------- 127 + int main( int /*argc*/, char** /*argv*/ ) { 88 128 UNITY_BEGIN(); 89 129 RUN_TEST( test_initial_state_is_clear ); 90 130 RUN_TEST( test_arm_request_publishes_coherent_snapshot );
-162
test/test_brand_registry/test_brand_registry.cpp
··· 1 - // Kleidos - Native unit tests: brand registry lookup and canonicalization. 2 - 3 - #include "ui/brands/brand_bitmap.h" 4 - #include "ui/brands/brand_registry.h" 5 - 6 - #include <unity.h> 7 - 8 - using namespace kleidos::ui::brands; 9 - 10 - extern "C" uint32_t millis() { 11 - return 0; 12 - } 13 - 14 - static void test_size_pixels( void ) { 15 - TEST_ASSERT_EQUAL_UINT8( 16, sizePixels( Size::S16 ) ); 16 - TEST_ASSERT_EQUAL_UINT8( 24, sizePixels( Size::S24 ) ); 17 - TEST_ASSERT_EQUAL_UINT8( 32, sizePixels( Size::S32 ) ); 18 - TEST_ASSERT_EQUAL_UINT8( 48, sizePixels( Size::S48 ) ); 19 - } 20 - 21 - static void test_canonicalize_url_strips_scheme_user_www_port_and_path( void ) { 22 - char out[kMaxCanonicalDomainLen + 1] = {}; 23 - TEST_ASSERT_TRUE( 24 - canonicalizeDomain( " HTTPS://user@WWW.GitHub.com:443/login?x=1 ", out, sizeof( out ) ) ); 25 - TEST_ASSERT_EQUAL_STRING( "github.com", out ); 26 - } 27 - 28 - static void test_canonicalize_plain_service_lowercases( void ) { 29 - char out[kMaxCanonicalDomainLen + 1] = {}; 30 - TEST_ASSERT_TRUE( canonicalizeDomain( "GitHub", out, sizeof( out ) ) ); 31 - TEST_ASSERT_EQUAL_STRING( "github", out ); 32 - } 33 - 34 - static void test_canonicalize_rejects_empty_input( void ) { 35 - char out[kMaxCanonicalDomainLen + 1] = {}; 36 - TEST_ASSERT_FALSE( canonicalizeDomain( " ", out, sizeof( out ) ) ); 37 - TEST_ASSERT_EQUAL_STRING( "", out ); 38 - } 39 - 40 - static void test_hash_domain_matches_canonical_hash( void ) { 41 - TEST_ASSERT_EQUAL_UINT32( fnv1a32( "github.com" ), 42 - hashDomain( "https://www.github.com/settings" ) ); 43 - } 44 - 45 - static void test_fixture_lookup_exact_domain( void ) { 46 - const BrandEntry* entry = findBrand( "github.com", Size::S24, Style::Auto ); 47 - TEST_ASSERT_NOT_NULL( entry ); 48 - TEST_ASSERT_EQUAL_UINT32( 0x49361819UL, entry->domainHash ); 49 - TEST_ASSERT_EQUAL_UINT8( 24, entry->width ); 50 - } 51 - 52 - static void test_fixture_lookup_service_slug( void ) { 53 - const BrandEntry* entry = findBrand( "GitHub", Size::S24, Style::Auto ); 54 - TEST_ASSERT_NOT_NULL( entry ); 55 - TEST_ASSERT_EQUAL_UINT32( 0x22DC7932UL, entry->domainHash ); 56 - } 57 - 58 - static void test_fixture_lookup_subdomain_falls_back_to_root( void ) { 59 - const BrandEntry* entry = findBrand( "mail.google.com", Size::S24, Style::Auto ); 60 - TEST_ASSERT_NOT_NULL( entry ); 61 - TEST_ASSERT_EQUAL_UINT32( 0x7796C245UL, entry->domainHash ); 62 - } 63 - 64 - static void test_lookup_filters_by_size( void ) { 65 - TEST_ASSERT_NULL( findBrand( "github.com", Size::S32, Style::Auto ) ); 66 - } 67 - 68 - static void test_lookup_filters_by_family( void ) { 69 - TEST_ASSERT_NULL( findBrand( "github.com", Size::S24, Style::Mono ) ); 70 - TEST_ASSERT_NOT_NULL( findBrand( "github.com", Size::S24, Style::HighContrast ) ); 71 - } 72 - 73 - static void test_has_brand_uses_available_sizes( void ) { 74 - TEST_ASSERT_TRUE( hasBrand( "https://github.com" ) ); 75 - TEST_ASSERT_FALSE( hasBrand( "example.invalid" ) ); 76 - } 77 - 78 - static void test_rle_decode_literals_and_runs( void ) { 79 - const uint8_t encoded[] = { 80 - 0x02, 0x10, 0x11, 0x12, 0x82, 0x7F, 0x01, 0x20, 0x21, 81 - }; 82 - uint8_t decoded[8] = {}; 83 - const bitmap::DecodeResult result = 84 - bitmap::decodeRle( encoded, sizeof( encoded ), decoded, sizeof( decoded ) ); 85 - const uint8_t expected[] = { 0x10, 0x11, 0x12, 0x7F, 0x7F, 0x7F, 0x20, 0x21 }; 86 - TEST_ASSERT_EQUAL_UINT8( static_cast<uint8_t>( bitmap::DecodeStatus::Ok ), 87 - static_cast<uint8_t>( result.status ) ); 88 - TEST_ASSERT_EQUAL_size_t( sizeof( encoded ), result.bytesRead ); 89 - TEST_ASSERT_EQUAL_size_t( sizeof( decoded ), result.bytesWritten ); 90 - TEST_ASSERT_EQUAL_UINT8_ARRAY( expected, decoded, sizeof( expected ) ); 91 - } 92 - 93 - static void test_rle_decode_rejects_truncated_run( void ) { 94 - const uint8_t encoded[] = { 0x82 }; 95 - uint8_t decoded[3] = {}; 96 - const bitmap::DecodeResult result = 97 - bitmap::decodeRle( encoded, sizeof( encoded ), decoded, sizeof( decoded ) ); 98 - TEST_ASSERT_EQUAL_UINT8( static_cast<uint8_t>( bitmap::DecodeStatus::InputTooShort ), 99 - static_cast<uint8_t>( result.status ) ); 100 - } 101 - 102 - static void test_decode_color_entry_composes_alpha_mask( void ) { 103 - const uint8_t atlas[] = { 104 - 0x07, 0xF8, 0x00, 0x07, 0xE0, 0x00, 0x1F, 0xFF, 0xFF, 0x00, 0xA0, 105 - }; 106 - const BrandEntry entry{ 0x12345678UL, 107 - 0, 108 - sizeof( atlas ), 109 - 0xF800, 110 - 2, 111 - 2, 112 - static_cast<uint8_t>( AssetFamily::Color ), 113 - kFlagHasAlphaMask }; 114 - uint16_t pixels[4] = {}; 115 - uint8_t scratch[bitmap::maskByteCount( 2, 2 )] = {}; 116 - TEST_ASSERT_TRUE( bitmap::decodeEntryToRgb565( entry, atlas, sizeof( atlas ), 0x0000, 0xFFFF, 117 - false, scratch, sizeof( scratch ), pixels, 4 ) ); 118 - TEST_ASSERT_EQUAL_HEX16( 0xF800, pixels[0] ); 119 - TEST_ASSERT_EQUAL_HEX16( 0x0000, pixels[1] ); 120 - TEST_ASSERT_EQUAL_HEX16( 0x001F, pixels[2] ); 121 - TEST_ASSERT_EQUAL_HEX16( 0x0000, pixels[3] ); 122 - } 123 - 124 - static void test_decode_mono_entry_tints_mask( void ) { 125 - const uint8_t atlas[] = { 0x00, 0x90 }; 126 - const BrandEntry entry{ 0x12345678UL, 127 - 0, 128 - sizeof( atlas ), 129 - 0xFFFF, 130 - 2, 131 - 2, 132 - static_cast<uint8_t>( AssetFamily::Mono ), 133 - 0 }; 134 - uint16_t pixels[4] = {}; 135 - uint8_t scratch[bitmap::maskByteCount( 2, 2 )] = {}; 136 - TEST_ASSERT_TRUE( bitmap::decodeEntryToRgb565( entry, atlas, sizeof( atlas ), 0x1111, 0xEEEE, 137 - false, scratch, sizeof( scratch ), pixels, 4 ) ); 138 - TEST_ASSERT_EQUAL_HEX16( 0xEEEE, pixels[0] ); 139 - TEST_ASSERT_EQUAL_HEX16( 0x1111, pixels[1] ); 140 - TEST_ASSERT_EQUAL_HEX16( 0x1111, pixels[2] ); 141 - TEST_ASSERT_EQUAL_HEX16( 0xEEEE, pixels[3] ); 142 - } 143 - 144 - int main( int, char** ) { 145 - UNITY_BEGIN(); 146 - RUN_TEST( test_size_pixels ); 147 - RUN_TEST( test_canonicalize_url_strips_scheme_user_www_port_and_path ); 148 - RUN_TEST( test_canonicalize_plain_service_lowercases ); 149 - RUN_TEST( test_canonicalize_rejects_empty_input ); 150 - RUN_TEST( test_hash_domain_matches_canonical_hash ); 151 - RUN_TEST( test_fixture_lookup_exact_domain ); 152 - RUN_TEST( test_fixture_lookup_service_slug ); 153 - RUN_TEST( test_fixture_lookup_subdomain_falls_back_to_root ); 154 - RUN_TEST( test_lookup_filters_by_size ); 155 - RUN_TEST( test_lookup_filters_by_family ); 156 - RUN_TEST( test_has_brand_uses_available_sizes ); 157 - RUN_TEST( test_rle_decode_literals_and_runs ); 158 - RUN_TEST( test_rle_decode_rejects_truncated_run ); 159 - RUN_TEST( test_decode_color_entry_composes_alpha_mask ); 160 - RUN_TEST( test_decode_mono_entry_tints_mask ); 161 - return UNITY_END(); 162 - }
-111
test/test_bruteforce/test_bruteforce.cpp
··· 1 - // Kleidos — Native test: BruteForceGuard logic 2 - // Tests lockout duration calculation without hardware. 3 - // Isolates the pure logic — NVS/RTC not available natively. 4 - 5 - #include <cstdint> 6 - 7 - #include <unity.h> 8 - 9 - // millis() stub required because HoldButton.cpp is linked in native env 10 - static uint32_t fakeMillis = 0; 11 - extern "C" uint32_t millis() { 12 - return fakeMillis; 13 - } 14 - 15 - // Replicate BruteForceGuard lockout constants 16 - static constexpr uint8_t WARN_THRESHOLD = 3; 17 - static constexpr uint8_t SHORT_LOCK_START = 4; 18 - static constexpr uint8_t LONG_LOCK_START = 7; 19 - static constexpr uint8_t WIPE_THRESHOLD = 10; 20 - static constexpr uint32_t SHORT_LOCK_SEC = 30; 21 - static constexpr uint32_t LONG_LOCK_SEC = 300; 22 - 23 - // Replicate the lockout duration function 24 - static uint32_t getLockoutDuration( uint8_t attempts ) { 25 - if ( attempts < SHORT_LOCK_START ) 26 - return 0; 27 - if ( attempts < LONG_LOCK_START ) 28 - return SHORT_LOCK_SEC; 29 - return LONG_LOCK_SEC; 30 - } 31 - 32 - static bool shouldWipe( uint8_t attempts ) { 33 - return attempts >= WIPE_THRESHOLD; 34 - } 35 - 36 - // --------------------------------------------------------------------------- 37 - // Tests 38 - // --------------------------------------------------------------------------- 39 - void test_no_lockout_first_3_attempts() { 40 - TEST_ASSERT_EQUAL( 0, getLockoutDuration( 1 ) ); 41 - TEST_ASSERT_EQUAL( 0, getLockoutDuration( 2 ) ); 42 - TEST_ASSERT_EQUAL( 0, getLockoutDuration( 3 ) ); 43 - } 44 - 45 - void test_30s_lockout_after_4_attempts() { 46 - TEST_ASSERT_EQUAL( SHORT_LOCK_SEC, getLockoutDuration( 4 ) ); 47 - TEST_ASSERT_EQUAL( SHORT_LOCK_SEC, getLockoutDuration( 5 ) ); 48 - TEST_ASSERT_EQUAL( SHORT_LOCK_SEC, getLockoutDuration( 6 ) ); 49 - } 50 - 51 - void test_300s_lockout_after_7_attempts() { 52 - TEST_ASSERT_EQUAL( LONG_LOCK_SEC, getLockoutDuration( 7 ) ); 53 - TEST_ASSERT_EQUAL( LONG_LOCK_SEC, getLockoutDuration( 8 ) ); 54 - TEST_ASSERT_EQUAL( LONG_LOCK_SEC, getLockoutDuration( 9 ) ); 55 - } 56 - 57 - void test_no_wipe_before_10() { 58 - for ( uint8_t i = 0; i < WIPE_THRESHOLD; i++ ) { 59 - TEST_ASSERT_FALSE( shouldWipe( i ) ); 60 - } 61 - } 62 - 63 - void test_wipe_at_10_attempts() { 64 - TEST_ASSERT_TRUE( shouldWipe( 10 ) ); 65 - TEST_ASSERT_TRUE( shouldWipe( 11 ) ); 66 - TEST_ASSERT_TRUE( shouldWipe( 255 ) ); 67 - } 68 - 69 - void test_zero_attempts_no_lockout() { 70 - TEST_ASSERT_EQUAL( 0, getLockoutDuration( 0 ) ); 71 - TEST_ASSERT_FALSE( shouldWipe( 0 ) ); 72 - } 73 - 74 - void test_boundary_short_to_long() { 75 - // Attempt 6 = short lockout, attempt 7 = long lockout 76 - TEST_ASSERT_EQUAL( SHORT_LOCK_SEC, getLockoutDuration( 6 ) ); 77 - TEST_ASSERT_EQUAL( LONG_LOCK_SEC, getLockoutDuration( 7 ) ); 78 - } 79 - 80 - void test_boundary_long_to_wipe() { 81 - // Attempt 9 = long lockout + no wipe, attempt 10 = wipe 82 - TEST_ASSERT_EQUAL( LONG_LOCK_SEC, getLockoutDuration( 9 ) ); 83 - TEST_ASSERT_FALSE( shouldWipe( 9 ) ); 84 - TEST_ASSERT_TRUE( shouldWipe( 10 ) ); 85 - } 86 - 87 - void test_lockout_monotonically_increases() { 88 - uint32_t prev = 0; 89 - for ( uint8_t i = 1; i <= 9; i++ ) { 90 - uint32_t current = getLockoutDuration( i ); 91 - TEST_ASSERT_GREATER_OR_EQUAL( prev, current ); 92 - prev = current; 93 - } 94 - } 95 - 96 - // --------------------------------------------------------------------------- 97 - // Runner 98 - // --------------------------------------------------------------------------- 99 - int main( int argc, char** argv ) { 100 - UNITY_BEGIN(); 101 - RUN_TEST( test_no_lockout_first_3_attempts ); 102 - RUN_TEST( test_30s_lockout_after_4_attempts ); 103 - RUN_TEST( test_300s_lockout_after_7_attempts ); 104 - RUN_TEST( test_no_wipe_before_10 ); 105 - RUN_TEST( test_wipe_at_10_attempts ); 106 - RUN_TEST( test_zero_attempts_no_lockout ); 107 - RUN_TEST( test_boundary_short_to_long ); 108 - RUN_TEST( test_boundary_long_to_wipe ); 109 - RUN_TEST( test_lockout_monotonically_increases ); 110 - return UNITY_END(); 111 - }
-267
test/test_bruteforce_ext/test_bruteforce_ext.cpp
··· 1 - // Kleidos — Native test: BruteForceGuard extended logic 2 - // Tests getRemainingLockout calculation, lockout state machine, 3 - // and edge cases not covered by the base test. 4 - 5 - #include <cstdint> 6 - #include <cstring> 7 - 8 - #include <unity.h> 9 - 10 - // millis() stub 11 - static uint32_t fakeMillis = 0; 12 - extern "C" uint32_t millis() { 13 - return fakeMillis; 14 - } 15 - 16 - // --------------------------------------------------------------------------- 17 - // Replicate constants from BruteForceGuard.cpp 18 - // --------------------------------------------------------------------------- 19 - static constexpr uint8_t WARN_THRESHOLD = 3; 20 - static constexpr uint8_t SHORT_LOCK_START = 4; 21 - static constexpr uint8_t LONG_LOCK_START = 7; 22 - static constexpr uint8_t WIPE_THRESHOLD = 10; 23 - static constexpr uint32_t SHORT_LOCK_SEC = 30; 24 - static constexpr uint32_t LONG_LOCK_SEC = 300; 25 - 26 - static uint32_t getLockoutDuration( uint8_t attempts ) { 27 - if ( attempts < SHORT_LOCK_START ) 28 - return 0; 29 - if ( attempts < LONG_LOCK_START ) 30 - return SHORT_LOCK_SEC; 31 - return LONG_LOCK_SEC; 32 - } 33 - 34 - static bool shouldWipe( uint8_t attempts ) { 35 - return attempts >= WIPE_THRESHOLD; 36 - } 37 - 38 - // --------------------------------------------------------------------------- 39 - // Simulate RTC-based lockout tracking 40 - // A simplified model of registerFailure + getRemainingLockout 41 - // --------------------------------------------------------------------------- 42 - struct SimBruteForce { 43 - uint8_t attempts = 0; 44 - uint32_t lockoutUntilSec = 0; // absolute RTC second when lockout expires 45 - uint32_t currentTimeSec = 0; // simulated RTC time 46 - 47 - uint32_t registerFailure() { 48 - attempts++; 49 - if ( attempts >= WIPE_THRESHOLD ) 50 - return UINT32_MAX; 51 - 52 - uint32_t lockSec = getLockoutDuration( attempts ); 53 - if ( lockSec > 0 ) { 54 - lockoutUntilSec = currentTimeSec + lockSec; 55 - } 56 - return lockSec; 57 - } 58 - 59 - uint32_t getRemainingLockout() const { 60 - if ( lockoutUntilSec == 0 ) 61 - return 0; 62 - if ( currentTimeSec >= lockoutUntilSec ) 63 - return 0; 64 - return lockoutUntilSec - currentTimeSec; 65 - } 66 - 67 - void resetAttempts() { 68 - attempts = 0; 69 - lockoutUntilSec = 0; 70 - } 71 - }; 72 - 73 - // --------------------------------------------------------------------------- 74 - // getRemainingLockout tests 75 - // --------------------------------------------------------------------------- 76 - void test_remaining_lockout_no_failures() { 77 - SimBruteForce bf; 78 - bf.currentTimeSec = 1000; 79 - TEST_ASSERT_EQUAL( 0, bf.getRemainingLockout() ); 80 - } 81 - 82 - void test_remaining_lockout_below_threshold() { 83 - SimBruteForce bf; 84 - bf.currentTimeSec = 1000; 85 - bf.registerFailure(); // 1 86 - bf.registerFailure(); // 2 87 - bf.registerFailure(); // 3 88 - TEST_ASSERT_EQUAL( 0, bf.getRemainingLockout() ); 89 - } 90 - 91 - void test_remaining_lockout_30s_immediately() { 92 - SimBruteForce bf; 93 - bf.currentTimeSec = 1000; 94 - bf.registerFailure(); // 1 95 - bf.registerFailure(); // 2 96 - bf.registerFailure(); // 3 97 - uint32_t lockSec = bf.registerFailure(); // 4 → 30s 98 - TEST_ASSERT_EQUAL( 30, lockSec ); 99 - TEST_ASSERT_EQUAL( 30, bf.getRemainingLockout() ); 100 - } 101 - 102 - void test_remaining_lockout_decreases_with_time() { 103 - SimBruteForce bf; 104 - bf.currentTimeSec = 1000; 105 - for ( int i = 0; i < 4; i++ ) 106 - bf.registerFailure(); 107 - // lockoutUntilSec = 1030 108 - 109 - bf.currentTimeSec = 1010; // 10s passed 110 - TEST_ASSERT_EQUAL( 20, bf.getRemainingLockout() ); 111 - 112 - bf.currentTimeSec = 1025; // 25s passed 113 - TEST_ASSERT_EQUAL( 5, bf.getRemainingLockout() ); 114 - } 115 - 116 - void test_remaining_lockout_expires() { 117 - SimBruteForce bf; 118 - bf.currentTimeSec = 1000; 119 - for ( int i = 0; i < 4; i++ ) 120 - bf.registerFailure(); 121 - 122 - bf.currentTimeSec = 1030; // exactly at lockout end 123 - TEST_ASSERT_EQUAL( 0, bf.getRemainingLockout() ); 124 - 125 - bf.currentTimeSec = 1031; // past lockout 126 - TEST_ASSERT_EQUAL( 0, bf.getRemainingLockout() ); 127 - } 128 - 129 - void test_remaining_lockout_300s_at_7_attempts() { 130 - SimBruteForce bf; 131 - bf.currentTimeSec = 5000; 132 - for ( int i = 0; i < 7; i++ ) 133 - bf.registerFailure(); 134 - // 7th attempt → 300s lockout 135 - TEST_ASSERT_EQUAL( 300, bf.getRemainingLockout() ); 136 - 137 - bf.currentTimeSec = 5150; // 150s passed 138 - TEST_ASSERT_EQUAL( 150, bf.getRemainingLockout() ); 139 - } 140 - 141 - void test_remaining_lockout_progressive() { 142 - SimBruteForce bf; 143 - bf.currentTimeSec = 0; 144 - 145 - // 1-3: no lockout 146 - for ( int i = 0; i < 3; i++ ) { 147 - uint32_t lock = bf.registerFailure(); 148 - TEST_ASSERT_EQUAL( 0, lock ); 149 - } 150 - 151 - // 4: 30s lockout 152 - uint32_t lock4 = bf.registerFailure(); 153 - TEST_ASSERT_EQUAL( 30, lock4 ); 154 - bf.currentTimeSec = 31; // wait it out 155 - 156 - // 5: still 30s 157 - uint32_t lock5 = bf.registerFailure(); 158 - TEST_ASSERT_EQUAL( 30, lock5 ); 159 - bf.currentTimeSec = 62; 160 - 161 - // 6: still 30s 162 - uint32_t lock6 = bf.registerFailure(); 163 - TEST_ASSERT_EQUAL( 30, lock6 ); 164 - bf.currentTimeSec = 93; 165 - 166 - // 7: 300s 167 - uint32_t lock7 = bf.registerFailure(); 168 - TEST_ASSERT_EQUAL( 300, lock7 ); 169 - TEST_ASSERT_EQUAL( 300, bf.getRemainingLockout() ); 170 - } 171 - 172 - void test_wipe_returns_uint32_max() { 173 - SimBruteForce bf; 174 - bf.currentTimeSec = 100; 175 - 176 - for ( int i = 0; i < 9; i++ ) 177 - bf.registerFailure(); 178 - uint32_t lock10 = bf.registerFailure(); // 10th → wipe 179 - TEST_ASSERT_EQUAL( UINT32_MAX, lock10 ); 180 - TEST_ASSERT_TRUE( shouldWipe( bf.attempts ) ); 181 - } 182 - 183 - void test_reset_clears_lockout() { 184 - SimBruteForce bf; 185 - bf.currentTimeSec = 100; 186 - for ( int i = 0; i < 5; i++ ) 187 - bf.registerFailure(); 188 - TEST_ASSERT_GREATER_THAN( 0, bf.getRemainingLockout() ); 189 - 190 - bf.resetAttempts(); 191 - TEST_ASSERT_EQUAL( 0, bf.getRemainingLockout() ); 192 - TEST_ASSERT_EQUAL( 0, bf.attempts ); 193 - } 194 - 195 - // --------------------------------------------------------------------------- 196 - // Edge: time very close to lockout boundary 197 - // --------------------------------------------------------------------------- 198 - void test_remaining_lockout_one_second_left() { 199 - SimBruteForce bf; 200 - bf.currentTimeSec = 1000; 201 - for ( int i = 0; i < 4; i++ ) 202 - bf.registerFailure(); 203 - // lockoutUntilSec = 1030 204 - 205 - bf.currentTimeSec = 1029; 206 - TEST_ASSERT_EQUAL( 1, bf.getRemainingLockout() ); 207 - } 208 - 209 - // --------------------------------------------------------------------------- 210 - // Edge: multiple failures during active lockout (lockout extends) 211 - // --------------------------------------------------------------------------- 212 - void test_lockout_extends_on_repeated_failures() { 213 - SimBruteForce bf; 214 - bf.currentTimeSec = 1000; 215 - 216 - // 4th failure → lockout until 1030 217 - for ( int i = 0; i < 4; i++ ) 218 - bf.registerFailure(); 219 - TEST_ASSERT_EQUAL( 30, bf.getRemainingLockout() ); 220 - 221 - // 5th failure at 1010 → new lockout until 1040 222 - bf.currentTimeSec = 1010; 223 - bf.registerFailure(); // 5th 224 - TEST_ASSERT_EQUAL( 30, bf.getRemainingLockout() ); // 1040 - 1010 = 30 225 - 226 - // At 1035, still locked (until 1040) 227 - bf.currentTimeSec = 1035; 228 - TEST_ASSERT_EQUAL( 5, bf.getRemainingLockout() ); 229 - } 230 - 231 - // --------------------------------------------------------------------------- 232 - // Edge: RTC total seconds calculation 233 - // --------------------------------------------------------------------------- 234 - void test_rtc_total_seconds_computation() { 235 - // Verify hours*3600 + minutes*60 + seconds as used in BruteForceGuard 236 - uint8_t hours = 14, minutes = 30, seconds = 45; 237 - uint32_t totalSec = hours * 3600u + minutes * 60u + seconds; 238 - TEST_ASSERT_EQUAL( 52245, totalSec ); 239 - 240 - // Midnight 241 - uint32_t midnight = 0 * 3600u + 0 * 60u + 0; 242 - TEST_ASSERT_EQUAL( 0, midnight ); 243 - 244 - // End of day 245 - uint32_t endOfDay = 23 * 3600u + 59 * 60u + 59; 246 - TEST_ASSERT_EQUAL( 86399, endOfDay ); 247 - } 248 - 249 - // --------------------------------------------------------------------------- 250 - // Runner 251 - // --------------------------------------------------------------------------- 252 - int main( int argc, char** argv ) { 253 - UNITY_BEGIN(); 254 - RUN_TEST( test_remaining_lockout_no_failures ); 255 - RUN_TEST( test_remaining_lockout_below_threshold ); 256 - RUN_TEST( test_remaining_lockout_30s_immediately ); 257 - RUN_TEST( test_remaining_lockout_decreases_with_time ); 258 - RUN_TEST( test_remaining_lockout_expires ); 259 - RUN_TEST( test_remaining_lockout_300s_at_7_attempts ); 260 - RUN_TEST( test_remaining_lockout_progressive ); 261 - RUN_TEST( test_wipe_returns_uint32_max ); 262 - RUN_TEST( test_reset_clears_lockout ); 263 - RUN_TEST( test_remaining_lockout_one_second_left ); 264 - RUN_TEST( test_lockout_extends_on_repeated_failures ); 265 - RUN_TEST( test_rtc_total_seconds_computation ); 266 - return UNITY_END(); 267 - }
+106 -43
test/test_change_pin_logic/test_change_pin_logic.cpp test/states/test_change_pin_logic/test_change_pin_logic.cpp
··· 1 - // Kleidos — Native tests for the Change-PIN wizard FSM. 2 - // 3 - // Covers: verify path (ok / wrong_current), new PIN length enforcement, 4 - // confirm match/mismatch, re-encrypt success/failure, bruteforce counter 5 - // integration via a simple mock. 1 + // Kleidos — Native test: ChangePinLogic (Change-PIN wizard FSM) 2 + // Build: pio test -e native -f states/test_change_pin_logic 6 3 4 + // --- File header ----------------------------------------------------------- 5 + 6 + // Group 1: module under test 7 7 #include "states/change_pin_logic.h" 8 8 9 + // Group 3: stdlib + Unity 9 10 #include <cstdint> 10 11 #include <cstring> 11 12 12 13 #include <unity.h> 13 14 14 - extern "C" uint32_t millis() { 15 - return 0; 16 - } 15 + // --- State variables ------------------------------------------------------- 17 16 18 - // --------------------------------------------------------------------------- 19 - // Bruteforce counter mock — the wizard does not own the counter; the UI 20 - // layer calls BruteForceGuard::registerFailure() whenever verifyCurrent() 21 - // returns WRONG_CURRENT. We model that contract here. 22 - // --------------------------------------------------------------------------- 23 - namespace mock { 24 - uint32_t bruteforceFailures = 0; 25 - void reset() { 26 - bruteforceFailures = 0; 27 - } 28 - void onWrongCurrent() { 29 - ++bruteforceFailures; 30 - } 31 - } // namespace mock 17 + struct { 18 + uint32_t bruteforceFailures; 19 + } spy; 20 + 21 + // --- Function declarations ------------------------------------------------- 22 + 23 + static void onWrongCurrent(); 24 + 25 + // --- setUp / tearDown ------------------------------------------------------ 32 26 33 27 void setUp() { 34 - mock::reset(); 28 + spy = {}; 35 29 } 36 - void tearDown() {} 37 30 38 - void test_initial_step_is_verify_current() { 31 + void tearDown() { /* intentionally empty */ } 32 + 33 + // --- Tests ----------------------------------------------------------------- 34 + 35 + static void test_initial_step_is_verify_current() { 36 + // ARRANGE + ACT 39 37 ChangePinLogic l; 38 + 39 + // ASSERT 40 40 TEST_ASSERT_EQUAL( static_cast<int>( ChangePinStep::VerifyCurrent ), 41 41 static_cast<int>( l.step() ) ); 42 42 TEST_ASSERT_EQUAL( 0, l.newPinLen() ); 43 43 } 44 44 45 - void test_verify_current_ok_advances_to_enter_new() { 45 + static void test_verify_current_ok_advances_to_enter_new() { 46 + // ARRANGE 46 47 ChangePinLogic l; 47 - auto r = l.verifyCurrent( true ); 48 + 49 + // ACT 50 + auto r = l.verifyCurrent( true ); 51 + 52 + // ASSERT 48 53 TEST_ASSERT_EQUAL( static_cast<int>( ChangePinResult::CONTINUE ), static_cast<int>( r ) ); 49 54 TEST_ASSERT_EQUAL( static_cast<int>( ChangePinStep::EnterNew ), static_cast<int>( l.step() ) ); 50 55 } 51 56 52 - void test_verify_current_wrong_stays_and_signals() { 57 + static void test_verify_current_wrong_stays_and_signals() { 58 + // ARRANGE 53 59 ChangePinLogic l; 54 - auto r = l.verifyCurrent( false ); 60 + 61 + // ACT 62 + auto r = l.verifyCurrent( false ); 55 63 if ( r == ChangePinResult::WrongCurrent ) { 56 - mock::onWrongCurrent(); 64 + onWrongCurrent(); 57 65 } 66 + 67 + // ASSERT 58 68 TEST_ASSERT_EQUAL( static_cast<int>( ChangePinResult::WrongCurrent ), static_cast<int>( r ) ); 59 69 TEST_ASSERT_EQUAL( static_cast<int>( ChangePinStep::VerifyCurrent ), 60 70 static_cast<int>( l.step() ) ); 61 - TEST_ASSERT_EQUAL_UINT32( 1, mock::bruteforceFailures ); 71 + TEST_ASSERT_EQUAL_UINT32( 1, spy.bruteforceFailures ); 62 72 63 73 // Another wrong attempt: counter increments again. 64 74 r = l.verifyCurrent( false ); 65 75 if ( r == ChangePinResult::WrongCurrent ) 66 - mock::onWrongCurrent(); 67 - TEST_ASSERT_EQUAL_UINT32( 2, mock::bruteforceFailures ); 76 + onWrongCurrent(); 77 + TEST_ASSERT_EQUAL_UINT32( 2, spy.bruteforceFailures ); 68 78 } 69 79 70 - void test_submit_new_rejects_too_short() { 80 + static void test_submit_new_rejects_too_short() { 81 + // ARRANGE 71 82 ChangePinLogic l; 72 83 l.verifyCurrent( true ); 84 + 85 + // ACT 73 86 l.submitNew( "12", 2 ); 87 + 88 + // ASSERT 74 89 TEST_ASSERT_EQUAL( static_cast<int>( ChangePinStep::EnterNew ), static_cast<int>( l.step() ) ); 75 90 TEST_ASSERT_EQUAL( 0, l.newPinLen() ); 76 91 } 77 92 78 - void test_submit_new_rejects_too_long() { 93 + static void test_submit_new_rejects_too_long() { 94 + // ARRANGE 79 95 ChangePinLogic l; 80 96 l.verifyCurrent( true ); 97 + 98 + // ACT 81 99 l.submitNew( "123456789", 9 ); 100 + 101 + // ASSERT 82 102 TEST_ASSERT_EQUAL( static_cast<int>( ChangePinStep::EnterNew ), static_cast<int>( l.step() ) ); 83 103 } 84 104 85 - void test_submit_new_accepts_valid_length_advances_to_confirm() { 105 + static void test_submit_new_accepts_valid_length_advances_to_confirm() { 106 + // ARRANGE 86 107 ChangePinLogic l; 87 108 l.verifyCurrent( true ); 109 + 110 + // ACT 88 111 l.submitNew( "1234", 4 ); 112 + 113 + // ASSERT 89 114 TEST_ASSERT_EQUAL( static_cast<int>( ChangePinStep::ConfirmNew ), 90 115 static_cast<int>( l.step() ) ); 91 116 TEST_ASSERT_EQUAL( 4, l.newPinLen() ); 92 117 } 93 118 94 - void test_confirm_match_moves_to_re_encrypt() { 119 + static void test_confirm_match_moves_to_re_encrypt() { 120 + // ARRANGE 95 121 ChangePinLogic l; 96 122 l.verifyCurrent( true ); 97 123 l.submitNew( "4242", 4 ); 124 + 125 + // ACT 98 126 auto r = l.submitConfirm( "4242", 4 ); 127 + 128 + // ASSERT 99 129 TEST_ASSERT_EQUAL( static_cast<int>( ChangePinResult::CONTINUE ), static_cast<int>( r ) ); 100 130 TEST_ASSERT_EQUAL( static_cast<int>( ChangePinStep::ReEncrypt ), static_cast<int>( l.step() ) ); 101 131 // newPin() still available for caller's re-encryption. 102 132 TEST_ASSERT_EQUAL_STRING( "4242", l.newPin() ); 103 133 } 104 134 105 - void test_confirm_mismatch_returns_to_enter_new_and_wipes() { 135 + static void test_confirm_mismatch_returns_to_enter_new_and_wipes() { 136 + // ARRANGE 106 137 ChangePinLogic l; 107 138 l.verifyCurrent( true ); 108 139 l.submitNew( "1111", 4 ); 140 + 141 + // ACT 109 142 auto r = l.submitConfirm( "2222", 4 ); 143 + 144 + // ASSERT 110 145 TEST_ASSERT_EQUAL( static_cast<int>( ChangePinResult::MISMATCH ), static_cast<int>( r ) ); 111 146 TEST_ASSERT_EQUAL( static_cast<int>( ChangePinStep::EnterNew ), static_cast<int>( l.step() ) ); 112 147 TEST_ASSERT_EQUAL( 0, l.newPinLen() ); 113 148 } 114 149 115 - void test_confirm_length_mismatch_treated_as_mismatch() { 150 + static void test_confirm_length_mismatch_treated_as_mismatch() { 151 + // ARRANGE 116 152 ChangePinLogic l; 117 153 l.verifyCurrent( true ); 118 154 l.submitNew( "1234", 4 ); 155 + 156 + // ACT 119 157 auto r = l.submitConfirm( "12345", 5 ); 158 + 159 + // ASSERT 120 160 TEST_ASSERT_EQUAL( static_cast<int>( ChangePinResult::MISMATCH ), static_cast<int>( r ) ); 121 161 TEST_ASSERT_EQUAL( static_cast<int>( ChangePinStep::EnterNew ), static_cast<int>( l.step() ) ); 122 162 } 123 163 124 - void test_finish_reencrypt_ok_moves_to_done_and_wipes_new() { 164 + static void test_finish_reencrypt_ok_moves_to_done_and_wipes_new() { 165 + // ARRANGE 125 166 ChangePinLogic l; 126 167 l.verifyCurrent( true ); 127 168 l.submitNew( "7777", 4 ); 128 169 l.submitConfirm( "7777", 4 ); 170 + 171 + // ACT 129 172 auto r = l.finishReencrypt( true ); 173 + 174 + // ASSERT 130 175 TEST_ASSERT_EQUAL( static_cast<int>( ChangePinResult::CONTINUE ), static_cast<int>( r ) ); 131 176 TEST_ASSERT_EQUAL( static_cast<int>( ChangePinStep::DONE ), static_cast<int>( l.step() ) ); 132 177 TEST_ASSERT_EQUAL( 0, l.newPinLen() ); 133 178 } 134 179 135 - void test_finish_reencrypt_failure_resets_wizard() { 180 + static void test_finish_reencrypt_failure_resets_wizard() { 181 + // ARRANGE 136 182 ChangePinLogic l; 137 183 l.verifyCurrent( true ); 138 184 l.submitNew( "9999", 4 ); 139 185 l.submitConfirm( "9999", 4 ); 186 + 187 + // ACT 140 188 auto r = l.finishReencrypt( false ); 189 + 190 + // ASSERT 141 191 TEST_ASSERT_EQUAL( static_cast<int>( ChangePinResult::FAILED ), static_cast<int>( r ) ); 142 192 TEST_ASSERT_EQUAL( static_cast<int>( ChangePinStep::VerifyCurrent ), 143 193 static_cast<int>( l.step() ) ); 144 194 TEST_ASSERT_EQUAL( 0, l.newPinLen() ); 145 195 } 146 196 147 - void test_acknowledge_done_exits() { 197 + static void test_acknowledge_done_exits() { 198 + // ARRANGE 148 199 ChangePinLogic l; 149 200 l.verifyCurrent( true ); 150 201 l.submitNew( "0000", 4 ); 151 202 l.submitConfirm( "0000", 4 ); 152 203 l.finishReencrypt( true ); 204 + 205 + // ACT 153 206 auto r = l.acknowledge(); 207 + 208 + // ASSERT 154 209 TEST_ASSERT_EQUAL( static_cast<int>( ChangePinResult::FINISHED ), static_cast<int>( r ) ); 155 210 TEST_ASSERT_EQUAL( static_cast<int>( ChangePinStep::EXIT ), static_cast<int>( l.step() ) ); 156 211 } 157 212 158 - int main( int, char** ) { 213 + // --- State variables ------------------------------------------------------- 214 + 215 + static void onWrongCurrent() { 216 + ++spy.bruteforceFailures; 217 + } 218 + 219 + // --- main() ---------------------------------------------------------------- 220 + 221 + int main( int /*argc*/, char** /*argv*/ ) { 159 222 UNITY_BEGIN(); 160 223 RUN_TEST( test_initial_step_is_verify_current ); 161 224 RUN_TEST( test_verify_current_ok_advances_to_enter_new );
+96 -48
test/test_crypto_native/test_crypto_native.cpp test/crypto/test_vault_crypto/test_vault_crypto.cpp
··· 1 - // Kleidos — Native test: VaultCrypto (AES-256-CBC, PBKDF2, HMAC, wipe) 2 - // Runs on PC — no hardware needed. Uses system mbedTLS. 1 + // Kleidos — Native test: VaultCrypto (AES-256-CBC, PBKDF2, HMAC-SHA256, wipe, random) 2 + // Build: pio test -e native -f crypto/test_vault_crypto 3 + // Uses real mbedTLS — no mocking. 4 + 5 + // --- File header ----------------------------------------------------------- 6 + // Group 1: module under test 7 + #include "crypto/vault_crypto.h" 3 8 9 + // Group 3: stdlib + Unity 4 10 #include <cstdint> 5 11 #include <cstdlib> 6 12 #include <cstring> 7 13 8 14 #include <unity.h> 9 15 10 - // millis() stub 11 - static uint32_t fakeMillis = 0; 12 - extern "C" uint32_t millis() { 13 - return fakeMillis; 14 - } 16 + // --- Function declarations ------------------------------------------------- 15 17 16 - #include "crypto/vault_crypto.h" 18 + static void seedRng(); 17 19 18 - static void seedRng() { 19 - srand( 42 ); 20 - } 20 + // --- setUp / tearDown ------------------------------------------------------ 21 + void setUp() {} 22 + void tearDown() { /* intentionally empty */ } 21 23 22 - // --------------------------------------------------------------------------- 23 - void test_pbkdf2_deterministic() { 24 + // --- Tests ----------------------------------------------------------------- 25 + static void test_pbkdf2_deterministic() { 26 + // ARRANGE 24 27 const char* pin = "1234"; 25 28 uint8_t salt[kSaltSize] = { 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 26 29 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0x10 }; 27 30 uint8_t key1[kAesKeySize]; 28 31 uint8_t key2[kAesKeySize]; 29 32 33 + // ACT 30 34 TEST_ASSERT_TRUE( 31 35 VaultCrypto::deriveKey( pin, strlen( pin ), salt, kSaltSize, key1, kAesKeySize ) ); 32 36 TEST_ASSERT_TRUE( 33 37 VaultCrypto::deriveKey( pin, strlen( pin ), salt, kSaltSize, key2, kAesKeySize ) ); 38 + 39 + // ASSERT 34 40 TEST_ASSERT_EQUAL_MEMORY( key1, key2, kAesKeySize ); 35 41 36 42 VaultCrypto::secureWipe( key1, sizeof( key1 ) ); 37 43 VaultCrypto::secureWipe( key2, sizeof( key2 ) ); 38 44 } 39 45 40 - void test_pbkdf2_different_pins() { 46 + static void test_pbkdf2_different_pins() { 47 + // ARRANGE 41 48 uint8_t salt[kSaltSize] = { 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF, 0x11, 0x22, 42 49 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0x00 }; 43 50 uint8_t key1[kAesKeySize]; 44 51 uint8_t key2[kAesKeySize]; 45 52 53 + // ACT 46 54 TEST_ASSERT_TRUE( VaultCrypto::deriveKey( "1234", 4, salt, kSaltSize, key1, kAesKeySize ) ); 47 55 TEST_ASSERT_TRUE( VaultCrypto::deriveKey( "5678", 4, salt, kSaltSize, key2, kAesKeySize ) ); 56 + 57 + // ASSERT 48 58 TEST_ASSERT_FALSE( memcmp( key1, key2, kAesKeySize ) == 0 ); 49 59 50 60 VaultCrypto::secureWipe( key1, sizeof( key1 ) ); 51 61 VaultCrypto::secureWipe( key2, sizeof( key2 ) ); 52 62 } 53 63 54 - void test_pbkdf2_different_salts() { 64 + static void test_pbkdf2_different_salts() { 65 + // ARRANGE 55 66 uint8_t salt1[kSaltSize] = { 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 56 67 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0x10 }; 57 68 uint8_t salt2[kSaltSize] = { 0x10, 0x0F, 0x0E, 0x0D, 0x0C, 0x0B, 0x0A, 0x09, ··· 59 70 uint8_t key1[kAesKeySize]; 60 71 uint8_t key2[kAesKeySize]; 61 72 73 + // ACT 62 74 TEST_ASSERT_TRUE( VaultCrypto::deriveKey( "1234", 4, salt1, kSaltSize, key1, kAesKeySize ) ); 63 75 TEST_ASSERT_TRUE( VaultCrypto::deriveKey( "1234", 4, salt2, kSaltSize, key2, kAesKeySize ) ); 76 + 77 + // ASSERT 64 78 TEST_ASSERT_FALSE( memcmp( key1, key2, kAesKeySize ) == 0 ); 65 79 66 80 VaultCrypto::secureWipe( key1, sizeof( key1 ) ); 67 81 VaultCrypto::secureWipe( key2, sizeof( key2 ) ); 68 82 } 69 83 70 - void test_aes_roundtrip() { 84 + static void test_aes_roundtrip() { 85 + // ARRANGE 71 86 seedRng(); 72 87 const char* pin = "9876"; 73 88 uint8_t salt[kSaltSize]; 74 89 VaultCrypto::generateRandom( salt, kSaltSize ); 75 - 76 90 uint8_t key[kAesKeySize]; 77 91 TEST_ASSERT_TRUE( 78 92 VaultCrypto::deriveKey( pin, strlen( pin ), salt, kSaltSize, key, kAesKeySize ) ); 79 - 80 93 const char* message = "{\"name\":\"GitHub\",\"user\":\"john\",\"pass\":\"s3cret!\"}"; 81 94 size_t msgLen = strlen( message ); 82 - 83 - uint8_t iv[kIvSize]; 84 - uint8_t cipher[512]; 85 - size_t cipherLen = 0; 86 - 95 + uint8_t iv[kIvSize]; 96 + uint8_t cipher[512]; 97 + size_t cipherLen = 0; 87 98 TEST_ASSERT_TRUE( VaultCrypto::encrypt( key, reinterpret_cast<const uint8_t*>( message ), 88 99 msgLen, iv, cipher, &cipherLen ) ); 89 - 90 100 TEST_ASSERT_TRUE( cipherLen > 0 ); 91 101 TEST_ASSERT_TRUE( cipherLen % kAesBlockSize == 0 ); 92 102 TEST_ASSERT_FALSE( memcmp( cipher, message, msgLen ) == 0 ); 93 103 104 + // ACT 94 105 uint8_t decrypted[512]; 95 106 size_t decryptedLen = 0; 96 - 97 - TEST_ASSERT_TRUE( 98 - VaultCrypto::decrypt( key, iv, cipher, cipherLen, decrypted, &decryptedLen ) ); 107 + bool ok = VaultCrypto::decrypt( key, iv, cipher, cipherLen, decrypted, &decryptedLen ); 99 108 109 + // ASSERT 110 + TEST_ASSERT_TRUE( ok ); 100 111 TEST_ASSERT_EQUAL( msgLen, decryptedLen ); 101 112 TEST_ASSERT_EQUAL_MEMORY( message, decrypted, msgLen ); 102 113 VaultCrypto::secureWipe( key, sizeof( key ) ); 103 114 } 104 115 105 - void test_decrypt_wrong_key() { 116 + static void test_decrypt_wrong_key() { 117 + // ARRANGE 106 118 seedRng(); 107 119 uint8_t salt[kSaltSize]; 108 120 VaultCrypto::generateRandom( salt, kSaltSize ); 109 - 110 121 uint8_t goodKey[kAesKeySize]; 111 122 uint8_t badKey[kAesKeySize]; 112 - 113 123 TEST_ASSERT_TRUE( VaultCrypto::deriveKey( "1111", 4, salt, kSaltSize, goodKey, kAesKeySize ) ); 114 124 TEST_ASSERT_TRUE( VaultCrypto::deriveKey( "2222", 4, salt, kSaltSize, badKey, kAesKeySize ) ); 115 - 116 125 const char* msg = "secret data here"; 117 126 uint8_t iv[kIvSize]; 118 127 uint8_t cipher[64]; 119 128 size_t cipherLen = 0; 120 - 121 129 TEST_ASSERT_TRUE( VaultCrypto::encrypt( goodKey, reinterpret_cast<const uint8_t*>( msg ), 122 130 strlen( msg ), iv, cipher, &cipherLen ) ); 123 131 132 + // ACT 124 133 uint8_t plain[64]; 125 134 size_t plainLen = 0; 126 - TEST_ASSERT_FALSE( VaultCrypto::decrypt( badKey, iv, cipher, cipherLen, plain, &plainLen ) ); 135 + bool ok = VaultCrypto::decrypt( badKey, iv, cipher, cipherLen, plain, &plainLen ); 136 + 137 + // ASSERT 138 + TEST_ASSERT_FALSE( ok ); 127 139 128 140 VaultCrypto::secureWipe( goodKey, sizeof( goodKey ) ); 129 141 VaultCrypto::secureWipe( badKey, sizeof( badKey ) ); 130 142 } 131 143 132 - void test_pin_hash_verify() { 144 + static void test_pin_hash_verify() { 145 + // ARRANGE 133 146 uint8_t salt[kSaltSize] = { 0x55, 0x66, 0x77, 0x88, 0x99, 0xAA, 0xBB, 0xCC, 134 147 0xDD, 0xEE, 0xFF, 0x00, 0x11, 0x22, 0x33, 0x44 }; 135 148 uint8_t hash1[kPinHashSize]; 136 149 uint8_t hash2[kPinHashSize]; 137 150 uint8_t hash3[kPinHashSize]; 138 151 152 + // ACT 139 153 TEST_ASSERT_TRUE( VaultCrypto::hashPin( "1234", 4, salt, kSaltSize, hash1 ) ); 140 154 TEST_ASSERT_TRUE( VaultCrypto::hashPin( "1234", 4, salt, kSaltSize, hash2 ) ); 141 155 TEST_ASSERT_TRUE( VaultCrypto::hashPin( "4321", 4, salt, kSaltSize, hash3 ) ); 142 156 157 + // ASSERT 143 158 TEST_ASSERT_TRUE( VaultCrypto::verifyPinHash( hash1, hash2 ) ); 144 159 TEST_ASSERT_FALSE( VaultCrypto::verifyPinHash( hash1, hash3 ) ); 145 160 } 146 161 147 - void test_secure_wipe() { 162 + static void test_secure_wipe() { 163 + // ARRANGE 148 164 uint8_t buf[32]; 149 165 memset( buf, 0xAA, sizeof( buf ) ); 166 + 167 + // ACT 150 168 VaultCrypto::secureWipe( buf, sizeof( buf ) ); 169 + 170 + // ASSERT 151 171 uint8_t zero[32] = { 0 }; 152 172 TEST_ASSERT_EQUAL_MEMORY( zero, buf, sizeof( buf ) ); 153 173 } 154 174 155 - void test_constant_time_equal() { 175 + static void test_constant_time_equal() { 176 + // ARRANGE 156 177 uint8_t a[16] = { 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 157 178 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0x10 }; 158 179 uint8_t b[16]; ··· 161 182 memcpy( c, a, 16 ); 162 183 c[15] = 0xFF; 163 184 185 + // ASSERT 164 186 TEST_ASSERT_TRUE( VaultCrypto::constantTimeEqual( a, b, 16 ) ); 165 187 TEST_ASSERT_FALSE( VaultCrypto::constantTimeEqual( a, c, 16 ) ); 166 188 } 167 189 168 - void test_constant_time_equal_zero_len() { 190 + static void test_constant_time_equal_zero_len() { 191 + // ARRANGE 169 192 uint8_t a[1] = { 0xAA }; 170 193 uint8_t b[1] = { 0xBB }; 194 + 195 + // ASSERT (zero-length comparison is always equal) 171 196 TEST_ASSERT_TRUE( VaultCrypto::constantTimeEqual( a, b, 0 ) ); 172 197 } 173 198 174 - void test_padding_edge_cases() { 199 + static void test_padding_edge_cases() { 200 + // ARRANGE 175 201 seedRng(); 176 202 uint8_t salt[kSaltSize] = { 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF, 0x01, 0x02, 177 203 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A }; 178 204 uint8_t key[kAesKeySize]; 179 205 TEST_ASSERT_TRUE( VaultCrypto::deriveKey( "test", 4, salt, kSaltSize, key, kAesKeySize ) ); 180 - 181 206 size_t testSizes[] = { 1, 15, 16, 17, 31, 32, 33, 48, 100 }; 182 207 208 + // ACT + ASSERT 183 209 for ( size_t s : testSizes ) { 184 210 uint8_t plaintext[128]; 185 211 memset( plaintext, 'A' + ( s % 26 ), s ); ··· 204 230 VaultCrypto::secureWipe( key, sizeof( key ) ); 205 231 } 206 232 207 - void test_decrypt_invalid_length() { 233 + static void test_decrypt_invalid_length() { 234 + // ARRANGE 208 235 uint8_t key[kAesKeySize] = { 0 }; 209 236 uint8_t iv[kIvSize] = { 0 }; 210 237 uint8_t cipher[17] = { 0 }; 211 238 uint8_t plain[17]; 212 239 size_t plainLen = 0; 213 240 241 + // ASSERT (length 0 and non-block-aligned length must both fail) 214 242 TEST_ASSERT_FALSE( VaultCrypto::decrypt( key, iv, cipher, 0, plain, &plainLen ) ); 215 243 TEST_ASSERT_FALSE( VaultCrypto::decrypt( key, iv, cipher, 17, plain, &plainLen ) ); 216 244 } 217 245 218 - void test_hmac_deterministic() { 246 + static void test_hmac_deterministic() { 247 + // ARRANGE 219 248 uint8_t key[32]; 220 249 memset( key, 0x0B, sizeof( key ) ); 221 - 222 250 const char* data = "test data for hmac"; 223 251 uint8_t mac1[kHmacSize]; 224 252 uint8_t mac2[kHmacSize]; 225 253 254 + // ACT 226 255 TEST_ASSERT_TRUE( VaultCrypto::hmacSha256( 227 256 key, sizeof( key ), reinterpret_cast<const uint8_t*>( data ), strlen( data ), mac1 ) ); 228 257 TEST_ASSERT_TRUE( VaultCrypto::hmacSha256( 229 258 key, sizeof( key ), reinterpret_cast<const uint8_t*>( data ), strlen( data ), mac2 ) ); 259 + 260 + // ASSERT 230 261 TEST_ASSERT_EQUAL_MEMORY( mac1, mac2, kHmacSize ); 231 262 } 232 263 233 - void test_hmac_different_data() { 264 + static void test_hmac_different_data() { 265 + // ARRANGE 234 266 uint8_t key[32]; 235 267 memset( key, 0x0B, sizeof( key ) ); 236 268 uint8_t mac1[kHmacSize]; 237 269 uint8_t mac2[kHmacSize]; 238 270 271 + // ACT 239 272 TEST_ASSERT_TRUE( VaultCrypto::hmacSha256( 240 273 key, sizeof( key ), reinterpret_cast<const uint8_t*>( "data1" ), 5, mac1 ) ); 241 274 TEST_ASSERT_TRUE( VaultCrypto::hmacSha256( 242 275 key, sizeof( key ), reinterpret_cast<const uint8_t*>( "data2" ), 5, mac2 ) ); 276 + 277 + // ASSERT 243 278 TEST_ASSERT_FALSE( memcmp( mac1, mac2, kHmacSize ) == 0 ); 244 279 } 245 280 246 - void test_hmac_different_key() { 281 + static void test_hmac_different_key() { 282 + // ARRANGE 247 283 uint8_t key1[32], key2[32]; 248 284 memset( key1, 0x0A, sizeof( key1 ) ); 249 285 memset( key2, 0x0B, sizeof( key2 ) ); 250 - 251 286 const char* data = "same data"; 252 287 uint8_t mac1[kHmacSize]; 253 288 uint8_t mac2[kHmacSize]; 254 289 290 + // ACT 255 291 TEST_ASSERT_TRUE( VaultCrypto::hmacSha256( 256 292 key1, sizeof( key1 ), reinterpret_cast<const uint8_t*>( data ), strlen( data ), mac1 ) ); 257 293 TEST_ASSERT_TRUE( VaultCrypto::hmacSha256( 258 294 key2, sizeof( key2 ), reinterpret_cast<const uint8_t*>( data ), strlen( data ), mac2 ) ); 295 + 296 + // ASSERT 259 297 TEST_ASSERT_FALSE( memcmp( mac1, mac2, kHmacSize ) == 0 ); 260 298 } 261 299 262 - void test_generate_random_not_zero() { 300 + static void test_generate_random_not_zero() { 301 + // ARRANGE 263 302 seedRng(); 264 303 uint8_t buf[32] = { 0 }; 304 + 305 + // ACT 265 306 VaultCrypto::generateRandom( buf, sizeof( buf ) ); 307 + 308 + // ASSERT 266 309 bool allZero = true; 267 310 for ( size_t i = 0; i < 32; i++ ) { 268 311 if ( buf[i] != 0 ) { ··· 273 316 TEST_ASSERT_FALSE( allZero ); 274 317 } 275 318 276 - // --------------------------------------------------------------------------- 277 - int main( int argc, char** argv ) { 319 + // --- State variables ------------------------------------------------------- 320 + static void seedRng() { 321 + srand( 42 ); 322 + } 323 + 324 + // --- main() ---------------------------------------------------------------- 325 + int main( int /*argc*/, char** /*argv*/ ) { 278 326 UNITY_BEGIN(); 279 327 RUN_TEST( test_secure_wipe ); 280 328 RUN_TEST( test_constant_time_equal );
+73 -22
test/test_display_graphics/test_display_graphics.cpp test/hal/test_display_graphics/test_display_graphics.cpp
··· 1 + // Kleidos — Native test: Display Graphics (pixel rendering, catalog validation) 2 + // Build: pio test -e native -f hal/test_display_graphics 3 + 4 + // --- File header ----------------------------------------------------------- 5 + // Group 1: module(s) under test 1 6 #include "hal/common/display_text.h" 2 7 #include "hal/display/display_catalog.h" 3 8 #include "hal/display/graphics.h" 4 9 #include "hal/display/text_renderer.h" 5 10 11 + // Group 3: stdlib + Unity 6 12 #include <array> 7 13 #include <cstdint> 8 14 9 15 #include <unity.h> 10 16 11 - namespace { 17 + // --- Function declarations ------------------------------------------------- 18 + // (no file-scope constants) 19 + 20 + // --- Function declarations ------------------------------------------------- 21 + // (no file-scope state variables) 12 22 23 + // --- Fake function definitions --------------------------------------------- 24 + // Fake: FakeSurface — in-memory pixel surface for renderer tests 13 25 class FakeSurface final : public display::graphics::Surface { 14 26 public: 15 27 int32_t width() const override { return kWidth; } ··· 42 54 std::array<uint16_t, kWidth * kHeight> pixels_ = {}; 43 55 }; 44 56 45 - void test_fold_utf8_latin1_maps_common_i18n_text() { 57 + // --- setUp / tearDown ------------------------------------------------------ 58 + void setUp( void ) {} 59 + void tearDown( void ) {} 60 + 61 + // --- Tests ----------------------------------------------------------------- 62 + static void test_fold_utf8_latin1_maps_common_i18n_text() { 63 + // ARRANGE 46 64 char output[display::text::kFoldBufferSize] = {}; 47 65 66 + // ACT 48 67 display::text::foldUtf8Latin1( "Español — Étape…", output, sizeof( output ) ); 49 68 69 + // ASSERT 50 70 TEST_ASSERT_EQUAL_STRING( "Espanol - Etape..", output ); 51 71 } 52 72 53 - void test_text_renderer_draws_centered_text_pixels() { 73 + static void test_text_renderer_draws_centered_text_pixels() { 74 + // ARRANGE 54 75 FakeSurface surface; 55 76 display::text_renderer::TextStyle style = {}; 56 77 style.font = display::Font::BODY; 57 78 style.foreground = 0x1234; 58 79 style.background = 0x0000; 59 - style.datum = middle_center; 80 + style.datum = kMiddleCenter; 60 81 82 + // ACT 61 83 display::text_renderer::drawString( surface, "OK", 32, 16, style ); 62 84 85 + // ASSERT 63 86 TEST_ASSERT_GREATER_THAN( 0U, surface.count( 0x1234 ) ); 64 87 TEST_ASSERT_EQUAL_UINT16( 0x0000, surface.pixel( 0, 0 ) ); 65 88 } 66 89 67 - void test_graphics_draw_qr_placeholder_has_foreground_and_background() { 90 + static void test_graphics_draw_qr_placeholder_has_foreground_and_background() { 91 + // ARRANGE 68 92 FakeSurface surface; 69 93 94 + // ACT 70 95 display::graphics::drawQrPlaceholder( surface, 0, 0, 21, 0xFFFF, 0x0001 ); 71 96 97 + // ASSERT 72 98 TEST_ASSERT_GREATER_THAN( 0U, surface.count( 0xFFFF ) ); 73 99 TEST_ASSERT_GREATER_THAN( 0U, surface.count( 0x0001 ) ); 74 100 } 75 101 76 - void test_display_catalog_names_known_interfaces() { 77 - TEST_ASSERT_EQUAL_STRING( "ST7789", display::catalog::panelControllerName( 78 - display::catalog::PanelController::St7789 ) ); 79 - TEST_ASSERT_EQUAL_STRING( "ILI9342", display::catalog::panelControllerName( 80 - display::catalog::PanelController::Ili9342 ) ); 81 - TEST_ASSERT_EQUAL_STRING( "SPI+DC", display::catalog::busInterfaceName( 82 - display::catalog::BusInterface::SpiDataCommand ) ); 102 + static void test_display_catalog_names_known_interfaces() { 103 + // ARRANGE: (none — catalog returns compile-time strings) 104 + 105 + // ACT 106 + const char* st7789 = 107 + display::catalog::panelControllerName( display::catalog::PanelController::St7789 ); 108 + const char* ili9342 = 109 + display::catalog::panelControllerName( display::catalog::PanelController::Ili9342 ); 110 + const char* spiDc = 111 + display::catalog::busInterfaceName( display::catalog::BusInterface::SpiDataCommand ); 112 + 113 + // ASSERT 114 + TEST_ASSERT_EQUAL_STRING( "ST7789", st7789 ); 115 + TEST_ASSERT_EQUAL_STRING( "ILI9342", ili9342 ); 116 + TEST_ASSERT_EQUAL_STRING( "SPI+DC", spiDc ); 83 117 } 84 118 85 - void test_display_catalog_rejects_incomplete_spi_config() { 119 + static void test_display_catalog_rejects_incomplete_spi_config() { 120 + // ARRANGE 86 121 display::catalog::PanelSpec panel = {}; 87 122 panel.controller = display::catalog::PanelController::St7789; 88 123 panel.interface = display::catalog::BusInterface::SpiDataCommand; ··· 98 133 bus.pins.cs = -1; 99 134 bus.pins.dc = 4; 100 135 101 - TEST_ASSERT_FALSE( display::catalog::isSupported( panel, bus ) ); 136 + // ACT 137 + const bool supported = display::catalog::isSupported( panel, bus ); 138 + 139 + // ASSERT 140 + TEST_ASSERT_FALSE( supported ); 102 141 } 103 142 104 - void test_display_catalog_accepts_spi_panel_config() { 143 + static void test_display_catalog_accepts_spi_panel_config() { 144 + // ARRANGE 105 145 display::catalog::PanelSpec panel = {}; 106 146 panel.controller = display::catalog::PanelController::Ili9342; 107 147 panel.interface = display::catalog::BusInterface::SpiDataCommand; ··· 117 157 bus.pins.cs = 14; 118 158 bus.pins.dc = 27; 119 159 120 - TEST_ASSERT_TRUE( display::catalog::isSupported( panel, bus ) ); 160 + // ACT 161 + const bool supported = display::catalog::isSupported( panel, bus ); 162 + 163 + // ASSERT 164 + TEST_ASSERT_TRUE( supported ); 121 165 } 122 166 123 - void test_display_catalog_accepts_gpio_backlight_spi_panel_config() { 167 + static void test_display_catalog_accepts_gpio_backlight_spi_panel_config() { 168 + // ARRANGE 124 169 display::catalog::PanelSpec panel = {}; 125 170 panel.controller = display::catalog::PanelController::St7789; 126 171 panel.interface = display::catalog::BusInterface::SpiDataCommand; ··· 138 183 bus.pins.backlight = 38; 139 184 bus.backlightPwm = false; 140 185 141 - TEST_ASSERT_TRUE( display::catalog::isSupported( panel, bus ) ); 186 + // ACT 187 + const bool supported = display::catalog::isSupported( panel, bus ); 188 + 189 + // ASSERT 190 + TEST_ASSERT_TRUE( supported ); 142 191 } 143 192 144 - } // namespace 193 + // --- Fake function definitions --------------------------------------------- 194 + // (no free-function fakes) 145 195 146 - int main( int argc, char** argv ) { 147 - (void)argc; 148 - (void)argv; 196 + // --- Helper definitions ---------------------------------------------------- 197 + // (no helper / arrange definitions) 149 198 199 + // --- main() ---------------------------------------------------------------- 200 + int main( int /*argc*/, char** /*argv*/ ) { 150 201 UNITY_BEGIN(); 151 202 RUN_TEST( test_fold_utf8_latin1_maps_common_i18n_text ); 152 203 RUN_TEST( test_text_renderer_draws_centered_text_pixels );
-330
test/test_holdbutton/test_holdbutton.cpp
··· 1 - // Kleidos — Native unit tests for HoldButton state machine 2 - // Run: pio test -e native --filter test_holdbutton 3 - 4 - #include "platform/clock.h" 5 - 6 - #include <cstdint> 7 - 8 - #include <unity.h> 9 - 10 - // --------------------------------------------------------------------------- 11 - // Controllable fake clock for native tests 12 - // --------------------------------------------------------------------------- 13 - static uint32_t fakeMillis = 0; 14 - 15 - static uint32_t fakeMillisProvider() { 16 - return fakeMillis; 17 - } 18 - 19 - static void advanceTime( uint32_t ms ) { 20 - fakeMillis += ms; 21 - } 22 - 23 - static void resetClock() { 24 - fakeMillis = 0; 25 - kleidos::platform::clock::setTestMillisProvider( fakeMillisProvider ); 26 - } 27 - 28 - // --------------------------------------------------------------------------- 29 - // Include units under test 30 - // --------------------------------------------------------------------------- 31 - #include "ui/input/hold_button.h" 32 - #include "ui/input/virtual_button_input.h" 33 - 34 - // --------------------------------------------------------------------------- 35 - // Test: short tap detection 36 - // --------------------------------------------------------------------------- 37 - void test_tap_detected() { 38 - resetClock(); 39 - VirtualButtonInput btn; 40 - HoldButton hold; 41 - 42 - // Press 43 - btn.setState( true ); 44 - hold.update( btn ); 45 - TEST_ASSERT_FALSE( hold.tapped() ); 46 - 47 - // Advance 50ms, release 48 - advanceTime( 50 ); 49 - btn.setState( false ); 50 - hold.update( btn ); 51 - TEST_ASSERT_TRUE( hold.tapped() ); 52 - } 53 - 54 - // --------------------------------------------------------------------------- 55 - // Test: tapped() is true only for one frame 56 - // --------------------------------------------------------------------------- 57 - void test_tap_clears_after_read() { 58 - resetClock(); 59 - VirtualButtonInput btn; 60 - HoldButton hold; 61 - 62 - btn.setState( true ); 63 - hold.update( btn ); 64 - advanceTime( 50 ); 65 - btn.setState( false ); 66 - hold.update( btn ); 67 - 68 - TEST_ASSERT_TRUE( hold.tapped() ); 69 - 70 - // Next frame — should be cleared 71 - hold.update( btn ); 72 - TEST_ASSERT_FALSE( hold.tapped() ); 73 - } 74 - 75 - // --------------------------------------------------------------------------- 76 - // Test: hold fires at threshold 77 - // --------------------------------------------------------------------------- 78 - void test_hold_fires_at_threshold() { 79 - resetClock(); 80 - VirtualButtonInput btn; 81 - HoldButton hold; 82 - 83 - btn.setState( true ); 84 - hold.update( btn ); 85 - TEST_ASSERT_FALSE( hold.checkHold( 800 ) ); 86 - 87 - // Advance 500ms — still holding, not yet at threshold 88 - advanceTime( 500 ); 89 - hold.update( btn ); 90 - TEST_ASSERT_FALSE( hold.checkHold( 800 ) ); 91 - TEST_ASSERT_FALSE( hold.tapped() ); 92 - 93 - // Advance to 800ms — should fire 94 - advanceTime( 300 ); 95 - hold.update( btn ); 96 - TEST_ASSERT_TRUE( hold.checkHold( 800 ) ); 97 - 98 - // Should NOT fire again 99 - advanceTime( 100 ); 100 - hold.update( btn ); 101 - TEST_ASSERT_FALSE( hold.checkHold( 800 ) ); 102 - } 103 - 104 - // --------------------------------------------------------------------------- 105 - // Test: hold does not generate tap on release 106 - // --------------------------------------------------------------------------- 107 - void test_hold_suppresses_tap() { 108 - resetClock(); 109 - VirtualButtonInput btn; 110 - HoldButton hold; 111 - 112 - btn.setState( true ); 113 - hold.update( btn ); 114 - 115 - advanceTime( 1000 ); 116 - hold.update( btn ); 117 - hold.checkHold( 800 ); // fires hold 118 - 119 - // Release after hold fired 120 - advanceTime( 100 ); 121 - btn.setState( false ); 122 - hold.update( btn ); 123 - TEST_ASSERT_FALSE( hold.tapped() ); // must not be a tap 124 - } 125 - 126 - // --------------------------------------------------------------------------- 127 - // Test: tap when released before threshold 128 - // --------------------------------------------------------------------------- 129 - void test_release_before_threshold_is_tap() { 130 - resetClock(); 131 - VirtualButtonInput btn; 132 - HoldButton hold; 133 - 134 - btn.setState( true ); 135 - hold.update( btn ); 136 - 137 - advanceTime( 400 ); 138 - hold.update( btn ); 139 - TEST_ASSERT_FALSE( hold.checkHold( 800 ) ); // not at threshold yet 140 - 141 - // Release before threshold 142 - btn.setState( false ); 143 - hold.update( btn ); 144 - TEST_ASSERT_TRUE( hold.tapped() ); 145 - } 146 - 147 - // --------------------------------------------------------------------------- 148 - // Test: reset clears all state 149 - // --------------------------------------------------------------------------- 150 - void test_reset_clears_state() { 151 - resetClock(); 152 - VirtualButtonInput btn; 153 - HoldButton hold; 154 - 155 - btn.setState( true ); 156 - hold.update( btn ); 157 - advanceTime( 50 ); 158 - btn.setState( false ); 159 - hold.update( btn ); 160 - 161 - // tap is pending 162 - hold.reset(); 163 - TEST_ASSERT_FALSE( hold.tapped() ); 164 - } 165 - 166 - // --------------------------------------------------------------------------- 167 - // Test: elapsed() reports correct timing 168 - // --------------------------------------------------------------------------- 169 - void test_elapsed_tracking() { 170 - resetClock(); 171 - VirtualButtonInput btn; 172 - HoldButton hold; 173 - 174 - TEST_ASSERT_EQUAL_UINT32( 0, hold.elapsed() ); 175 - 176 - btn.setState( true ); 177 - hold.update( btn ); 178 - advanceTime( 350 ); 179 - hold.update( btn ); 180 - TEST_ASSERT_EQUAL_UINT32( 350, hold.elapsed() ); 181 - 182 - // Release 183 - btn.setState( false ); 184 - hold.update( btn ); 185 - TEST_ASSERT_EQUAL_UINT32( 0, hold.elapsed() ); 186 - } 187 - 188 - // --------------------------------------------------------------------------- 189 - // Test: VirtualButtonInput wasPressed/wasReleased are one-shot 190 - // --------------------------------------------------------------------------- 191 - void test_virtual_button_oneshot() { 192 - VirtualButtonInput btn; 193 - 194 - btn.setState( true ); 195 - TEST_ASSERT_TRUE( btn.wasPressed() ); 196 - TEST_ASSERT_FALSE( btn.wasPressed() ); // consumed 197 - 198 - btn.setState( false ); 199 - TEST_ASSERT_TRUE( btn.wasReleased() ); 200 - TEST_ASSERT_FALSE( btn.wasReleased() ); // consumed 201 - } 202 - 203 - // --------------------------------------------------------------------------- 204 - // Test: VirtualButtonInput no transition when same state 205 - // --------------------------------------------------------------------------- 206 - void test_virtual_button_no_spurious_events() { 207 - VirtualButtonInput btn; 208 - 209 - btn.setState( false ); 210 - TEST_ASSERT_FALSE( btn.wasPressed() ); 211 - TEST_ASSERT_FALSE( btn.wasReleased() ); 212 - 213 - btn.setState( true ); 214 - btn.wasPressed(); // consume 215 - btn.setState( true ); // same state 216 - TEST_ASSERT_FALSE( btn.wasPressed() ); 217 - TEST_ASSERT_FALSE( btn.wasReleased() ); 218 - } 219 - 220 - // --------------------------------------------------------------------------- 221 - // Test: multiple press-release cycles 222 - // --------------------------------------------------------------------------- 223 - void test_multiple_cycles() { 224 - resetClock(); 225 - VirtualButtonInput btn; 226 - HoldButton hold; 227 - 228 - // Cycle 1: tap 229 - btn.setState( true ); 230 - hold.update( btn ); 231 - advanceTime( 30 ); 232 - btn.setState( false ); 233 - hold.update( btn ); 234 - TEST_ASSERT_TRUE( hold.tapped() ); 235 - 236 - // Cycle 2: hold 237 - advanceTime( 100 ); 238 - btn.setState( true ); 239 - hold.update( btn ); 240 - advanceTime( 900 ); 241 - hold.update( btn ); 242 - TEST_ASSERT_TRUE( hold.checkHold( 800 ) ); 243 - 244 - // Cycle 3: tap again (proves state resets between cycles) 245 - advanceTime( 100 ); 246 - btn.setState( false ); 247 - hold.update( btn ); 248 - advanceTime( 100 ); 249 - btn.setState( true ); 250 - hold.update( btn ); 251 - advanceTime( 50 ); 252 - btn.setState( false ); 253 - hold.update( btn ); 254 - TEST_ASSERT_TRUE( hold.tapped() ); 255 - } 256 - 257 - // --------------------------------------------------------------------------- 258 - // Test: millis() rollover (uint32_t wrap around) 259 - // --------------------------------------------------------------------------- 260 - void test_millis_rollover() { 261 - // Set time near uint32_t max 262 - fakeMillis = UINT32_MAX - 100; 263 - VirtualButtonInput btn; 264 - HoldButton hold; 265 - 266 - btn.setState( true ); 267 - hold.update( btn ); 268 - 269 - // Wrap around 270 - fakeMillis = 50; // 150ms elapsed due to wrap 271 - hold.update( btn ); 272 - TEST_ASSERT_TRUE( hold.elapsed() > 100 ); 273 - TEST_ASSERT_TRUE( hold.checkHold( 150 ) ); 274 - } 275 - 276 - // --------------------------------------------------------------------------- 277 - // Test: zero-duration press is still a tap 278 - // --------------------------------------------------------------------------- 279 - void test_instant_press_release() { 280 - resetClock(); 281 - VirtualButtonInput btn; 282 - HoldButton hold; 283 - 284 - btn.setState( true ); 285 - hold.update( btn ); 286 - // Release same ms 287 - btn.setState( false ); 288 - hold.update( btn ); 289 - TEST_ASSERT_TRUE( hold.tapped() ); 290 - } 291 - 292 - // --------------------------------------------------------------------------- 293 - // Test: elapsed is non-zero while pressed 294 - // --------------------------------------------------------------------------- 295 - void test_pressed_shows_elapsed() { 296 - resetClock(); 297 - VirtualButtonInput btn; 298 - HoldButton hold; 299 - 300 - btn.setState( true ); 301 - hold.update( btn ); 302 - advanceTime( 100 ); 303 - hold.update( btn ); 304 - TEST_ASSERT_TRUE( hold.elapsed() > 0 ); 305 - 306 - btn.setState( false ); 307 - hold.update( btn ); 308 - TEST_ASSERT_EQUAL_UINT32( 0, hold.elapsed() ); 309 - } 310 - 311 - // --------------------------------------------------------------------------- 312 - int main() { 313 - UNITY_BEGIN(); 314 - 315 - RUN_TEST( test_tap_detected ); 316 - RUN_TEST( test_tap_clears_after_read ); 317 - RUN_TEST( test_hold_fires_at_threshold ); 318 - RUN_TEST( test_hold_suppresses_tap ); 319 - RUN_TEST( test_release_before_threshold_is_tap ); 320 - RUN_TEST( test_reset_clears_state ); 321 - RUN_TEST( test_elapsed_tracking ); 322 - RUN_TEST( test_virtual_button_oneshot ); 323 - RUN_TEST( test_virtual_button_no_spurious_events ); 324 - RUN_TEST( test_multiple_cycles ); 325 - RUN_TEST( test_millis_rollover ); 326 - RUN_TEST( test_instant_press_release ); 327 - RUN_TEST( test_pressed_shows_elapsed ); 328 - 329 - return UNITY_END(); 330 - }
-147
test/test_keyboard_input/test_keyboard_input.cpp
··· 1 - // Kleidos — Unit tests for KeyEventMapper (key→event mapping). 2 - // Runs on the host via the native env; no Arduino / M5 dependencies. 3 - 4 - #include "hal/common/keyboard_input.h" 5 - #include "ui/keyboard/key_event_mapper.h" 6 - 7 - #include <cstdint> 8 - #include <cstring> 9 - 10 - #include <unity.h> 11 - 12 - // millis() stub — required because HoldButton.cpp is linked in native env. 13 - extern "C" uint32_t millis() { 14 - return 0; 15 - } 16 - 17 - using ui::KeyFrame; 18 - using ui::mapKeyFrame; 19 - 20 - void setUp( void ) {} 21 - void tearDown( void ) {} 22 - 23 - // Empty frame produces no event. 24 - void test_empty_frame_emits_nothing() { 25 - KeyFrame f{}; 26 - TEST_ASSERT_EQUAL( key_code::kNone, mapKeyFrame( f, key_code::kNone ) ); 27 - } 28 - 29 - // Enter takes priority over everything else. 30 - void test_enter_wins_over_word() { 31 - KeyFrame f{}; 32 - f.enter = true; 33 - const char w[] = "a"; 34 - f.word = w; 35 - f.wordLen = 1; 36 - TEST_ASSERT_EQUAL( key_code::kEnter, mapKeyFrame( f, key_code::kNone ) ); 37 - } 38 - 39 - // Backspace takes priority over Fn/Space/word but not over Enter. 40 - void test_backspace_wins_over_word_and_space() { 41 - KeyFrame f{}; 42 - f.backspace = true; 43 - f.space = true; 44 - const char w[] = "x"; 45 - f.word = w; 46 - f.wordLen = 1; 47 - TEST_ASSERT_EQUAL( key_code::kBackspace, mapKeyFrame( f, key_code::kNone ) ); 48 - } 49 - 50 - // Fn alone (no other key) becomes Escape — used for "back" / cancel. 51 - void test_fn_alone_becomes_escape() { 52 - KeyFrame f{}; 53 - f.fnAlone = true; 54 - TEST_ASSERT_EQUAL( key_code::kEscape, mapKeyFrame( f, key_code::kNone ) ); 55 - } 56 - 57 - // Fn alongside Enter does NOT trigger Escape (Enter wins). 58 - void test_fn_with_enter_does_not_emit_escape() { 59 - KeyFrame f{}; 60 - f.enter = true; 61 - f.fnAlone = false; // would be set to false by the driver in this case 62 - TEST_ASSERT_EQUAL( key_code::kEnter, mapKeyFrame( f, key_code::kNone ) ); 63 - } 64 - 65 - // Arrow keys take priority over Fn-alone Escape and printable words. 66 - void test_arrow_wins_over_fn_and_word() { 67 - KeyFrame f{}; 68 - f.down = true; 69 - f.fnAlone = true; 70 - const char w[] = "j"; 71 - f.word = w; 72 - f.wordLen = 1; 73 - TEST_ASSERT_EQUAL( key_code::kDown, mapKeyFrame( f, key_code::kNone ) ); 74 - } 75 - 76 - // Space emits the space character when no higher-priority key is set. 77 - void test_space_emits_space_char() { 78 - KeyFrame f{}; 79 - f.space = true; 80 - TEST_ASSERT_EQUAL( ' ', mapKeyFrame( f, key_code::kNone ) ); 81 - } 82 - 83 - // Plain printable character is forwarded as-is. 84 - void test_word_emits_first_char() { 85 - KeyFrame f{}; 86 - const char w[] = "k"; 87 - f.word = w; 88 - f.wordLen = 1; 89 - TEST_ASSERT_EQUAL( 'k', mapKeyFrame( f, key_code::kNone ) ); 90 - } 91 - 92 - // A digit character (PIN entry use case) round-trips correctly. 93 - void test_digit_emits_digit() { 94 - KeyFrame f{}; 95 - const char w[] = "7"; 96 - f.word = w; 97 - f.wordLen = 1; 98 - TEST_ASSERT_EQUAL( '7', mapKeyFrame( f, key_code::kNone ) ); 99 - } 100 - 101 - // Held key: same character emitted on prev frame is suppressed (edge only). 102 - void test_held_key_does_not_repeat() { 103 - KeyFrame f{}; 104 - const char w[] = "a"; 105 - f.word = w; 106 - f.wordLen = 1; 107 - 108 - char first = mapKeyFrame( f, key_code::kNone ); 109 - char second = mapKeyFrame( f, first ); 110 - 111 - TEST_ASSERT_EQUAL( 'a', first ); 112 - TEST_ASSERT_EQUAL( key_code::kNone, second ); 113 - } 114 - 115 - // Releasing then re-pressing the same key emits it again. 116 - void test_re_press_after_release_emits_again() { 117 - KeyFrame held{}; 118 - const char w[] = "z"; 119 - held.word = w; 120 - held.wordLen = 1; 121 - 122 - char first = mapKeyFrame( held, key_code::kNone ); 123 - TEST_ASSERT_EQUAL( 'z', first ); 124 - 125 - KeyFrame released{}; 126 - char none = mapKeyFrame( released, first ); 127 - TEST_ASSERT_EQUAL( key_code::kNone, none ); 128 - 129 - char third = mapKeyFrame( held, none ); 130 - TEST_ASSERT_EQUAL( 'z', third ); 131 - } 132 - 133 - int main( int, char** ) { 134 - UNITY_BEGIN(); 135 - RUN_TEST( test_empty_frame_emits_nothing ); 136 - RUN_TEST( test_enter_wins_over_word ); 137 - RUN_TEST( test_backspace_wins_over_word_and_space ); 138 - RUN_TEST( test_fn_alone_becomes_escape ); 139 - RUN_TEST( test_fn_with_enter_does_not_emit_escape ); 140 - RUN_TEST( test_arrow_wins_over_fn_and_word ); 141 - RUN_TEST( test_space_emits_space_char ); 142 - RUN_TEST( test_word_emits_first_char ); 143 - RUN_TEST( test_digit_emits_digit ); 144 - RUN_TEST( test_held_key_does_not_repeat ); 145 - RUN_TEST( test_re_press_after_release_emits_again ); 146 - return UNITY_END(); 147 - }
-278
test/test_keyboard_layouts/test_keyboard_layouts.cpp
··· 1 - // Kleidos — Native test: Keyboard Layout Tables 2 - // Validates all 6 layout tables for structural correctness. 3 - 4 - #include <cstdint> 5 - #include <cstdio> 6 - #include <cstring> 7 - 8 - #include <unity.h> 9 - 10 - // millis() stub 11 - static uint32_t fakeMillis = 0; 12 - extern "C" uint32_t millis() { 13 - return fakeMillis; 14 - } 15 - 16 - // pgmspace.h stub for native 17 - #ifndef PROGMEM 18 - #define PROGMEM 19 - #endif 20 - #ifndef pgm_read_byte 21 - #define pgm_read_byte( addr ) ( *(const uint8_t*)( addr ) ) 22 - #endif 23 - 24 - #include "ble/keyboard_layouts.h" 25 - 26 - // KeyboardLayouts.cpp is now linked through the native env's 27 - // build_src_filter (since B2). No #include here — that would cause 28 - // duplicate symbol definitions for LAYOUT_ES/UK/FR/DE/PT/IT. 29 - 30 - // --------------------------------------------------------------------------- 31 - // Helper: read a LayoutEntry from PROGMEM (or direct on native) 32 - // --------------------------------------------------------------------------- 33 - static LayoutEntry readEntry( const LayoutEntry* table, uint8_t idx ) { 34 - LayoutEntry e; 35 - const uint8_t* p = reinterpret_cast<const uint8_t*>( &table[idx] ); 36 - e.scancode = pgm_read_byte( p ); 37 - e.flags = pgm_read_byte( p + 1 ); 38 - return e; 39 - } 40 - 41 - // --------------------------------------------------------------------------- 42 - // Struct for iterating all layouts 43 - // --------------------------------------------------------------------------- 44 - struct LayoutInfo { 45 - const char* name; 46 - const LayoutEntry* table; 47 - }; 48 - 49 - static const LayoutInfo allLayouts[] = { 50 - { "ES", kLayoutEs }, { "UK", kLayoutUk }, { "FR", kLayoutFr }, 51 - { "DE", kLayoutDe }, { "PT", kLayoutPt }, { "IT", kLayoutIt }, 52 - }; 53 - static constexpr size_t NUM_LAYOUTS = sizeof( allLayouts ) / sizeof( allLayouts[0] ); 54 - 55 - // --------------------------------------------------------------------------- 56 - // Test: Space (ASCII 32) has scancode in all layouts 57 - // --------------------------------------------------------------------------- 58 - void test_space_has_scancode() { 59 - for ( size_t i = 0; i < NUM_LAYOUTS; i++ ) { 60 - LayoutEntry e = readEntry( allLayouts[i].table, 32 ); 61 - char msg[64]; 62 - snprintf( msg, sizeof( msg ), "Space missing scancode in %s", allLayouts[i].name ); 63 - TEST_ASSERT_NOT_EQUAL_MESSAGE( 0, e.scancode, msg ); 64 - // Space should be 0x2c (HID usage for spacebar) 65 - TEST_ASSERT_EQUAL_HEX8_MESSAGE( 0x2c, e.scancode, msg ); 66 - } 67 - } 68 - 69 - // --------------------------------------------------------------------------- 70 - // Test: All digits 0-9 have scancodes in all layouts 71 - // --------------------------------------------------------------------------- 72 - void test_digits_have_scancodes() { 73 - for ( size_t i = 0; i < NUM_LAYOUTS; i++ ) { 74 - for ( uint8_t c = '0'; c <= '9'; c++ ) { 75 - LayoutEntry e = readEntry( allLayouts[i].table, c ); 76 - char msg[64]; 77 - snprintf( msg, sizeof( msg ), "Digit '%c' missing in %s", c, allLayouts[i].name ); 78 - TEST_ASSERT_NOT_EQUAL_MESSAGE( 0, e.scancode, msg ); 79 - } 80 - } 81 - } 82 - 83 - // --------------------------------------------------------------------------- 84 - // Test: All lowercase letters a-z have scancodes in all layouts 85 - // --------------------------------------------------------------------------- 86 - void test_lowercase_have_scancodes() { 87 - for ( size_t i = 0; i < NUM_LAYOUTS; i++ ) { 88 - for ( uint8_t c = 'a'; c <= 'z'; c++ ) { 89 - LayoutEntry e = readEntry( allLayouts[i].table, c ); 90 - char msg[64]; 91 - snprintf( msg, sizeof( msg ), "Letter '%c' missing in %s", c, allLayouts[i].name ); 92 - TEST_ASSERT_NOT_EQUAL_MESSAGE( 0, e.scancode, msg ); 93 - // Lowercase should NOT have shift flag 94 - TEST_ASSERT_EQUAL_MESSAGE( 0, e.flags & kKlShift, msg ); 95 - } 96 - } 97 - } 98 - 99 - // --------------------------------------------------------------------------- 100 - // Test: All uppercase letters A-Z have scancodes in all layouts 101 - // --------------------------------------------------------------------------- 102 - void test_uppercase_have_scancodes() { 103 - for ( size_t i = 0; i < NUM_LAYOUTS; i++ ) { 104 - for ( uint8_t c = 'A'; c <= 'Z'; c++ ) { 105 - LayoutEntry e = readEntry( allLayouts[i].table, c ); 106 - char msg[64]; 107 - snprintf( msg, sizeof( msg ), "Letter '%c' missing in %s", c, allLayouts[i].name ); 108 - TEST_ASSERT_NOT_EQUAL_MESSAGE( 0, e.scancode, msg ); 109 - // Uppercase must have shift flag 110 - TEST_ASSERT_NOT_EQUAL_MESSAGE( 0, e.flags & kKlShift, msg ); 111 - } 112 - } 113 - } 114 - 115 - // --------------------------------------------------------------------------- 116 - // Test: Upper and lowercase letters use same scancode (just shift differs) 117 - // --------------------------------------------------------------------------- 118 - void test_case_scancode_match() { 119 - for ( size_t i = 0; i < NUM_LAYOUTS; i++ ) { 120 - for ( uint8_t lower = 'a'; lower <= 'z'; lower++ ) { 121 - uint8_t upper = lower - 32; 122 - LayoutEntry eLow = readEntry( allLayouts[i].table, lower ); 123 - LayoutEntry eUp = readEntry( allLayouts[i].table, upper ); 124 - char msg[64]; 125 - snprintf( msg, sizeof( msg ), "'%c'/'%c' scancode mismatch in %s", upper, lower, 126 - allLayouts[i].name ); 127 - TEST_ASSERT_EQUAL_HEX8_MESSAGE( eLow.scancode, eUp.scancode, msg ); 128 - } 129 - } 130 - } 131 - 132 - // --------------------------------------------------------------------------- 133 - // Test: Control characters (0-31 except 8,9,10) have no scancode 134 - // --------------------------------------------------------------------------- 135 - void test_control_chars_empty() { 136 - for ( size_t i = 0; i < NUM_LAYOUTS; i++ ) { 137 - for ( uint8_t c = 0; c <= 7; c++ ) { 138 - LayoutEntry e = readEntry( allLayouts[i].table, c ); 139 - char msg[64]; 140 - snprintf( msg, sizeof( msg ), "Ctrl char %u has scancode in %s", c, 141 - allLayouts[i].name ); 142 - TEST_ASSERT_EQUAL_MESSAGE( 0, e.scancode, msg ); 143 - } 144 - for ( uint8_t c = 11; c <= 31; c++ ) { 145 - LayoutEntry e = readEntry( allLayouts[i].table, c ); 146 - char msg[64]; 147 - snprintf( msg, sizeof( msg ), "Ctrl char %u has scancode in %s", c, 148 - allLayouts[i].name ); 149 - TEST_ASSERT_EQUAL_MESSAGE( 0, e.scancode, msg ); 150 - } 151 - } 152 - } 153 - 154 - // --------------------------------------------------------------------------- 155 - // Test: BS (8), TAB (9), Enter (10) have standard scancodes 156 - // --------------------------------------------------------------------------- 157 - void test_special_control_chars() { 158 - for ( size_t i = 0; i < NUM_LAYOUTS; i++ ) { 159 - LayoutEntry bs = readEntry( allLayouts[i].table, 8 ); 160 - TEST_ASSERT_EQUAL_HEX8( 0x2a, bs.scancode ); // Backspace 161 - 162 - LayoutEntry tab = readEntry( allLayouts[i].table, 9 ); 163 - TEST_ASSERT_EQUAL_HEX8( 0x2b, tab.scancode ); // Tab 164 - 165 - LayoutEntry enter = readEntry( allLayouts[i].table, 10 ); 166 - TEST_ASSERT_EQUAL_HEX8( 0x28, enter.scancode ); // Enter 167 - } 168 - } 169 - 170 - // --------------------------------------------------------------------------- 171 - // Test: DEL (127) has no scancode 172 - // --------------------------------------------------------------------------- 173 - void test_del_empty() { 174 - for ( size_t i = 0; i < NUM_LAYOUTS; i++ ) { 175 - LayoutEntry e = readEntry( allLayouts[i].table, 127 ); 176 - TEST_ASSERT_EQUAL( 0, e.scancode ); 177 - } 178 - } 179 - 180 - // --------------------------------------------------------------------------- 181 - // Test: All flags use only valid bits (KL_SHIFT | KL_ALTGR | KL_DEAD) 182 - // --------------------------------------------------------------------------- 183 - void test_flags_valid_bits() { 184 - constexpr uint8_t VALID_MASK = kKlShift | kKlAltgr | kKlDead; 185 - for ( size_t i = 0; i < NUM_LAYOUTS; i++ ) { 186 - for ( uint8_t c = 0; c < 128; c++ ) { 187 - LayoutEntry e = readEntry( allLayouts[i].table, c ); 188 - uint8_t invalid = e.flags & ~VALID_MASK; 189 - char msg[64]; 190 - snprintf( msg, sizeof( msg ), "Invalid flags 0x%02X at %u in %s", e.flags, c, 191 - allLayouts[i].name ); 192 - TEST_ASSERT_EQUAL_MESSAGE( 0, invalid, msg ); 193 - } 194 - } 195 - } 196 - 197 - // --------------------------------------------------------------------------- 198 - // Test: Common printable chars have scancodes in all layouts 199 - // --------------------------------------------------------------------------- 200 - void test_common_printable_chars() { 201 - // These should be typeable in every layout 202 - const char* common = "!@#$%^&*()-_=+[]{}|;:,.<>?/\\\"' "; 203 - for ( size_t i = 0; i < NUM_LAYOUTS; i++ ) { 204 - for ( size_t j = 0; common[j] != '\0'; j++ ) { 205 - uint8_t c = static_cast<uint8_t>( common[j] ); 206 - if ( c >= 128 ) 207 - continue; 208 - LayoutEntry e = readEntry( allLayouts[i].table, c ); 209 - // Most should have scancodes, but backtick on IT is {0,0} 210 - // so we skip zero-scancode as acceptable for some layouts 211 - } 212 - } 213 - } 214 - 215 - // --------------------------------------------------------------------------- 216 - // Test: Scancodes are in valid HID range (0x04-0x65 for keyboard) 217 - // --------------------------------------------------------------------------- 218 - void test_scancodes_valid_range() { 219 - for ( size_t i = 0; i < NUM_LAYOUTS; i++ ) { 220 - for ( uint8_t c = 0; c < 128; c++ ) { 221 - LayoutEntry e = readEntry( allLayouts[i].table, c ); 222 - if ( e.scancode == 0 ) 223 - continue; // no mapping 224 - char msg[64]; 225 - snprintf( msg, sizeof( msg ), "Scancode 0x%02X out of range at %u in %s", e.scancode, c, 226 - allLayouts[i].name ); 227 - // Valid HID keyboard scancodes: 0x04 (a) to 0x65 (Keypad =) 228 - // Also 0x28 (Enter), 0x2a (BS), 0x2b (Tab), 0x2c (Space) 229 - TEST_ASSERT_TRUE_MESSAGE( e.scancode >= 0x04 && e.scancode <= 0x65, msg ); 230 - } 231 - } 232 - } 233 - 234 - // --------------------------------------------------------------------------- 235 - // Test: FR AZERTY has correct A/Q and W/Z swaps 236 - // --------------------------------------------------------------------------- 237 - void test_fr_azerty_swaps() { 238 - // In FR: 'a' maps to Q key (0x14), 'q' maps to A key (0x04) 239 - LayoutEntry a = readEntry( kLayoutFr, 'a' ); 240 - LayoutEntry q = readEntry( kLayoutFr, 'q' ); 241 - TEST_ASSERT_EQUAL_HEX8( 0x14, a.scancode ); // Q key 242 - TEST_ASSERT_EQUAL_HEX8( 0x04, q.scancode ); // A key 243 - 244 - // 'w' maps to Z key (0x1d in FR), 'z' maps to W key (0x1a) 245 - LayoutEntry w = readEntry( kLayoutFr, 'w' ); 246 - LayoutEntry z = readEntry( kLayoutFr, 'z' ); 247 - TEST_ASSERT_EQUAL_HEX8( 0x1d, w.scancode ); // Z key 248 - TEST_ASSERT_EQUAL_HEX8( 0x1a, z.scancode ); // W key 249 - } 250 - 251 - // --------------------------------------------------------------------------- 252 - // Test: DE QWERTZ has correct Y/Z swap 253 - // --------------------------------------------------------------------------- 254 - void test_de_qwertz_swap() { 255 - LayoutEntry y = readEntry( kLayoutDe, 'y' ); 256 - LayoutEntry z = readEntry( kLayoutDe, 'z' ); 257 - TEST_ASSERT_EQUAL_HEX8( 0x1d, y.scancode ); // Z key 258 - TEST_ASSERT_EQUAL_HEX8( 0x1c, z.scancode ); // Y key 259 - } 260 - 261 - // --------------------------------------------------------------------------- 262 - int main( int argc, char** argv ) { 263 - UNITY_BEGIN(); 264 - RUN_TEST( test_space_has_scancode ); 265 - RUN_TEST( test_digits_have_scancodes ); 266 - RUN_TEST( test_lowercase_have_scancodes ); 267 - RUN_TEST( test_uppercase_have_scancodes ); 268 - RUN_TEST( test_case_scancode_match ); 269 - RUN_TEST( test_control_chars_empty ); 270 - RUN_TEST( test_special_control_chars ); 271 - RUN_TEST( test_del_empty ); 272 - RUN_TEST( test_flags_valid_bits ); 273 - RUN_TEST( test_common_printable_chars ); 274 - RUN_TEST( test_scancodes_valid_range ); 275 - RUN_TEST( test_fr_azerty_swaps ); 276 - RUN_TEST( test_de_qwertz_swap ); 277 - return UNITY_END(); 278 - }
+176 -109
test/test_onboarding_logic/test_onboarding_logic.cpp test/states/test_onboarding_logic/test_onboarding_logic.cpp
··· 1 - // Kleidos — Native tests for the onboarding FSM logic and boot routing 2 - // 3 - // Exercises the OnboardingLogic state machine directly (PIN capture, 4 - // constant-time confirm, step transitions) and models the BOOT_CHECK 5 - // routing rule ("no vault + no NVS flag => ONBOARDING") with mocks. 6 - // No hardware dependencies: runs in `pio test -e native`. 1 + // Kleidos — Native test: OnboardingLogic (onboarding FSM and boot routing) 2 + // Build: pio test -e native -f states/test_onboarding_logic 7 3 4 + // --- File header ----------------------------------------------------------- 5 + 6 + // Group 1: module under test 8 7 #include "states/onboarding_logic.h" 9 8 9 + // Group 3: stdlib + Unity 10 10 #include <cstdint> 11 11 #include <cstring> 12 12 13 13 #include <unity.h> 14 14 15 - // millis() stub — required because the native build filter pulls in 16 - // HoldButton.cpp which references it. Onboarding logic itself does not 17 - // use time, so a static zero is enough. 18 - extern "C" uint32_t millis() { 19 - return 0; 20 - } 15 + // --- Constants ------------------------------------------------------------- 21 16 22 - // --------------------------------------------------------------------------- 23 - // Mocks for BOOT_CHECK routing test 24 - // --------------------------------------------------------------------------- 25 - namespace mock { 26 - bool vaultProvisioned = false; 27 - bool onboardingDone = false; 17 + /// Replicates the boot-routing rule from main.cpp's BOOT_CHECK block. 18 + enum class BootTarget { 19 + ONBOARDING, 20 + PIN_ENTRY 21 + }; 28 22 29 - // PIN that the fake VaultStore would have persisted. 30 - char storedPin[16] = {}; 31 - size_t storedLen = 0; 23 + // --- State variables ------------------------------------------------------- 32 24 33 - void reset() { 34 - vaultProvisioned = false; 35 - onboardingDone = false; 36 - memset( storedPin, 0, sizeof( storedPin ) ); 37 - storedLen = 0; 38 - } 25 + struct { 26 + bool vaultProvisioned; 27 + bool onboardingDone; 28 + char storedPin[16]; 29 + size_t storedLen; 30 + } spy; 39 31 40 - bool provision( const char* pin, size_t len ) { 41 - if ( len == 0 || len > sizeof( storedPin ) ) 42 - return false; 43 - memcpy( storedPin, pin, len ); 44 - storedLen = len; 45 - vaultProvisioned = true; 46 - return true; 32 + // --- Function declarations ------------------------------------------------- 33 + 34 + static BootTarget routeBoot( bool hasVault, bool done ); 35 + static bool provision( const char* pin, size_t len ); 36 + 37 + // --- setUp / tearDown ------------------------------------------------------ 38 + 39 + void setUp() { 40 + spy = {}; 47 41 } 48 - } // namespace mock 49 42 50 - /// Replicates the rule encoded in main.cpp's BOOT_CHECK block. 51 - enum class BootTarget { 52 - ONBOARDING, 53 - PIN_ENTRY 54 - }; 55 - static BootTarget routeBoot( bool hasVault, bool done ) { 56 - if ( !hasVault && !done ) 57 - return BootTarget::ONBOARDING; 58 - return BootTarget::PIN_ENTRY; 59 - } 43 + void tearDown() { /* intentionally empty */ } 44 + 45 + // --- Tests ----------------------------------------------------------------- 60 46 61 - // --------------------------------------------------------------------------- 62 - // OnboardingLogic tests 63 - // --------------------------------------------------------------------------- 64 - void test_initial_step_is_welcome() { 47 + static void test_initial_step_is_welcome() { 48 + // ARRANGE + ACT 65 49 OnboardingLogic logic; 50 + 51 + // ASSERT 66 52 TEST_ASSERT_EQUAL( static_cast<int>( OnboardingStep::WELCOME ), 67 53 static_cast<int>( logic.step() ) ); 68 54 TEST_ASSERT_EQUAL( 0, logic.createdPinLen() ); 69 55 } 70 56 71 - void test_advance_from_welcome() { 57 + static void test_advance_from_welcome() { 58 + // ARRANGE 72 59 OnboardingLogic logic; 73 - auto r = logic.advanceFromWelcome(); 60 + 61 + // ACT 62 + auto r = logic.advanceFromWelcome(); 63 + 64 + // ASSERT 74 65 TEST_ASSERT_EQUAL( static_cast<int>( OnboardingResult::CONTINUE ), static_cast<int>( r ) ); 75 66 TEST_ASSERT_EQUAL( static_cast<int>( OnboardingStep::LANGUAGE ), 76 67 static_cast<int>( logic.step() ) ); 77 68 } 78 69 79 - void test_advance_from_language() { 70 + static void test_advance_from_language() { 71 + // ARRANGE 80 72 OnboardingLogic logic; 81 73 logic.advanceFromWelcome(); 74 + 75 + // ACT 82 76 auto r = logic.advanceFromLanguage(); 77 + 78 + // ASSERT 83 79 TEST_ASSERT_EQUAL( static_cast<int>( OnboardingResult::CONTINUE ), static_cast<int>( r ) ); 84 80 TEST_ASSERT_EQUAL( static_cast<int>( OnboardingStep::CreatePin ), 85 81 static_cast<int>( logic.step() ) ); 86 82 } 87 83 88 - void test_store_created_pin_advances_to_confirm() { 84 + static void test_store_created_pin_advances_to_confirm() { 85 + // ARRANGE 89 86 OnboardingLogic logic; 90 87 logic.advanceFromWelcome(); 91 88 logic.advanceFromLanguage(); 89 + 90 + // ACT 92 91 logic.storeCreatedPin( "1234", 4 ); 92 + 93 + // ASSERT 93 94 TEST_ASSERT_EQUAL( static_cast<int>( OnboardingStep::ConfirmPin ), 94 95 static_cast<int>( logic.step() ) ); 95 96 TEST_ASSERT_EQUAL( 4, logic.createdPinLen() ); 96 97 } 97 98 98 - void test_store_rejects_too_short_pin() { 99 + static void test_store_rejects_too_short_pin() { 100 + // ARRANGE 99 101 OnboardingLogic logic; 100 102 logic.advanceFromWelcome(); 101 103 logic.advanceFromLanguage(); 104 + 105 + // ACT 102 106 logic.storeCreatedPin( "12", 2 ); 107 + 108 + // ASSERT 103 109 TEST_ASSERT_EQUAL( static_cast<int>( OnboardingStep::CreatePin ), 104 110 static_cast<int>( logic.step() ) ); 105 111 TEST_ASSERT_EQUAL( 0, logic.createdPinLen() ); 106 112 } 107 113 108 - void test_confirm_matches_stays_in_confirm_until_advance() { 114 + static void test_confirm_matches_stays_in_confirm_until_advance() { 115 + // ARRANGE 109 116 OnboardingLogic logic; 110 117 logic.advanceFromWelcome(); 111 118 logic.advanceFromLanguage(); 112 119 logic.storeCreatedPin( "4242", 4 ); 120 + 121 + // ACT 113 122 auto r = logic.confirmPin( "4242", 4 ); 123 + 124 + // ASSERT 114 125 TEST_ASSERT_EQUAL( static_cast<int>( OnboardingResult::CONTINUE ), static_cast<int>( r ) ); 115 126 // New 7-step flow: match keeps step_ at CONFIRM_PIN so the caller can 116 127 // provision the vault before explicitly advancing to PAIR. 117 128 TEST_ASSERT_EQUAL( static_cast<int>( OnboardingStep::ConfirmPin ), 118 129 static_cast<int>( logic.step() ) ); 119 130 131 + // ACT 120 132 logic.advanceFromConfirm(); 133 + 134 + // ASSERT 121 135 TEST_ASSERT_EQUAL( static_cast<int>( OnboardingStep::PAIR ), static_cast<int>( logic.step() ) ); 122 136 } 123 137 124 - void test_confirm_mismatch_returns_to_create_and_wipes() { 138 + static void test_confirm_mismatch_returns_to_create_and_wipes() { 139 + // ARRANGE 125 140 OnboardingLogic logic; 126 141 logic.advanceFromWelcome(); 127 142 logic.advanceFromLanguage(); 128 143 logic.storeCreatedPin( "1111", 4 ); 144 + 145 + // ACT 129 146 auto r = logic.confirmPin( "2222", 4 ); 147 + 148 + // ASSERT 130 149 TEST_ASSERT_EQUAL( static_cast<int>( OnboardingResult::MISMATCH ), static_cast<int>( r ) ); 131 150 TEST_ASSERT_EQUAL( static_cast<int>( OnboardingStep::CreatePin ), 132 151 static_cast<int>( logic.step() ) ); ··· 134 153 TEST_ASSERT_EQUAL( 0, logic.createdPinLen() ); 135 154 } 136 155 137 - void test_confirm_length_mismatch_is_treated_as_mismatch() { 156 + static void test_confirm_length_mismatch_is_treated_as_mismatch() { 157 + // ARRANGE 138 158 OnboardingLogic logic; 139 159 logic.advanceFromWelcome(); 140 160 logic.advanceFromLanguage(); 141 161 logic.storeCreatedPin( "1234", 4 ); 162 + 163 + // ACT 142 164 auto r = logic.confirmPin( "12345", 5 ); 165 + 166 + // ASSERT 143 167 TEST_ASSERT_EQUAL( static_cast<int>( OnboardingResult::MISMATCH ), static_cast<int>( r ) ); 144 168 TEST_ASSERT_EQUAL( static_cast<int>( OnboardingStep::CreatePin ), 145 169 static_cast<int>( logic.step() ) ); 146 170 } 147 171 148 - void test_acknowledge_done_finishes() { 172 + static void test_acknowledge_done_finishes() { 173 + // ARRANGE 149 174 OnboardingLogic logic; 150 175 logic.advanceFromWelcome(); 151 176 logic.advanceFromLanguage(); ··· 154 179 logic.advanceFromConfirm(); 155 180 logic.advanceFromPair(); 156 181 logic.advanceFromDemo(); 182 + 183 + // ASSERT (DONE step reached) 157 184 TEST_ASSERT_EQUAL( static_cast<int>( OnboardingStep::DONE ), static_cast<int>( logic.step() ) ); 185 + 186 + // ACT 158 187 auto r = logic.acknowledgeDone(); 188 + 189 + // ASSERT 159 190 TEST_ASSERT_EQUAL( static_cast<int>( OnboardingResult::FINISHED ), static_cast<int>( r ) ); 160 191 } 161 192 162 - void test_full_7_step_sequence() { 193 + static void test_full_7_step_sequence() { 194 + // ARRANGE 163 195 OnboardingLogic logic; 196 + 197 + // ACT + ASSERT (each advance verified immediately) 164 198 TEST_ASSERT_EQUAL( static_cast<int>( OnboardingStep::WELCOME ), 165 199 static_cast<int>( logic.step() ) ); 166 200 logic.advanceFromWelcome(); ··· 181 215 TEST_ASSERT_EQUAL( static_cast<int>( OnboardingStep::DONE ), static_cast<int>( logic.step() ) ); 182 216 } 183 217 184 - void test_optional_steps_can_be_skipped() { 218 + static void test_optional_steps_can_be_skipped() { 185 219 // Skipping PAIR and DEMO is just calling advanceFrom* without doing 186 220 // any side-effect — the logic layer treats every advance the same way. 221 + 222 + // ARRANGE 187 223 OnboardingLogic logic; 188 224 logic.advanceFromWelcome(); 189 - logic.advanceFromLanguage(); // skip LANGUAGE without selecting 225 + logic.advanceFromLanguage(); 190 226 logic.storeCreatedPin( "1357", 4 ); 191 227 logic.confirmPin( "1357", 4 ); 228 + 229 + // ACT 192 230 logic.advanceFromConfirm(); 193 231 logic.advanceFromPair(); // skip PAIR 194 232 logic.advanceFromDemo(); // skip DEMO 233 + 234 + // ASSERT 195 235 TEST_ASSERT_EQUAL( static_cast<int>( OnboardingStep::DONE ), static_cast<int>( logic.step() ) ); 196 236 } 197 237 198 - void test_progress_is_monotonic_and_hits_full() { 238 + static void test_progress_is_monotonic_and_hits_full() { 239 + // ARRANGE + ACT (static dispatch, no mutable state) 240 + 241 + // ASSERT 199 242 TEST_ASSERT_FLOAT_WITHIN( 0.01f, 1.0f / 7.0f, 200 243 OnboardingLogic::getProgress( OnboardingStep::WELCOME ) ); 201 244 TEST_ASSERT_FLOAT_WITHIN( 0.01f, 2.0f / 7.0f, ··· 207 250 TEST_ASSERT_EQUAL_UINT8( 7, OnboardingLogic::stepIndex( OnboardingStep::DONE ) ); 208 251 } 209 252 210 - void test_reset_returns_to_welcome_and_wipes_pin() { 253 + static void test_reset_returns_to_welcome_and_wipes_pin() { 254 + // ARRANGE 211 255 OnboardingLogic logic; 212 256 logic.advanceFromWelcome(); 213 257 logic.storeCreatedPin( "9876", 4 ); 258 + 259 + // ACT 214 260 logic.reset(); 261 + 262 + // ASSERT 215 263 TEST_ASSERT_EQUAL( static_cast<int>( OnboardingStep::WELCOME ), 216 264 static_cast<int>( logic.step() ) ); 217 265 TEST_ASSERT_EQUAL( 0, logic.createdPinLen() ); 218 266 } 219 267 220 - // --------------------------------------------------------------------------- 221 - // BOOT_CHECK routing tests 222 - // --------------------------------------------------------------------------- 223 - void test_boot_no_vault_no_flag_goes_to_onboarding() { 224 - mock::reset(); 225 - auto t = routeBoot( mock::vaultProvisioned, mock::onboardingDone ); 268 + static void test_boot_no_vault_no_flag_goes_to_onboarding() { 269 + // ARRANGE + ACT 270 + // (setUp() ensures spy.vaultProvisioned=false, spy.onboardingDone=false) 271 + auto t = routeBoot( spy.vaultProvisioned, spy.onboardingDone ); 272 + 273 + // ASSERT 226 274 TEST_ASSERT_EQUAL( static_cast<int>( BootTarget::ONBOARDING ), static_cast<int>( t ) ); 227 275 } 228 276 229 - void test_boot_with_vault_skips_onboarding() { 230 - mock::reset(); 231 - mock::vaultProvisioned = true; 232 - auto t = routeBoot( mock::vaultProvisioned, mock::onboardingDone ); 277 + static void test_boot_with_vault_skips_onboarding() { 278 + // ARRANGE 279 + spy.vaultProvisioned = true; 280 + 281 + // ACT 282 + auto t = routeBoot( spy.vaultProvisioned, spy.onboardingDone ); 283 + 284 + // ASSERT 233 285 TEST_ASSERT_EQUAL( static_cast<int>( BootTarget::PIN_ENTRY ), static_cast<int>( t ) ); 234 286 } 235 287 236 - void test_boot_with_flag_but_no_vault_skips_onboarding() { 288 + static void test_boot_with_flag_but_no_vault_skips_onboarding() { 237 289 // Factory reset that preserved NVS but wiped LittleFS — we respect the 238 290 // user's acknowledgment that they already saw the explainer. 239 - mock::reset(); 240 - mock::onboardingDone = true; 241 - auto t = routeBoot( mock::vaultProvisioned, mock::onboardingDone ); 291 + 292 + // ARRANGE 293 + spy.onboardingDone = true; 294 + 295 + // ACT 296 + auto t = routeBoot( spy.vaultProvisioned, spy.onboardingDone ); 297 + 298 + // ASSERT 242 299 TEST_ASSERT_EQUAL( static_cast<int>( BootTarget::PIN_ENTRY ), static_cast<int>( t ) ); 243 300 } 244 301 245 - // --------------------------------------------------------------------------- 246 - // End-to-end happy + sad path — using mock provisioning 247 - // --------------------------------------------------------------------------- 248 - void test_happy_path_creates_vault_and_marks_done() { 249 - mock::reset(); 302 + static void test_happy_path_creates_vault_and_marks_done() { 303 + // ARRANGE 250 304 OnboardingLogic logic; 251 305 252 - // Boot => onboarding (no vault, no flag). 253 - TEST_ASSERT_EQUAL( 254 - static_cast<int>( BootTarget::ONBOARDING ), 255 - static_cast<int>( routeBoot( mock::vaultProvisioned, mock::onboardingDone ) ) ); 306 + // ASSERT (boot check: onboarding required before provisioning) 307 + TEST_ASSERT_EQUAL( static_cast<int>( BootTarget::ONBOARDING ), 308 + static_cast<int>( routeBoot( spy.vaultProvisioned, spy.onboardingDone ) ) ); 256 309 310 + // ACT: walk through wizard to the confirm step 257 311 logic.advanceFromWelcome(); 258 312 logic.advanceFromLanguage(); 259 313 logic.storeCreatedPin( "8675", 4 ); 260 314 auto cr = logic.confirmPin( "8675", 4 ); 261 315 262 - // Match → caller now persists the vault and marks NVS done. 316 + // ASSERT: match, step stays at ConfirmPin waiting for caller to provision 263 317 TEST_ASSERT_EQUAL( static_cast<int>( OnboardingResult::CONTINUE ), static_cast<int>( cr ) ); 264 318 TEST_ASSERT_EQUAL( static_cast<int>( OnboardingStep::ConfirmPin ), 265 319 static_cast<int>( logic.step() ) ); 266 320 267 - TEST_ASSERT_TRUE( mock::provision( logic.createdPin(), logic.createdPinLen() ) ); 268 - mock::onboardingDone = true; 321 + // ACT: caller persists the vault and marks NVS done 322 + TEST_ASSERT_TRUE( provision( logic.createdPin(), logic.createdPinLen() ) ); 323 + spy.onboardingDone = true; 269 324 logic.wipe(); 270 325 271 - // Next boot: vault exists OR flag set → never show onboarding again. 272 - TEST_ASSERT_EQUAL( 273 - static_cast<int>( BootTarget::PIN_ENTRY ), 274 - static_cast<int>( routeBoot( mock::vaultProvisioned, mock::onboardingDone ) ) ); 326 + // ASSERT: next boot goes to PIN_ENTRY 327 + TEST_ASSERT_EQUAL( static_cast<int>( BootTarget::PIN_ENTRY ), 328 + static_cast<int>( routeBoot( spy.vaultProvisioned, spy.onboardingDone ) ) ); 275 329 } 276 330 277 - void test_sad_path_mismatch_does_not_write_vault() { 278 - mock::reset(); 331 + static void test_sad_path_mismatch_does_not_write_vault() { 332 + // ARRANGE 279 333 OnboardingLogic logic; 280 - 281 334 logic.advanceFromWelcome(); 282 335 logic.advanceFromLanguage(); 283 336 logic.storeCreatedPin( "1111", 4 ); 337 + 338 + // ACT 284 339 auto cr = logic.confirmPin( "2222", 4 ); 340 + 341 + // ASSERT: mismatch, no vault written, no NVS flag flip 285 342 TEST_ASSERT_EQUAL( static_cast<int>( OnboardingResult::MISMATCH ), static_cast<int>( cr ) ); 343 + TEST_ASSERT_FALSE( spy.vaultProvisioned ); 344 + TEST_ASSERT_FALSE( spy.onboardingDone ); 286 345 287 - // No provision happened — vault still empty, no NVS flag flip. 288 - TEST_ASSERT_FALSE( mock::vaultProvisioned ); 289 - TEST_ASSERT_FALSE( mock::onboardingDone ); 346 + // Boot still lands on onboarding until the user succeeds. 347 + TEST_ASSERT_EQUAL( static_cast<int>( BootTarget::ONBOARDING ), 348 + static_cast<int>( routeBoot( spy.vaultProvisioned, spy.onboardingDone ) ) ); 349 + } 290 350 291 - // Boot still lands on onboarding until the user succeeds. 292 - TEST_ASSERT_EQUAL( 293 - static_cast<int>( BootTarget::ONBOARDING ), 294 - static_cast<int>( routeBoot( mock::vaultProvisioned, mock::onboardingDone ) ) ); 351 + // --- State variables ------------------------------------------------------- 352 + 353 + static BootTarget routeBoot( bool hasVault, bool done ) { 354 + if ( !hasVault && !done ) 355 + return BootTarget::ONBOARDING; 356 + return BootTarget::PIN_ENTRY; 357 + } 358 + 359 + static bool provision( const char* pin, size_t len ) { 360 + if ( len == 0 || len > sizeof( spy.storedPin ) ) 361 + return false; 362 + memcpy( spy.storedPin, pin, len ); 363 + spy.storedLen = len; 364 + spy.vaultProvisioned = true; 365 + return true; 295 366 } 296 367 297 - // --------------------------------------------------------------------------- 298 - // Unity entry point 299 - // --------------------------------------------------------------------------- 300 - void setUp() {} 301 - void tearDown() {} 368 + // --- main() ---------------------------------------------------------------- 302 369 303 - int main( int, char** ) { 370 + int main( int /*argc*/, char** /*argv*/ ) { 304 371 UNITY_BEGIN(); 305 372 RUN_TEST( test_initial_step_is_welcome ); 306 373 RUN_TEST( test_advance_from_welcome );
+67 -23
test/test_ota_state_logic/test_ota_state_logic.cpp test/ota/test_ota_state_logic/test_ota_state_logic.cpp
··· 1 - // Kleidos — Native tests for OTA boot-time decision logic. 1 + // Kleidos — Native test: OTA State Logic (boot-time decision: mark-valid, rollback, none) 2 + // Build: pio test -e native -f ota/test_ota_state_logic 2 3 4 + // --- File header ----------------------------------------------------------- 5 + // Group 1: module under test 3 6 #include "ota/ota_state_logic.h" 4 7 8 + // Group 3: stdlib + Unity 5 9 #include <unity.h> 6 10 11 + // --- Constants ------------------------------------------------------------- 7 12 using ota::BootAction; 8 13 using ota::decideOnBoot; 9 14 using ota::ImgState; 10 15 11 - extern "C" uint32_t millis() { 12 - return 0; 13 - } 16 + // --- State variables ------------------------------------------------------- 17 + 18 + // --- Function declarations ------------------------------------------------- 19 + // ota_state_logic.cpp calls millis() directly; stub satisfies linker 14 20 21 + // --- setUp / tearDown ------------------------------------------------------ 15 22 void setUp() {} 16 - void tearDown() {} 23 + void tearDown() { /* intentionally empty */ } 17 24 18 - // --------------------------------------------------------------------------- 25 + // --- Tests ----------------------------------------------------------------- 19 26 // MARK_VALID: pending-verify takes priority over any other signal. 20 - // --------------------------------------------------------------------------- 21 - void test_pending_verify_returns_mark_valid() { 27 + static void test_pending_verify_returns_mark_valid() { 28 + // ARRANGE + ACT 22 29 auto a = decideOnBoot( ImgState::PendingVerify, false, "1.0.0", "1.0.0" ); 30 + 31 + // ASSERT 23 32 TEST_ASSERT_EQUAL( static_cast<int>( BootAction::MarkValid ), static_cast<int>( a ) ); 24 33 } 25 34 26 - void test_pending_verify_with_attempted_still_mark_valid() { 35 + static void test_pending_verify_with_attempted_still_mark_valid() { 36 + // ARRANGE + ACT 27 37 auto a = decideOnBoot( ImgState::PendingVerify, true, "1.1.0", "1.1.0" ); 38 + 39 + // ASSERT 28 40 TEST_ASSERT_EQUAL( static_cast<int>( BootAction::MarkValid ), static_cast<int>( a ) ); 29 41 } 30 42 31 - void test_pending_verify_with_version_mismatch_prefers_mark_valid() { 43 + static void test_pending_verify_with_version_mismatch_prefers_mark_valid() { 32 44 // An OTA that just landed must still be marked valid — the app is the 33 45 // source of truth for "this binary works". Rollback detection happens 34 46 // on the *next* boot. 47 + 48 + // ARRANGE + ACT 35 49 auto a = decideOnBoot( ImgState::PendingVerify, true, "1.0.0", "2.0.0" ); 50 + 51 + // ASSERT 36 52 TEST_ASSERT_EQUAL( static_cast<int>( BootAction::MarkValid ), static_cast<int>( a ) ); 37 53 } 38 54 39 - // --------------------------------------------------------------------------- 40 55 // ROLLBACK_DETECTED: attempted flag set AND current version == previous. 41 - // --------------------------------------------------------------------------- 42 - void test_rollback_detected_when_current_matches_previous() { 56 + static void test_rollback_detected_when_current_matches_previous() { 43 57 // Previous firmware was 1.0.0, OTA to 2.0.0 attempted, but we booted 44 58 // back to 1.0.0 — rollback. 59 + 60 + // ARRANGE + ACT 45 61 auto a = decideOnBoot( ImgState::VALID, true, "1.0.0", "1.0.0" ); 62 + 63 + // ASSERT 46 64 TEST_ASSERT_EQUAL( static_cast<int>( BootAction::RollbackDetected ), static_cast<int>( a ) ); 47 65 } 48 66 49 - void test_rollback_detected_with_undefined_state() { 67 + static void test_rollback_detected_with_undefined_state() { 68 + // ARRANGE + ACT 50 69 auto a = decideOnBoot( ImgState::UNDEFINED, true, "1.0.0", "1.0.0" ); 70 + 71 + // ASSERT 51 72 TEST_ASSERT_EQUAL( static_cast<int>( BootAction::RollbackDetected ), static_cast<int>( a ) ); 52 73 } 53 74 54 - void test_rollback_not_detected_when_versions_differ() { 75 + static void test_rollback_not_detected_when_versions_differ() { 55 76 // OTA landed and we are running the new version — no rollback. 77 + 78 + // ARRANGE + ACT 56 79 auto a = decideOnBoot( ImgState::VALID, true, "1.0.0", "2.0.0" ); 80 + 81 + // ASSERT 57 82 TEST_ASSERT_EQUAL( static_cast<int>( BootAction::NONE ), static_cast<int>( a ) ); 58 83 } 59 84 60 - void test_rollback_not_detected_without_attempted_flag() { 85 + static void test_rollback_not_detected_without_attempted_flag() { 86 + // ARRANGE + ACT 61 87 auto a = decideOnBoot( ImgState::VALID, false, "1.0.0", "1.0.0" ); 88 + 89 + // ASSERT 62 90 TEST_ASSERT_EQUAL( static_cast<int>( BootAction::NONE ), static_cast<int>( a ) ); 63 91 } 64 92 65 - void test_rollback_not_detected_when_previous_version_empty() { 93 + static void test_rollback_not_detected_when_previous_version_empty() { 66 94 // No previous version recorded → cannot assert rollback happened. 95 + 96 + // ARRANGE + ACT 67 97 auto a = decideOnBoot( ImgState::VALID, true, "", "1.0.0" ); 98 + 99 + // ASSERT 68 100 TEST_ASSERT_EQUAL( static_cast<int>( BootAction::NONE ), static_cast<int>( a ) ); 69 101 } 70 102 71 - void test_rollback_not_detected_when_previous_version_null() { 103 + static void test_rollback_not_detected_when_previous_version_null() { 104 + // ARRANGE + ACT 72 105 auto a = decideOnBoot( ImgState::VALID, true, nullptr, "1.0.0" ); 106 + 107 + // ASSERT 73 108 TEST_ASSERT_EQUAL( static_cast<int>( BootAction::NONE ), static_cast<int>( a ) ); 74 109 } 75 110 76 - // --------------------------------------------------------------------------- 77 111 // NONE: normal cold boot. 78 - // --------------------------------------------------------------------------- 79 - void test_none_for_plain_cold_boot() { 112 + static void test_none_for_plain_cold_boot() { 113 + // ARRANGE + ACT 80 114 auto a = decideOnBoot( ImgState::VALID, false, "", "1.0.0" ); 115 + 116 + // ASSERT 81 117 TEST_ASSERT_EQUAL( static_cast<int>( BootAction::NONE ), static_cast<int>( a ) ); 82 118 } 83 119 84 - void test_none_for_new_image_state_without_attempted() { 120 + static void test_none_for_new_image_state_without_attempted() { 121 + // ARRANGE + ACT 85 122 auto a = decideOnBoot( ImgState::NEW, false, "", "1.0.0" ); 123 + 124 + // ASSERT 86 125 TEST_ASSERT_EQUAL( static_cast<int>( BootAction::NONE ), static_cast<int>( a ) ); 87 126 } 88 127 89 - int main( int, char** ) { 128 + // --- Fake function definitions --------------------------------------------- 129 + 130 + // --- Helper definitions ---------------------------------------------------- 131 + 132 + // --- main() ---------------------------------------------------------------- 133 + int main( int /*argc*/, char** /*argv*/ ) { 90 134 UNITY_BEGIN(); 91 135 RUN_TEST( test_pending_verify_returns_mark_valid ); 92 136 RUN_TEST( test_pending_verify_with_attempted_still_mark_valid );
-125
test/test_passgen/test_passgen.cpp
··· 1 - // Kleidos — Native test: Password Generator 2 - 3 - #include <cmath> 4 - #include <cstdint> 5 - #include <cstring> 6 - 7 - #include <unity.h> 8 - 9 - // millis() stub required because HoldButton.cpp is linked in native env 10 - static uint32_t fakeMillis = 0; 11 - extern "C" uint32_t millis() { 12 - return fakeMillis; 13 - } 14 - 15 - #include "crypto/password_generator.h" 16 - 17 - // --------------------------------------------------------------------------- 18 - void test_generates_correct_length() { 19 - char buf[65]; 20 - PasswordGenerator::generate( buf, 16, 0x0F ); 21 - TEST_ASSERT_EQUAL( 16, strlen( buf ) ); 22 - } 23 - 24 - void test_generates_min_length() { 25 - char buf[65]; 26 - PasswordGenerator::generate( buf, 4, 0x0F ); // below min → clamped to 8 27 - TEST_ASSERT_EQUAL( 8, strlen( buf ) ); 28 - } 29 - 30 - void test_generates_max_length() { 31 - char buf[65]; 32 - PasswordGenerator::generate( buf, 64, 0x0F ); 33 - TEST_ASSERT_EQUAL( 64, strlen( buf ) ); 34 - } 35 - 36 - void test_only_lowercase() { 37 - char buf[65]; 38 - PasswordGenerator::generate( buf, 64, 0x01 ); 39 - for ( size_t i = 0; i < 64; i++ ) { 40 - TEST_ASSERT_TRUE_MESSAGE( buf[i] >= 'a' && buf[i] <= 'z', "Expected lowercase character" ); 41 - } 42 - } 43 - 44 - void test_only_uppercase() { 45 - char buf[65]; 46 - PasswordGenerator::generate( buf, 64, 0x02 ); 47 - for ( size_t i = 0; i < 64; i++ ) { 48 - TEST_ASSERT_TRUE_MESSAGE( buf[i] >= 'A' && buf[i] <= 'Z', "Expected uppercase character" ); 49 - } 50 - } 51 - 52 - void test_only_digits() { 53 - char buf[65]; 54 - PasswordGenerator::generate( buf, 64, 0x04 ); 55 - for ( size_t i = 0; i < 64; i++ ) { 56 - TEST_ASSERT_TRUE_MESSAGE( buf[i] >= '0' && buf[i] <= '9', "Expected digit character" ); 57 - } 58 - } 59 - 60 - void test_entropy_all_charsets() { 61 - // 16 chars, all 88 chars pool → 16 × log2(88) ≈ 103.4 bits 62 - float e = PasswordGenerator::entropyBits( 16, 0x0F ); 63 - TEST_ASSERT_FLOAT_WITHIN( 1.0f, 103.4f, e ); 64 - } 65 - 66 - void test_entropy_digits_only() { 67 - // 6 digits → 6 × log2(10) ≈ 19.9 bits 68 - float e = PasswordGenerator::entropyBits( 6, 0x04 ); 69 - TEST_ASSERT_FLOAT_WITHIN( 0.5f, 19.9f, e ); 70 - } 71 - 72 - void test_entropy_zero_charsets() { 73 - float e = PasswordGenerator::entropyBits( 16, 0x00 ); 74 - TEST_ASSERT_FLOAT_WITHIN( 0.01f, 0.0f, e ); 75 - } 76 - 77 - void test_null_terminated() { 78 - char buf[65]; 79 - memset( buf, 'X', sizeof( buf ) ); 80 - PasswordGenerator::generate( buf, 16, 0x0F ); 81 - TEST_ASSERT_EQUAL( '\0', buf[16] ); 82 - } 83 - 84 - void test_zero_charsets_defaults_to_all() { 85 - char buf[65]; 86 - PasswordGenerator::generate( buf, 16, 0x00 ); 87 - // Should default to ALL, producing 16 chars 88 - TEST_ASSERT_EQUAL( 16, strlen( buf ) ); 89 - } 90 - 91 - void test_only_symbols() { 92 - char buf[65]; 93 - PasswordGenerator::generate( buf, 32, 0x08 ); 94 - const char* symbols = "!@#$%^&*()-_=+[]{}|;:,.<>?"; 95 - for ( size_t i = 0; i < 32; i++ ) { 96 - TEST_ASSERT_NOT_NULL_MESSAGE( strchr( symbols, buf[i] ), "Expected symbol character" ); 97 - } 98 - } 99 - 100 - void test_two_consecutive_different() { 101 - // Two generates should produce different passwords (statistical) 102 - char buf1[65], buf2[65]; 103 - PasswordGenerator::generate( buf1, 32, 0x0F ); 104 - PasswordGenerator::generate( buf2, 32, 0x0F ); 105 - TEST_ASSERT_FALSE( strcmp( buf1, buf2 ) == 0 ); 106 - } 107 - 108 - // --------------------------------------------------------------------------- 109 - int main( int argc, char** argv ) { 110 - UNITY_BEGIN(); 111 - RUN_TEST( test_generates_correct_length ); 112 - RUN_TEST( test_generates_min_length ); 113 - RUN_TEST( test_generates_max_length ); 114 - RUN_TEST( test_only_lowercase ); 115 - RUN_TEST( test_only_uppercase ); 116 - RUN_TEST( test_only_digits ); 117 - RUN_TEST( test_entropy_all_charsets ); 118 - RUN_TEST( test_entropy_digits_only ); 119 - RUN_TEST( test_entropy_zero_charsets ); 120 - RUN_TEST( test_null_terminated ); 121 - RUN_TEST( test_zero_charsets_defaults_to_all ); 122 - RUN_TEST( test_only_symbols ); 123 - RUN_TEST( test_two_consecutive_different ); 124 - return UNITY_END(); 125 - }
-226
test/test_popup_logic/test_popup_logic.cpp
··· 1 - // Kleidos — Native unit tests: PopupQueue logic 2 - // Covers FIFO semantics, toast timeouts, modal resolution and spinner flag. 3 - 4 - #include "ui/popup/popup_queue.h" 5 - 6 - #include <unity.h> 7 - 8 - // millis() stub — the native env links in ui/hold_button.cpp for other tests, 9 - // which references millis(). PopupQueue itself takes nowMs explicitly. 10 - extern "C" uint32_t millis() { 11 - return 0; 12 - } 13 - 14 - using namespace popup; 15 - 16 - namespace { 17 - PopupQueue q; 18 - constexpr uint32_t TOAST_MS = 2000; 19 - } // namespace 20 - 21 - void setUp() { 22 - q.reset(); 23 - } 24 - void tearDown() {} 25 - 26 - // --------------------------------------------------------------------------- 27 - // Toast FIFO 28 - // --------------------------------------------------------------------------- 29 - static void test_push_toast_fifo_drops_oldest( void ) { 30 - q.pushToast( "A", "d1", Severity::INFO, 0, TOAST_MS ); 31 - q.pushToast( "B", "d2", Severity::SUCCESS, 0, TOAST_MS ); 32 - q.pushToast( "C", "d3", Severity::CAUTION, 0, TOAST_MS ); 33 - TEST_ASSERT_EQUAL_UINT8( 3, q.activeToasts() ); 34 - 35 - q.pushToast( "D", "d4", Severity::WARNING, 0, TOAST_MS ); 36 - TEST_ASSERT_EQUAL_UINT8( 3, q.activeToasts() ); 37 - // Oldest ("A") must be gone; order preserved: B, C, D. 38 - TEST_ASSERT_EQUAL_STRING( "B", q.toastAt( 0 )->title ); 39 - TEST_ASSERT_EQUAL_STRING( "C", q.toastAt( 1 )->title ); 40 - TEST_ASSERT_EQUAL_STRING( "D", q.toastAt( 2 )->title ); 41 - } 42 - 43 - static void test_strings_are_copied( void ) { 44 - char mut[8] = "Hello"; 45 - q.pushToast( mut, "det", Severity::INFO, 0, TOAST_MS ); 46 - mut[0] = 'X'; 47 - TEST_ASSERT_EQUAL_STRING( "Hello", q.toastAt( 0 )->title ); 48 - } 49 - 50 - // --------------------------------------------------------------------------- 51 - // Toast expiry via tick() 52 - // --------------------------------------------------------------------------- 53 - static void test_toast_expires_on_tick( void ) { 54 - q.pushToast( "t", nullptr, Severity::INFO, /*now=*/0, /*timeout=*/TOAST_MS ); 55 - TEST_ASSERT_EQUAL_UINT8( 1, q.activeToasts() ); 56 - 57 - TEST_ASSERT_EQUAL_UINT8( 0, q.tick( 1999 ) ); // still alive 58 - TEST_ASSERT_EQUAL_UINT8( 1, q.activeToasts() ); 59 - 60 - TEST_ASSERT_EQUAL_UINT8( 1, q.tick( 2000 ) ); // deadline reached → dismissed 61 - TEST_ASSERT_EQUAL_UINT8( 0, q.activeToasts() ); 62 - TEST_ASSERT_NULL( q.toastAt( 0 ) ); 63 - } 64 - 65 - static void test_toast_without_tick_does_not_expire( void ) { 66 - q.pushToast( "t", nullptr, Severity::INFO, /*now=*/0, /*timeout=*/TOAST_MS ); 67 - // Large amount of time elapses but nobody calls tick() — toast stays. 68 - TEST_ASSERT_EQUAL_UINT8( 1, q.activeToasts() ); 69 - TEST_ASSERT_EQUAL_UINT8( 1, q.activeToasts() ); 70 - TEST_ASSERT_EQUAL_UINT8( 1, q.activeToasts() ); 71 - } 72 - 73 - static void test_tick_dismisses_multiple_at_once( void ) { 74 - q.pushToast( "a", nullptr, Severity::INFO, 0, 1000 ); 75 - q.pushToast( "b", nullptr, Severity::INFO, 0, 1500 ); 76 - q.pushToast( "c", nullptr, Severity::INFO, 0, 3000 ); 77 - TEST_ASSERT_EQUAL_UINT8( 2, q.tick( 2000 ) ); // a + b expire 78 - TEST_ASSERT_EQUAL_UINT8( 1, q.activeToasts() ); 79 - TEST_ASSERT_EQUAL_STRING( "c", q.toastAt( 0 )->title ); 80 - } 81 - 82 - static void test_clear_toasts( void ) { 83 - q.pushToast( "a", nullptr, Severity::INFO, 0, TOAST_MS ); 84 - q.pushToast( "b", nullptr, Severity::INFO, 0, TOAST_MS ); 85 - q.clearToasts(); 86 - TEST_ASSERT_EQUAL_UINT8( 0, q.activeToasts() ); 87 - } 88 - 89 - // --------------------------------------------------------------------------- 90 - // Modal (confirm / alert) 91 - // --------------------------------------------------------------------------- 92 - static void test_confirm_resolves_with_confirm( void ) { 93 - q.beginModal( ModalKind::CONFIRM, "Delete?", "cred", "Delete", "Cancel", Severity::WARNING, 0, 94 - 0 ); 95 - TEST_ASSERT_TRUE( q.hasModal() ); 96 - TEST_ASSERT_EQUAL( ConfirmResult::PENDING, q.modal().result ); 97 - 98 - q.resolveModal( ConfirmResult::CONFIRM ); 99 - TEST_ASSERT_FALSE( q.hasModal() ); 100 - TEST_ASSERT_EQUAL( ConfirmResult::CONFIRM, q.modal().result ); 101 - TEST_ASSERT_EQUAL( ModalKind::NONE, q.modal().kind ); 102 - } 103 - 104 - static void test_confirm_resolves_with_cancel( void ) { 105 - q.beginModal( ModalKind::CONFIRM, "t", "m", "OK", "Cancel", Severity::WARNING, 0, 0 ); 106 - q.resolveModal( ConfirmResult::CANCEL ); 107 - TEST_ASSERT_EQUAL( ConfirmResult::CANCEL, q.modal().result ); 108 - } 109 - 110 - static void test_modal_timeout( void ) { 111 - q.beginModal( ModalKind::ALERT, "t", "m", "OK", "", Severity::INFO, /*now=*/0, 112 - /*timeoutMs=*/5000 ); 113 - 114 - TEST_ASSERT_FALSE( q.tickModal( 0 ) ); 115 - TEST_ASSERT_FALSE( q.tickModal( 4999 ) ); 116 - TEST_ASSERT_TRUE( q.hasModal() ); 117 - 118 - TEST_ASSERT_TRUE( q.tickModal( 5000 ) ); // exactly at deadline 119 - TEST_ASSERT_FALSE( q.hasModal() ); 120 - TEST_ASSERT_EQUAL( ConfirmResult::TIMEOUT, q.modal().result ); 121 - } 122 - 123 - static void test_modal_infinite_timeout_never_fires( void ) { 124 - q.beginModal( ModalKind::CONFIRM, "t", "m", "OK", "Cancel", Severity::WARNING, 0, 125 - /*timeout=*/0 ); 126 - TEST_ASSERT_FALSE( q.tickModal( 1000000 ) ); 127 - TEST_ASSERT_TRUE( q.hasModal() ); 128 - } 129 - 130 - static void test_modal_action_counts( void ) { 131 - TEST_ASSERT_EQUAL_UINT8( 0, modalActionCount( ModalKind::NOTICE ) ); 132 - TEST_ASSERT_EQUAL_UINT8( 1, modalActionCount( ModalKind::ALERT ) ); 133 - TEST_ASSERT_EQUAL_UINT8( 2, modalActionCount( ModalKind::CONFIRM ) ); 134 - TEST_ASSERT_EQUAL_UINT8( 3, modalActionCount( ModalKind::CHOICE ) ); 135 - } 136 - 137 - static void test_notice_modal_has_no_labels( void ) { 138 - q.beginNotice( "Paused", "No action required", Severity::INFO, 0, 0 ); 139 - TEST_ASSERT_TRUE( q.hasModal() ); 140 - TEST_ASSERT_EQUAL( ModalKind::NOTICE, q.modal().kind ); 141 - TEST_ASSERT_EQUAL_STRING( "", q.modal().cancelLabel ); 142 - TEST_ASSERT_EQUAL_STRING( "", q.modal().confirmLabel ); 143 - TEST_ASSERT_EQUAL_STRING( "", q.modal().thirdLabel ); 144 - } 145 - 146 - static void test_choice_modal_copies_three_labels( void ) { 147 - q.beginChoice( "Choose", "target", "Back", "User", "Pass", Severity::INFO, 0, 0 ); 148 - TEST_ASSERT_TRUE( q.hasModal() ); 149 - TEST_ASSERT_EQUAL( ModalKind::CHOICE, q.modal().kind ); 150 - TEST_ASSERT_EQUAL_STRING( "Back", q.modal().cancelLabel ); 151 - TEST_ASSERT_EQUAL_STRING( "User", q.modal().confirmLabel ); 152 - TEST_ASSERT_EQUAL_STRING( "Pass", q.modal().thirdLabel ); 153 - } 154 - 155 - // --------------------------------------------------------------------------- 156 - // Spinner 157 - // --------------------------------------------------------------------------- 158 - static void test_spinner_show_hide( void ) { 159 - TEST_ASSERT_FALSE( q.spinnerActive() ); 160 - q.spinnerShow( "Pairing..." ); 161 - TEST_ASSERT_TRUE( q.spinnerActive() ); 162 - TEST_ASSERT_EQUAL_STRING( "Pairing...", q.spinnerLabel() ); 163 - q.spinnerHide(); 164 - TEST_ASSERT_FALSE( q.spinnerActive() ); 165 - } 166 - 167 - // --------------------------------------------------------------------------- 168 - // Transition status 169 - // --------------------------------------------------------------------------- 170 - static void test_transition_show_and_expire( void ) { 171 - TEST_ASSERT_FALSE( q.transitionActive() ); 172 - q.transitionShow( "Opening vault", "Decrypting index", Severity::INFO, 100, 1200 ); 173 - TEST_ASSERT_TRUE( q.transitionActive() ); 174 - TEST_ASSERT_EQUAL_STRING( "Opening vault", q.transition().title ); 175 - TEST_ASSERT_EQUAL_STRING( "Decrypting index", q.transition().detail ); 176 - 177 - TEST_ASSERT_FALSE( q.tickTransition( 1299 ) ); 178 - TEST_ASSERT_TRUE( q.transitionActive() ); 179 - TEST_ASSERT_TRUE( q.tickTransition( 1300 ) ); 180 - TEST_ASSERT_FALSE( q.transitionActive() ); 181 - } 182 - 183 - // --------------------------------------------------------------------------- 184 - // Aggregated state 185 - // --------------------------------------------------------------------------- 186 - static void test_any_active_reports_correctly( void ) { 187 - TEST_ASSERT_FALSE( q.anyActive() ); 188 - q.pushToast( "t", nullptr, Severity::INFO, 0, TOAST_MS ); 189 - TEST_ASSERT_TRUE( q.anyActive() ); 190 - q.clearToasts(); 191 - TEST_ASSERT_FALSE( q.anyActive() ); 192 - q.spinnerShow( "x" ); 193 - TEST_ASSERT_TRUE( q.anyActive() ); 194 - q.spinnerHide(); 195 - TEST_ASSERT_FALSE( q.anyActive() ); 196 - q.transitionShow( "x", nullptr, Severity::INFO, 0, 1000 ); 197 - TEST_ASSERT_TRUE( q.anyActive() ); 198 - q.clearTransition(); 199 - TEST_ASSERT_FALSE( q.anyActive() ); 200 - q.beginModal( ModalKind::ALERT, "t", "m", "OK", "", Severity::INFO, 0, 0 ); 201 - TEST_ASSERT_TRUE( q.anyActive() ); 202 - q.resolveModal( ConfirmResult::CONFIRM ); 203 - TEST_ASSERT_FALSE( q.anyActive() ); 204 - } 205 - 206 - // --------------------------------------------------------------------------- 207 - int main( int, char** ) { 208 - UNITY_BEGIN(); 209 - RUN_TEST( test_push_toast_fifo_drops_oldest ); 210 - RUN_TEST( test_strings_are_copied ); 211 - RUN_TEST( test_toast_expires_on_tick ); 212 - RUN_TEST( test_toast_without_tick_does_not_expire ); 213 - RUN_TEST( test_tick_dismisses_multiple_at_once ); 214 - RUN_TEST( test_clear_toasts ); 215 - RUN_TEST( test_confirm_resolves_with_confirm ); 216 - RUN_TEST( test_confirm_resolves_with_cancel ); 217 - RUN_TEST( test_modal_timeout ); 218 - RUN_TEST( test_modal_infinite_timeout_never_fires ); 219 - RUN_TEST( test_modal_action_counts ); 220 - RUN_TEST( test_notice_modal_has_no_labels ); 221 - RUN_TEST( test_choice_modal_copies_three_labels ); 222 - RUN_TEST( test_spinner_show_hide ); 223 - RUN_TEST( test_transition_show_and_expire ); 224 - RUN_TEST( test_any_active_reports_correctly ); 225 - return UNITY_END(); 226 - }
-202
test/test_rekey_recovery/test_rekey_recovery.cpp
··· 1 - // Kleidos — Native tests for the rekey-recovery decision logic. 2 - 3 - #include "vault/vault_recovery_logic.h" 4 - 5 - #include <unity.h> 6 - 7 - using vaultrec::Action; 8 - using vaultrec::decideRecoveryAction; 9 - using vaultrec::InspectionInput; 10 - 11 - extern "C" uint32_t millis() { 12 - return 0; 13 - } 14 - 15 - void setUp() {} 16 - void tearDown() {} 17 - 18 - // --------------------------------------------------------------------------- 19 - // Helper: build an InspectionInput for the keyVersion-aware overload. 20 - // --------------------------------------------------------------------------- 21 - static InspectionInput makeIn( bool hasShadow, bool hasBak, bool hasMeta, uint32_t metaKv, 22 - uint32_t minKv, uint32_t maxKv, uint32_t count, 23 - bool hasOld = false ) { 24 - InspectionInput in{}; 25 - in.hasShadowDir = hasShadow; 26 - in.hasOldDir = hasOld; 27 - in.hasMetaBak = hasBak; 28 - in.hasCanonicalMeta = hasMeta; 29 - in.metaKeyVersion = metaKv; 30 - in.minCredKeyVersion = minKv; 31 - in.maxCredKeyVersion = maxKv; 32 - in.credCount = count; 33 - return in; 34 - } 35 - 36 - // --------------------------------------------------------------------------- 37 - // NOTHING 38 - // --------------------------------------------------------------------------- 39 - void test_clean_fs_requires_no_action() { 40 - TEST_ASSERT_EQUAL( static_cast<int>( Action::NOTHING ), 41 - static_cast<int>( decideRecoveryAction( false, false, true ) ) ); 42 - } 43 - 44 - void test_empty_fs_requires_no_action() { 45 - // Fresh device, no vault at all yet. 46 - TEST_ASSERT_EQUAL( static_cast<int>( Action::NOTHING ), 47 - static_cast<int>( decideRecoveryAction( false, false, false ) ) ); 48 - } 49 - 50 - // --------------------------------------------------------------------------- 51 - // ROLLBACK_SHADOW — pre-commit crash 52 - // --------------------------------------------------------------------------- 53 - void test_shadow_without_bak_rolls_back_shadow() { 54 - TEST_ASSERT_EQUAL( static_cast<int>( Action::RollbackShadow ), 55 - static_cast<int>( decideRecoveryAction( true, false, true ) ) ); 56 - } 57 - 58 - // --------------------------------------------------------------------------- 59 - // RESTORE_META — mid-commit crash 60 - // --------------------------------------------------------------------------- 61 - void test_bak_without_shadow_restores_meta() { 62 - // Shadow already fully purged but meta.bak still present (commit finished 63 - // swaps but crashed before dropping meta.bak). 64 - TEST_ASSERT_EQUAL( static_cast<int>( Action::RestoreMeta ), 65 - static_cast<int>( decideRecoveryAction( false, true, true ) ) ); 66 - } 67 - 68 - void test_bak_with_shadow_restores_meta() { 69 - // Full mid-commit: meta.bak is present alongside shadow files. 70 - TEST_ASSERT_EQUAL( static_cast<int>( Action::RestoreMeta ), 71 - static_cast<int>( decideRecoveryAction( true, true, true ) ) ); 72 - } 73 - 74 - void test_bak_without_canonical_meta_still_restores() { 75 - // Worst case: canonical meta was removed and we crashed before the new 76 - // one could be renamed into place. meta.bak is the only survivor. 77 - TEST_ASSERT_EQUAL( static_cast<int>( Action::RestoreMeta ), 78 - static_cast<int>( decideRecoveryAction( true, true, false ) ) ); 79 - } 80 - 81 - // --------------------------------------------------------------------------- 82 - // G4 — keyVersion consistency cases 83 - // --------------------------------------------------------------------------- 84 - 85 - void test_consistent_creds_no_action() { 86 - // All canonical creds match meta.keyVersion → nothing to do. 87 - auto in = makeIn( false, false, true, 88 - /*metaKv=*/3, /*minKv=*/3, /*maxKv=*/3, /*count=*/12 ); 89 - TEST_ASSERT_EQUAL( static_cast<int>( Action::NOTHING ), 90 - static_cast<int>( decideRecoveryAction( in ) ) ); 91 - } 92 - 93 - void test_legacy_zero_kv_consistent_no_action() { 94 - // Legacy vault: meta.kv missing (=0) and no .kv sidecars (all =0). 95 - // Should look fully consistent and trigger no recovery. 96 - auto in = makeIn( false, false, true, 97 - /*metaKv=*/0, /*minKv=*/0, /*maxKv=*/0, /*count=*/8 ); 98 - TEST_ASSERT_EQUAL( static_cast<int>( Action::NOTHING ), 99 - static_cast<int>( decideRecoveryAction( in ) ) ); 100 - } 101 - 102 - void test_partial_rename_with_bak_rolls_back_partial() { 103 - // meta.kv was bumped to 5; some canonical creds still report 4 104 - // (their rename never executed). meta.bak still present → can roll 105 - // back the meta swap (best effort, data loss is logged separately). 106 - auto in = makeIn( /*hasShadow=*/true, /*hasBak=*/true, /*hasMeta=*/true, 107 - /*metaKv=*/5, /*minKv=*/4, /*maxKv=*/5, /*count=*/10 ); 108 - TEST_ASSERT_EQUAL( static_cast<int>( Action::RollbackPartialRename ), 109 - static_cast<int>( decideRecoveryAction( in ) ) ); 110 - } 111 - 112 - void test_partial_rename_no_bak_requires_manual() { 113 - // Same inconsistency but meta.bak is gone → no automatic recovery 114 - // is safe; the caller must wipe + restore from a user backup. 115 - auto in = makeIn( /*hasShadow=*/false, /*hasBak=*/false, /*hasMeta=*/true, 116 - /*metaKv=*/5, /*minKv=*/4, /*maxKv=*/5, /*count=*/10 ); 117 - TEST_ASSERT_EQUAL( static_cast<int>( Action::RequireManualRecovery ), 118 - static_cast<int>( decideRecoveryAction( in ) ) ); 119 - } 120 - 121 - void test_cred_ahead_of_meta_requires_manual() { 122 - // Pathological: a cred reports kv > meta.kv. Should not happen in 123 - // normal operation (meta is swapped before per-cred renames in the 124 - // current rekey path) — defensive REQUIRE_MANUAL_RECOVERY. 125 - auto in = makeIn( /*hasShadow=*/false, /*hasBak=*/false, /*hasMeta=*/true, 126 - /*metaKv=*/2, /*minKv=*/2, /*maxKv=*/3, /*count=*/5 ); 127 - TEST_ASSERT_EQUAL( static_cast<int>( Action::RequireManualRecovery ), 128 - static_cast<int>( decideRecoveryAction( in ) ) ); 129 - } 130 - 131 - void test_cred_ahead_of_meta_with_bak_rolls_back_partial() { 132 - // Same pathological case but meta.bak present → rollback path. 133 - auto in = makeIn( /*hasShadow=*/false, /*hasBak=*/true, /*hasMeta=*/true, 134 - /*metaKv=*/2, /*minKv=*/2, /*maxKv=*/3, /*count=*/5 ); 135 - TEST_ASSERT_EQUAL( static_cast<int>( Action::RollbackPartialRename ), 136 - static_cast<int>( decideRecoveryAction( in ) ) ); 137 - } 138 - 139 - void test_g4_takes_priority_over_shadow_only() { 140 - // If a partial-rename is detected, it must take priority over the 141 - // legacy ROLLBACK_SHADOW decision (otherwise we'd miss the data 142 - // safety issue). 143 - auto in = makeIn( /*hasShadow=*/true, /*hasBak=*/false, /*hasMeta=*/true, 144 - /*metaKv=*/5, /*minKv=*/4, /*maxKv=*/5, /*count=*/10 ); 145 - TEST_ASSERT_EQUAL( static_cast<int>( Action::RequireManualRecovery ), 146 - static_cast<int>( decideRecoveryAction( in ) ) ); 147 - } 148 - 149 - // --------------------------------------------------------------------------- 150 - // H2 — /vault/old/ staging tree 151 - // --------------------------------------------------------------------------- 152 - 153 - // Mid-rename crash with /vault/old/ + meta.bak: rollback losslessly. 154 - void test_h2_old_dir_with_bak_and_inconsistent_creds_rolls_back_from_old() { 155 - auto in = makeIn( /*hasShadow=*/true, /*hasBak=*/true, /*hasMeta=*/true, 156 - /*metaKv=*/6, /*minKv=*/5, /*maxKv=*/6, /*count=*/8, 157 - /*hasOld=*/true ); 158 - TEST_ASSERT_EQUAL( static_cast<int>( Action::RollbackFromOldDir ), 159 - static_cast<int>( decideRecoveryAction( in ) ) ); 160 - } 161 - 162 - // Rekey already finished but cleanup didn't drop /vault/old/ yet. 163 - // All creds match meta — nothing to restore. The I/O caller is 164 - // responsible for purging the leftover staging tree. 165 - void test_h2_old_dir_with_consistent_creds_is_nothing() { 166 - auto in = makeIn( /*hasShadow=*/false, /*hasBak=*/true, /*hasMeta=*/true, 167 - /*metaKv=*/6, /*minKv=*/6, /*maxKv=*/6, /*count=*/8, 168 - /*hasOld=*/true ); 169 - TEST_ASSERT_EQUAL( static_cast<int>( Action::NOTHING ), 170 - static_cast<int>( decideRecoveryAction( in ) ) ); 171 - } 172 - 173 - // Pathological: /vault/old/ present but meta.bak gone and creds 174 - // inconsistent. We have no safe target meta version → escalate. 175 - void test_h2_old_dir_without_bak_and_inconsistent_creds_requires_manual() { 176 - auto in = makeIn( /*hasShadow=*/false, /*hasBak=*/false, /*hasMeta=*/true, 177 - /*metaKv=*/6, /*minKv=*/5, /*maxKv=*/6, /*count=*/8, 178 - /*hasOld=*/true ); 179 - TEST_ASSERT_EQUAL( static_cast<int>( Action::RequireManualRecovery ), 180 - static_cast<int>( decideRecoveryAction( in ) ) ); 181 - } 182 - 183 - int main( int, char** ) { 184 - UNITY_BEGIN(); 185 - RUN_TEST( test_clean_fs_requires_no_action ); 186 - RUN_TEST( test_empty_fs_requires_no_action ); 187 - RUN_TEST( test_shadow_without_bak_rolls_back_shadow ); 188 - RUN_TEST( test_bak_without_shadow_restores_meta ); 189 - RUN_TEST( test_bak_with_shadow_restores_meta ); 190 - RUN_TEST( test_bak_without_canonical_meta_still_restores ); 191 - RUN_TEST( test_consistent_creds_no_action ); 192 - RUN_TEST( test_legacy_zero_kv_consistent_no_action ); 193 - RUN_TEST( test_partial_rename_with_bak_rolls_back_partial ); 194 - RUN_TEST( test_partial_rename_no_bak_requires_manual ); 195 - RUN_TEST( test_cred_ahead_of_meta_requires_manual ); 196 - RUN_TEST( test_cred_ahead_of_meta_with_bak_rolls_back_partial ); 197 - RUN_TEST( test_g4_takes_priority_over_shadow_only ); 198 - RUN_TEST( test_h2_old_dir_with_bak_and_inconsistent_creds_rolls_back_from_old ); 199 - RUN_TEST( test_h2_old_dir_with_consistent_creds_is_nothing ); 200 - RUN_TEST( test_h2_old_dir_without_bak_and_inconsistent_creds_requires_manual ); 201 - return UNITY_END(); 202 - }
-331
test/test_safe_string/test_safe_string.cpp
··· 1 - // Kleidos — Native test: Safe String facade (kleidos::platform::str) 2 - 3 - #include "platform/safe_string.h" 4 - 5 - #include <cstdarg> 6 - #include <cstring> 7 - 8 - #include <unity.h> 9 - 10 - namespace str = kleidos::platform::str; 11 - 12 - // --- copy ------------------------------------------------------------------ 13 - void test_copy_basic() { 14 - char dest[8] = {}; 15 - const size_t n = str::copy( dest, "abc" ); 16 - TEST_ASSERT_EQUAL_STRING( "abc", dest ); 17 - TEST_ASSERT_EQUAL( 3u, n ); 18 - } 19 - 20 - void test_copy_truncates_and_terminates() { 21 - char dest[4] = {}; 22 - const size_t n = str::copy( dest, "abcdef" ); 23 - TEST_ASSERT_EQUAL_STRING( "abc", dest ); // 3 chars + NUL in a 4-byte buffer 24 - TEST_ASSERT_EQUAL( 3u, n ); 25 - TEST_ASSERT_EQUAL( '\0', dest[3] ); 26 - } 27 - 28 - void test_copy_exact_fit() { 29 - char dest[4] = {}; 30 - const size_t n = str::copy( dest, "abc" ); 31 - TEST_ASSERT_EQUAL_STRING( "abc", dest ); 32 - TEST_ASSERT_EQUAL( 3u, n ); 33 - } 34 - 35 - void test_copy_empty_source() { 36 - char dest[4] = { 'x', 'x', 'x', '\0' }; 37 - const size_t n = str::copy( dest, "" ); 38 - TEST_ASSERT_EQUAL_STRING( "", dest ); 39 - TEST_ASSERT_EQUAL( 0u, n ); 40 - } 41 - 42 - void test_copy_null_source() { 43 - char dest[4] = { 'x', 'x', 'x', '\0' }; 44 - const size_t n = str::copy( dest, nullptr ); 45 - TEST_ASSERT_EQUAL_STRING( "", dest ); 46 - TEST_ASSERT_EQUAL( 0u, n ); 47 - } 48 - 49 - void test_copy_explicit_size() { 50 - char dest[16] = {}; 51 - const size_t n = str::copy( dest, 4, "abcdef" ); 52 - TEST_ASSERT_EQUAL_STRING( "abc", dest ); 53 - TEST_ASSERT_EQUAL( 3u, n ); 54 - } 55 - 56 - void test_copy_zero_size_is_noop() { 57 - char dest[2] = { 'y', '\0' }; 58 - const size_t n = str::copy( dest, 0, "abc" ); 59 - TEST_ASSERT_EQUAL( 0u, n ); 60 - TEST_ASSERT_EQUAL( 'y', dest[0] ); // untouched 61 - } 62 - 63 - // --- append ---------------------------------------------------------------- 64 - void test_append_basic() { 65 - char dest[8] = "ab"; 66 - const size_t n = str::append( dest, "cd" ); 67 - TEST_ASSERT_EQUAL_STRING( "abcd", dest ); 68 - TEST_ASSERT_EQUAL( 4u, n ); 69 - } 70 - 71 - void test_append_truncates() { 72 - char dest[5] = "abc"; 73 - const size_t n = str::append( dest, "XYZ" ); 74 - TEST_ASSERT_EQUAL_STRING( "abcX", dest ); // only one char fits 75 - TEST_ASSERT_EQUAL( 4u, n ); 76 - } 77 - 78 - void test_append_to_full_buffer() { 79 - char dest[4] = "abc"; 80 - const size_t n = str::append( dest, "Z" ); 81 - TEST_ASSERT_EQUAL_STRING( "abc", dest ); // no room, unchanged 82 - TEST_ASSERT_EQUAL( 3u, n ); 83 - } 84 - 85 - void test_append_empty_source() { 86 - char dest[8] = "ab"; 87 - const size_t n = str::append( dest, "" ); 88 - TEST_ASSERT_EQUAL_STRING( "ab", dest ); 89 - TEST_ASSERT_EQUAL( 2u, n ); 90 - } 91 - 92 - // --- length ---------------------------------------------------------------- 93 - void test_length_basic() { 94 - TEST_ASSERT_EQUAL( 3u, str::length( "abc", 16 ) ); 95 - } 96 - 97 - void test_length_capped() { 98 - TEST_ASSERT_EQUAL( 2u, str::length( "abcdef", 2 ) ); 99 - } 100 - 101 - void test_length_null() { 102 - TEST_ASSERT_EQUAL( 0u, str::length( nullptr, 16 ) ); 103 - } 104 - 105 - void test_length_array_overload() { 106 - char buf[8] = "abc"; 107 - TEST_ASSERT_EQUAL( 3u, str::length( buf ) ); 108 - } 109 - 110 - void test_length_unterminated_capped_at_array_size() { 111 - char buf[4] = { 'a', 'b', 'c', 'd' }; // no NUL 112 - TEST_ASSERT_EQUAL( 4u, str::length( buf ) ); 113 - } 114 - 115 - void test_length_unbounded_basic() { 116 - const char* p = "hello"; 117 - TEST_ASSERT_EQUAL( 5u, str::length( p ) ); 118 - } 119 - 120 - void test_length_unbounded_null() { 121 - const char* p = nullptr; 122 - TEST_ASSERT_EQUAL( 0u, str::length( p ) ); 123 - } 124 - 125 - // --- format ---------------------------------------------------------------- 126 - void test_format_basic() { 127 - char buf[16] = {}; 128 - const size_t n = str::format( buf, sizeof( buf ), "%d-%s", 42, "x" ); 129 - TEST_ASSERT_EQUAL_STRING( "42-x", buf ); 130 - TEST_ASSERT_EQUAL( 4u, n ); 131 - } 132 - 133 - void test_format_exact_fit() { 134 - char buf[4] = {}; 135 - const size_t n = str::format( buf, sizeof( buf ), "%s", "abc" ); 136 - TEST_ASSERT_EQUAL_STRING( "abc", buf ); 137 - TEST_ASSERT_EQUAL( 3u, n ); 138 - } 139 - 140 - void test_format_truncates_and_clamps_return() { 141 - char buf[4] = {}; 142 - const size_t n = str::format( buf, sizeof( buf ), "%s", "abcdef" ); 143 - TEST_ASSERT_EQUAL_STRING( "abc", buf ); // 3 chars + NUL in a 4-byte buffer 144 - TEST_ASSERT_EQUAL( 3u, n ); // clamped, not the snprintf "would write" 6 145 - TEST_ASSERT_EQUAL( '\0', buf[3] ); 146 - } 147 - 148 - void test_format_null_dest() { 149 - const size_t n = str::format( nullptr, 8, "%d", 1 ); 150 - TEST_ASSERT_EQUAL( 0u, n ); 151 - } 152 - 153 - void test_format_zero_size_is_noop() { 154 - char buf[2] = { 'y', '\0' }; 155 - const size_t n = str::format( buf, 0, "%d", 1 ); 156 - TEST_ASSERT_EQUAL( 0u, n ); 157 - TEST_ASSERT_EQUAL( 'y', buf[0] ); // untouched 158 - } 159 - 160 - void test_format_null_fmt() { 161 - char buf[4] = { 'x', 'x', 'x', '\0' }; 162 - const char* fmt = nullptr; // non-literal so -Wformat stays quiet 163 - const size_t n = str::format( buf, sizeof( buf ), fmt ); 164 - TEST_ASSERT_EQUAL( 0u, n ); 165 - TEST_ASSERT_EQUAL_STRING( "", buf ); 166 - } 167 - 168 - // Mirrors how a module-local printf-style shim forwards captured args to the 169 - // va_list overload of the facade. 170 - static size_t callFormatV( char* dest, size_t destSize, const char* fmt, ... ) 171 - __attribute__( ( __format__( __printf__, 3, 4 ) ) ); 172 - static size_t callFormatV( char* dest, size_t destSize, const char* fmt, ... ) { 173 - va_list args; 174 - va_start( args, fmt ); 175 - const size_t n = str::formatV( dest, destSize, fmt, args ); 176 - va_end( args ); 177 - return n; 178 - } 179 - 180 - void test_formatV_forwards_arguments() { 181 - char buf[16] = {}; 182 - const size_t n = callFormatV( buf, sizeof( buf ), "%s=%d", "k", 7 ); 183 - TEST_ASSERT_EQUAL_STRING( "k=7", buf ); 184 - TEST_ASSERT_EQUAL( 3u, n ); 185 - } 186 - 187 - // --- equals ---------------------------------------------------------------- 188 - void test_equals_basic() { 189 - TEST_ASSERT_TRUE( str::equals( "abc", "abc" ) ); 190 - TEST_ASSERT_FALSE( str::equals( "abc", "abx" ) ); 191 - TEST_ASSERT_FALSE( str::equals( "abc", "ab" ) ); // prefix is not equal 192 - } 193 - 194 - void test_equals_null_safety() { 195 - TEST_ASSERT_TRUE( str::equals( nullptr, nullptr ) ); 196 - TEST_ASSERT_FALSE( str::equals( "abc", nullptr ) ); 197 - TEST_ASSERT_FALSE( str::equals( nullptr, "abc" ) ); 198 - } 199 - 200 - void test_equals_bounded() { 201 - TEST_ASSERT_TRUE( str::equals( "abcZZ", "abcYY", 3 ) ); // first 3 match 202 - TEST_ASSERT_FALSE( str::equals( "abcZZ", "abxYY", 3 ) ); // differ within cap 203 - TEST_ASSERT_TRUE( str::equals( "ab", "ab", 8 ) ); // stop at shared NUL 204 - TEST_ASSERT_FALSE( str::equals( "ab", "abc", 8 ) ); // length differs 205 - } 206 - 207 - void test_equals_bounded_does_not_overread() { 208 - // Unterminated buffers: comparison must stop at maxLen, never past it. 209 - const char a[3] = { 'x', 'y', 'z' }; // no NUL 210 - const char b[3] = { 'x', 'y', 'z' }; // no NUL 211 - TEST_ASSERT_TRUE( str::equals( a, b, 3 ) ); 212 - } 213 - 214 - void test_equals_ignore_case() { 215 - TEST_ASSERT_TRUE( str::equalsIgnoreCase( "AbC", "aBc" ) ); 216 - TEST_ASSERT_TRUE( str::equalsIgnoreCase( "HELLO", "hello" ) ); 217 - TEST_ASSERT_FALSE( str::equalsIgnoreCase( "abc", "abx" ) ); 218 - TEST_ASSERT_TRUE( str::equalsIgnoreCase( nullptr, nullptr ) ); 219 - TEST_ASSERT_FALSE( str::equalsIgnoreCase( "abc", nullptr ) ); 220 - } 221 - 222 - void test_equals_ignore_case_only_folds_ascii() { 223 - // High (negative) bytes must be compared verbatim, never locale-folded. 224 - const char a[] = { static_cast<char>( 0xC3 ), static_cast<char>( 0xA9 ), '\0' }; 225 - const char b[] = { static_cast<char>( 0xC3 ), static_cast<char>( 0xA9 ), '\0' }; 226 - const char c[] = { static_cast<char>( 0xC3 ), static_cast<char>( 0x89 ), '\0' }; 227 - TEST_ASSERT_TRUE( str::equalsIgnoreCase( a, b ) ); 228 - TEST_ASSERT_FALSE( str::equalsIgnoreCase( a, c ) ); // not folded as A-Z 229 - } 230 - 231 - // --- startsWith ------------------------------------------------------------ 232 - void test_starts_with() { 233 - TEST_ASSERT_TRUE( str::startsWith( "cred_login", "cred_" ) ); 234 - TEST_ASSERT_FALSE( str::startsWith( "totp_login", "cred_" ) ); 235 - TEST_ASSERT_TRUE( str::startsWith( "abc", "" ) ); // empty prefix matches 236 - TEST_ASSERT_FALSE( str::startsWith( "ab", "abc" ) ); // str shorter than prefix 237 - TEST_ASSERT_FALSE( str::startsWith( nullptr, "a" ) ); 238 - TEST_ASSERT_FALSE( str::startsWith( "a", nullptr ) ); 239 - } 240 - 241 - void test_starts_with_ignore_case() { 242 - TEST_ASSERT_TRUE( str::startsWithIgnoreCase( "0xABCD", "0X" ) ); 243 - TEST_ASSERT_TRUE( str::startsWithIgnoreCase( "HTTPS://x", "https://" ) ); 244 - TEST_ASSERT_FALSE( str::startsWithIgnoreCase( "ftp://x", "https://" ) ); 245 - } 246 - 247 - // --- findChar / findLastChar ----------------------------------------------- 248 - void test_find_char() { 249 - const char* s = "a.b.c"; 250 - TEST_ASSERT_EQUAL_PTR( s + 1, str::findChar( s, '.' ) ); 251 - TEST_ASSERT_NULL( str::findChar( s, 'Z' ) ); 252 - const char* nullStr = nullptr; 253 - TEST_ASSERT_NULL( str::findChar( nullStr, 'a' ) ); 254 - TEST_ASSERT_EQUAL_PTR( s + 5, str::findChar( s, '\0' ) ); // terminator, like strchr 255 - 256 - // Mutable overload: returns a writable pointer for in-place splitting. 257 - char buf[] = "user|pass"; 258 - char* sep = str::findChar( buf, '|' ); 259 - TEST_ASSERT_EQUAL_PTR( buf + 4, sep ); 260 - *sep = '\0'; 261 - TEST_ASSERT_EQUAL_STRING( "user", buf ); 262 - char* nullMut = nullptr; 263 - TEST_ASSERT_NULL( str::findChar( nullMut, 'a' ) ); 264 - } 265 - 266 - void test_find_last_char() { 267 - const char* s = "a.b.c"; 268 - TEST_ASSERT_EQUAL_PTR( s + 3, str::findLastChar( s, '.' ) ); 269 - TEST_ASSERT_NULL( str::findLastChar( s, 'Z' ) ); 270 - const char* nullStr = nullptr; 271 - TEST_ASSERT_NULL( str::findLastChar( nullStr, 'a' ) ); 272 - 273 - // Mutable overload: returns a writable pointer to the last match. 274 - char buf[] = "a/b/c"; 275 - char* last = str::findLastChar( buf, '/' ); 276 - TEST_ASSERT_EQUAL_PTR( buf + 3, last ); 277 - char* nullMut = nullptr; 278 - TEST_ASSERT_NULL( str::findLastChar( nullMut, 'a' ) ); 279 - } 280 - 281 - // --- findSubstring --------------------------------------------------------- 282 - void test_find_substring() { 283 - const char* s = "host://path"; 284 - TEST_ASSERT_EQUAL_PTR( s + 4, str::findSubstring( s, "://" ) ); 285 - TEST_ASSERT_NULL( str::findSubstring( s, "@" ) ); 286 - TEST_ASSERT_EQUAL_PTR( s, str::findSubstring( s, "" ) ); // empty needle, like strstr 287 - TEST_ASSERT_NULL( str::findSubstring( nullptr, "a" ) ); 288 - TEST_ASSERT_NULL( str::findSubstring( "a", nullptr ) ); 289 - } 290 - 291 - // --------------------------------------------------------------------------- 292 - int main( int /*argc*/, char** /*argv*/ ) { 293 - UNITY_BEGIN(); 294 - RUN_TEST( test_copy_basic ); 295 - RUN_TEST( test_copy_truncates_and_terminates ); 296 - RUN_TEST( test_copy_exact_fit ); 297 - RUN_TEST( test_copy_empty_source ); 298 - RUN_TEST( test_copy_null_source ); 299 - RUN_TEST( test_copy_explicit_size ); 300 - RUN_TEST( test_copy_zero_size_is_noop ); 301 - RUN_TEST( test_append_basic ); 302 - RUN_TEST( test_append_truncates ); 303 - RUN_TEST( test_append_to_full_buffer ); 304 - RUN_TEST( test_append_empty_source ); 305 - RUN_TEST( test_length_basic ); 306 - RUN_TEST( test_length_capped ); 307 - RUN_TEST( test_length_null ); 308 - RUN_TEST( test_length_array_overload ); 309 - RUN_TEST( test_length_unterminated_capped_at_array_size ); 310 - RUN_TEST( test_length_unbounded_basic ); 311 - RUN_TEST( test_length_unbounded_null ); 312 - RUN_TEST( test_format_basic ); 313 - RUN_TEST( test_format_exact_fit ); 314 - RUN_TEST( test_format_truncates_and_clamps_return ); 315 - RUN_TEST( test_format_null_dest ); 316 - RUN_TEST( test_format_zero_size_is_noop ); 317 - RUN_TEST( test_format_null_fmt ); 318 - RUN_TEST( test_formatV_forwards_arguments ); 319 - RUN_TEST( test_equals_basic ); 320 - RUN_TEST( test_equals_null_safety ); 321 - RUN_TEST( test_equals_bounded ); 322 - RUN_TEST( test_equals_bounded_does_not_overread ); 323 - RUN_TEST( test_equals_ignore_case ); 324 - RUN_TEST( test_equals_ignore_case_only_folds_ascii ); 325 - RUN_TEST( test_starts_with ); 326 - RUN_TEST( test_starts_with_ignore_case ); 327 - RUN_TEST( test_find_char ); 328 - RUN_TEST( test_find_last_char ); 329 - RUN_TEST( test_find_substring ); 330 - return UNITY_END(); 331 - }
+110 -25
test/test_settings_logic/test_settings_logic.cpp test/states/test_settings_nav_logic/test_settings_nav_logic.cpp
··· 1 - // Kleidos — Native tests for the on-device Settings navigation tree. 1 + // Kleidos — Native test: SettingsNavLogic (settings navigation tree) 2 + // Build: pio test -e native -f states/test_settings_nav_logic 3 + 4 + // --- File header ----------------------------------------------------------- 2 5 6 + // Group 1: module under test 3 7 #include "states/settings_nav_logic.h" 4 8 9 + // Group 3: stdlib + Unity 5 10 #include <cstdint> 6 11 7 12 #include <unity.h> 8 13 9 - extern "C" uint32_t millis() { 10 - return 0; 11 - } 14 + // --- setUp / tearDown ------------------------------------------------------ 15 + 16 + void setUp() { /* intentionally empty */ } 12 17 13 - void test_initial_state_is_root_first_section() { 18 + void tearDown() { /* intentionally empty */ } 19 + 20 + // --- Tests ----------------------------------------------------------------- 21 + 22 + static void test_initial_state_is_root_first_section() { 23 + // ARRANGE + ACT 14 24 SettingsNavLogic n; 25 + 26 + // ASSERT 15 27 TEST_ASSERT_EQUAL( static_cast<int>( SettingsLevel::ROOT ), static_cast<int>( n.level() ) ); 16 28 TEST_ASSERT_EQUAL( 0, n.sectionIdx() ); 17 29 TEST_ASSERT_EQUAL( static_cast<int>( SettingsSection::DISPLAY ), 18 30 static_cast<int>( n.section() ) ); 19 31 } 20 32 21 - void test_root_navigation_wraps() { 33 + static void test_root_navigation_wraps() { 34 + // ARRANGE 22 35 SettingsNavLogic n; 36 + 37 + // ACT 23 38 n.next(); 39 + 40 + // ASSERT 24 41 TEST_ASSERT_EQUAL( static_cast<int>( SettingsSection::SECURITY ), 25 42 static_cast<int>( n.section() ) ); 26 - // 5 sections total: 5 next() from DISPLAY returns to DISPLAY. 43 + 44 + // ACT (5 sections total: 5 next() from DISPLAY returns to DISPLAY) 27 45 n.reset(); 28 46 for ( int i = 0; i < 5; ++i ) 29 47 n.next(); 48 + 49 + // ASSERT 30 50 TEST_ASSERT_EQUAL( static_cast<int>( SettingsSection::DISPLAY ), 31 51 static_cast<int>( n.section() ) ); 52 + 53 + // ACT 32 54 n.prev(); 55 + 56 + // ASSERT 33 57 TEST_ASSERT_EQUAL( static_cast<int>( SettingsSection::ABOUT ), 34 58 static_cast<int>( n.section() ) ); 35 59 } 36 60 37 - void test_section_counts() { 61 + static void test_section_counts() { 62 + // ARRANGE + ACT (static dispatch) 63 + 64 + // ASSERT 38 65 TEST_ASSERT_EQUAL( 2, SettingsNavLogic::itemCountForSection( SettingsSection::DISPLAY ) ); 39 66 TEST_ASSERT_EQUAL( 3, SettingsNavLogic::itemCountForSection( SettingsSection::SECURITY ) ); 40 67 TEST_ASSERT_EQUAL( 2, SettingsNavLogic::itemCountForSection( SettingsSection::POWER ) ); ··· 42 69 TEST_ASSERT_EQUAL( 1, SettingsNavLogic::itemCountForSection( SettingsSection::ABOUT ) ); 43 70 } 44 71 45 - void test_select_root_enters_section() { 72 + static void test_select_root_enters_section() { 73 + // ARRANGE 46 74 SettingsNavLogic n; 47 - auto a = n.select(); 75 + 76 + // ACT 77 + auto a = n.select(); 78 + 79 + // ASSERT 48 80 TEST_ASSERT_EQUAL( static_cast<int>( ItemAction::NONE ), static_cast<int>( a ) ); 49 81 TEST_ASSERT_EQUAL( static_cast<int>( SettingsLevel::SECTION ), static_cast<int>( n.level() ) ); 50 82 TEST_ASSERT_EQUAL( 0, n.itemIdx() ); 51 83 } 52 84 53 - void test_section_navigation_wraps() { 85 + static void test_section_navigation_wraps() { 86 + // ARRANGE 54 87 SettingsNavLogic n; 55 - n.select(); // DISPLAY section 88 + n.select(); // enter DISPLAY section 89 + 90 + // ASSERT (initial index) 56 91 TEST_ASSERT_EQUAL( 0, n.itemIdx() ); 92 + 93 + // ACT + ASSERT (forward) 57 94 n.next(); 58 95 TEST_ASSERT_EQUAL( 1, n.itemIdx() ); 59 96 n.next(); // wrap 60 97 TEST_ASSERT_EQUAL( 0, n.itemIdx() ); 98 + 99 + // ACT + ASSERT (backward wrap) 61 100 n.prev(); 62 101 TEST_ASSERT_EQUAL( 1, n.itemIdx() ); 63 102 } 64 103 65 - void test_back_from_section_to_root() { 104 + static void test_back_from_section_to_root() { 105 + // ARRANGE 66 106 SettingsNavLogic n; 67 107 n.select(); // enter DISPLAY 108 + 109 + // ACT 68 110 bool leave = n.back(); 111 + 112 + // ASSERT 69 113 TEST_ASSERT_FALSE( leave ); 70 114 TEST_ASSERT_EQUAL( static_cast<int>( SettingsLevel::ROOT ), static_cast<int>( n.level() ) ); 71 115 } 72 116 73 - void test_back_from_root_signals_leave() { 117 + static void test_back_from_root_signals_leave() { 118 + // ARRANGE 74 119 SettingsNavLogic n; 75 - bool leave = n.back(); 120 + 121 + // ACT 122 + bool leave = n.back(); 123 + 124 + // ASSERT 76 125 TEST_ASSERT_TRUE( leave ); 77 126 } 78 127 79 - void test_display_items_return_cycle_value() { 128 + static void test_display_items_return_cycle_value() { 129 + // ARRANGE 80 130 SettingsNavLogic n; 81 - n.select(); // DISPLAY 131 + n.select(); // enter DISPLAY 132 + 133 + // ACT 82 134 auto a = n.select(); 135 + 136 + // ASSERT 83 137 TEST_ASSERT_EQUAL( static_cast<int>( ItemAction::CycleValue ), static_cast<int>( a ) ); 138 + 139 + // ACT 84 140 n.next(); 85 141 a = n.select(); 142 + 143 + // ASSERT 86 144 TEST_ASSERT_EQUAL( static_cast<int>( ItemAction::CycleValue ), static_cast<int>( a ) ); 87 145 } 88 146 89 - void test_security_section_items() { 147 + static void test_security_section_items() { 148 + // ARRANGE 90 149 SettingsNavLogic n; 91 150 n.next(); // SECURITY 92 151 n.select(); 93 - // 0: CHANGE_PIN 152 + 153 + // ACT + ASSERT item 0: CHANGE_PIN 94 154 auto a = n.select(); 95 155 TEST_ASSERT_EQUAL( static_cast<int>( ItemAction::LaunchChangePin ), static_cast<int>( a ) ); 96 - n.next(); // AUTO_LOCK_SEC 156 + 157 + // ACT + ASSERT item 1: AUTO_LOCK_SEC 158 + n.next(); 97 159 a = n.select(); 98 160 TEST_ASSERT_EQUAL( static_cast<int>( ItemAction::CycleValue ), static_cast<int>( a ) ); 99 - n.next(); // WIPE_VAULT 161 + 162 + // ACT + ASSERT item 2: WIPE_VAULT 163 + n.next(); 100 164 a = n.select(); 101 165 TEST_ASSERT_EQUAL( static_cast<int>( ItemAction::ConfirmWipe ), static_cast<int>( a ) ); 102 166 } 103 167 104 - void test_power_section_items() { 168 + static void test_power_section_items() { 169 + // ARRANGE 105 170 SettingsNavLogic n; 106 171 n.next(); 107 172 n.next(); // POWER 108 173 n.select(); 174 + 175 + // ACT + ASSERT item 0 109 176 auto a = n.select(); 110 177 TEST_ASSERT_EQUAL( static_cast<int>( ItemAction::ShowInfo ), static_cast<int>( a ) ); 178 + 179 + // ACT + ASSERT item 1 111 180 n.next(); 112 181 a = n.select(); 113 182 TEST_ASSERT_EQUAL( static_cast<int>( ItemAction::ConfirmSleep ), static_cast<int>( a ) ); 114 183 } 115 184 116 - void test_device_section_items() { 185 + static void test_device_section_items() { 186 + // ARRANGE 117 187 SettingsNavLogic n; 118 188 n.next(); 119 189 n.next(); 120 190 n.next(); // DEVICE 121 191 n.select(); 192 + 193 + // ACT + ASSERT item 0 122 194 auto a = n.select(); 123 195 TEST_ASSERT_EQUAL( static_cast<int>( ItemAction::ShowInfo ), static_cast<int>( a ) ); 196 + 197 + // ACT + ASSERT item 1 124 198 n.next(); 125 199 a = n.select(); 126 200 TEST_ASSERT_EQUAL( static_cast<int>( ItemAction::CycleValue ), static_cast<int>( a ) ); 201 + 202 + // ACT + ASSERT item 2 127 203 n.next(); 128 204 a = n.select(); 129 205 TEST_ASSERT_EQUAL( static_cast<int>( ItemAction::ConfirmRestart ), static_cast<int>( a ) ); 130 206 } 131 207 132 - void test_about_section_single_item() { 208 + static void test_about_section_single_item() { 209 + // ARRANGE 133 210 SettingsNavLogic n; 134 211 n.next(); 135 212 n.next(); 136 213 n.next(); 137 214 n.next(); // ABOUT 215 + 216 + // ASSERT (section reached) 138 217 TEST_ASSERT_EQUAL( static_cast<int>( SettingsSection::ABOUT ), 139 218 static_cast<int>( n.section() ) ); 219 + 220 + // ACT 140 221 n.select(); 141 222 auto a = n.select(); 223 + 224 + // ASSERT 142 225 TEST_ASSERT_EQUAL( static_cast<int>( ItemAction::ShowInfo ), static_cast<int>( a ) ); 143 226 } 144 227 145 - int main( int, char** ) { 228 + // --- main() ---------------------------------------------------------------- 229 + 230 + int main( int /*argc*/, char** /*argv*/ ) { 146 231 UNITY_BEGIN(); 147 232 RUN_TEST( test_initial_state_is_root_first_section ); 148 233 RUN_TEST( test_root_navigation_wraps );
-127
test/test_shake_logic/test_shake_logic.cpp
··· 1 - // Kleidos — Native test: Shake detection logic 2 - // Tests magnitude calculation and threshold comparison without IMU hardware. 3 - 4 - #include <cmath> 5 - #include <cstdint> 6 - 7 - #include <unity.h> 8 - 9 - // millis() stub 10 - static uint32_t fakeMillis = 0; 11 - extern "C" uint32_t millis() { 12 - return fakeMillis; 13 - } 14 - 15 - // Replicate ShakeDetector magnitude logic 16 - static float accelMagnitude( float x, float y, float z ) { 17 - return sqrtf( x * x + y * y + z * z ); 18 - } 19 - 20 - static constexpr float DEFAULT_THRESHOLD = 2.5f; 21 - static constexpr uint32_t COOLDOWN_MS = 500; 22 - 23 - // Simulated detector state 24 - struct SimShake { 25 - float threshold = DEFAULT_THRESHOLD; 26 - bool enabled = true; 27 - uint32_t lastShakeMs = 0; 28 - 29 - bool check( float ax, float ay, float az ) { 30 - if ( !enabled ) 31 - return false; 32 - if ( fakeMillis - lastShakeMs < COOLDOWN_MS ) 33 - return false; 34 - 35 - float mag = accelMagnitude( ax, ay, az ); 36 - if ( mag > threshold ) { 37 - lastShakeMs = fakeMillis; 38 - return true; 39 - } 40 - return false; 41 - } 42 - }; 43 - 44 - // --------------------------------------------------------------------------- 45 - void test_magnitude_at_rest() { 46 - // At rest, gravity ~ 1.0g in one axis 47 - float mag = accelMagnitude( 0.0f, 0.0f, 1.0f ); 48 - TEST_ASSERT_FLOAT_WITHIN( 0.01f, 1.0f, mag ); 49 - } 50 - 51 - void test_magnitude_strong_shake() { 52 - float mag = accelMagnitude( 2.0f, 2.0f, 2.0f ); 53 - TEST_ASSERT_FLOAT_WITHIN( 0.1f, 3.46f, mag ); 54 - } 55 - 56 - void test_threshold_triggers_shake() { 57 - SimShake s; 58 - fakeMillis = 1000; 59 - // Strong shake (3g total) 60 - TEST_ASSERT_TRUE( s.check( 2.0f, 2.0f, 1.0f ) ); 61 - } 62 - 63 - void test_threshold_no_trigger_below() { 64 - SimShake s; 65 - fakeMillis = 1000; 66 - // Normal movement (1g) 67 - TEST_ASSERT_FALSE( s.check( 0.0f, 0.0f, 1.0f ) ); 68 - } 69 - 70 - void test_cooldown_prevents_rapid_fire() { 71 - SimShake s; 72 - fakeMillis = 1000; 73 - TEST_ASSERT_TRUE( s.check( 3.0f, 0.0f, 0.0f ) ); 74 - 75 - // Within cooldown 76 - fakeMillis = 1200; 77 - TEST_ASSERT_FALSE( s.check( 3.0f, 0.0f, 0.0f ) ); 78 - 79 - // After cooldown 80 - fakeMillis = 1600; 81 - TEST_ASSERT_TRUE( s.check( 3.0f, 0.0f, 0.0f ) ); 82 - } 83 - 84 - void test_disabled_never_triggers() { 85 - SimShake s; 86 - s.enabled = false; 87 - fakeMillis = 1000; 88 - TEST_ASSERT_FALSE( s.check( 10.0f, 10.0f, 10.0f ) ); 89 - } 90 - 91 - void test_custom_threshold() { 92 - SimShake s; 93 - s.threshold = 5.0f; 94 - fakeMillis = 1000; 95 - 96 - // 3g should NOT trigger with 5g threshold 97 - TEST_ASSERT_FALSE( s.check( 2.0f, 2.0f, 1.0f ) ); 98 - 99 - // 6g should trigger 100 - TEST_ASSERT_TRUE( s.check( 4.0f, 4.0f, 2.0f ) ); 101 - } 102 - 103 - void test_threshold_boundary() { 104 - SimShake s; 105 - s.threshold = 2.5f; 106 - fakeMillis = 1000; 107 - 108 - // Exactly at threshold should NOT trigger (> not >=) 109 - TEST_ASSERT_FALSE( s.check( 2.5f, 0.0f, 0.0f ) ); 110 - 111 - // Just above 112 - TEST_ASSERT_TRUE( s.check( 2.51f, 0.0f, 0.0f ) ); 113 - } 114 - 115 - // --------------------------------------------------------------------------- 116 - int main( int argc, char** argv ) { 117 - UNITY_BEGIN(); 118 - RUN_TEST( test_magnitude_at_rest ); 119 - RUN_TEST( test_magnitude_strong_shake ); 120 - RUN_TEST( test_threshold_triggers_shake ); 121 - RUN_TEST( test_threshold_no_trigger_below ); 122 - RUN_TEST( test_cooldown_prevents_rapid_fire ); 123 - RUN_TEST( test_disabled_never_triggers ); 124 - RUN_TEST( test_custom_threshold ); 125 - RUN_TEST( test_threshold_boundary ); 126 - return UNITY_END(); 127 - }
-207
test/test_status_notifier/test_status_notifier.cpp
··· 1 - // Kleidos — Native unit tests: StatusNotifier pure decision logic. 2 - // 3 - // Exercises hysteresis / one-shot rules of the notifier without linking 4 - // any Arduino or PopupSystem code. Only `notifier::decide*()` is used. 5 - 6 - #include "ui/notifications/status_notifier.h" 7 - 8 - #include <unity.h> 9 - 10 - extern "C" uint32_t millis() { 11 - return 0; 12 - } 13 - 14 - using notifier::Event; 15 - 16 - // --------------------------------------------------------------------------- 17 - // Battery hysteresis 18 - // --------------------------------------------------------------------------- 19 - static void test_battery_prime_does_not_fire( void ) { 20 - notifier::BatteryState st; 21 - // First sample always swallowed — prevents "charging started" at boot. 22 - TEST_ASSERT_EQUAL( static_cast<int>( Event::NONE ), 23 - static_cast<int>( notifier::decideBattery( st, 50, false ) ) ); 24 - } 25 - 26 - static void test_battery_low_fires_once_then_holds( void ) { 27 - notifier::BatteryState st; 28 - (void)notifier::decideBattery( st, 50, false ); // prime 29 - 30 - TEST_ASSERT_EQUAL( static_cast<int>( Event::BatteryLow ), 31 - static_cast<int>( notifier::decideBattery( st, 20, false ) ) ); 32 - // Second sample at 19 % — no re-emit (still below exit). 33 - TEST_ASSERT_EQUAL( static_cast<int>( Event::NONE ), 34 - static_cast<int>( notifier::decideBattery( st, 19, false ) ) ); 35 - } 36 - 37 - static void test_battery_low_hysteresis_19_21_no_spam( void ) { 38 - notifier::BatteryState st; 39 - (void)notifier::decideBattery( st, 50, false ); 40 - (void)notifier::decideBattery( st, 20, false ); // fires LOW 41 - 42 - // Oscillate 19/21 — below exit threshold of 22 → never re-arms. 43 - for ( int i = 0; i < 10; ++i ) { 44 - TEST_ASSERT_EQUAL( 45 - static_cast<int>( Event::NONE ), 46 - static_cast<int>( notifier::decideBattery( st, ( i % 2 ) ? 21 : 19, false ) ) ); 47 - } 48 - } 49 - 50 - static void test_battery_low_rearms_after_exit_threshold( void ) { 51 - notifier::BatteryState st; 52 - (void)notifier::decideBattery( st, 50, false ); 53 - (void)notifier::decideBattery( st, 20, false ); // fires 54 - (void)notifier::decideBattery( st, 22, false ); // re-arm 55 - TEST_ASSERT_EQUAL( static_cast<int>( Event::BatteryLow ), 56 - static_cast<int>( notifier::decideBattery( st, 20, false ) ) ); 57 - } 58 - 59 - static void test_battery_critical_fires_once_per_boot( void ) { 60 - notifier::BatteryState st; 61 - (void)notifier::decideBattery( st, 50, false ); 62 - TEST_ASSERT_EQUAL( static_cast<int>( Event::BatteryCritical ), 63 - static_cast<int>( notifier::decideBattery( st, 5, false ) ) ); 64 - // Any further sample (even dropping further) must not refire. 65 - TEST_ASSERT_EQUAL( static_cast<int>( Event::NONE ), 66 - static_cast<int>( notifier::decideBattery( st, 3, false ) ) ); 67 - TEST_ASSERT_EQUAL( static_cast<int>( Event::NONE ), 68 - static_cast<int>( notifier::decideBattery( st, 2, false ) ) ); 69 - } 70 - 71 - static void test_battery_charging_edge( void ) { 72 - notifier::BatteryState st; 73 - (void)notifier::decideBattery( st, 50, false ); // prime discharging 74 - TEST_ASSERT_EQUAL( static_cast<int>( Event::BatteryCharging ), 75 - static_cast<int>( notifier::decideBattery( st, 50, true ) ) ); 76 - // Same charging state → no refire. 77 - TEST_ASSERT_EQUAL( static_cast<int>( Event::NONE ), 78 - static_cast<int>( notifier::decideBattery( st, 55, true ) ) ); 79 - } 80 - 81 - static void test_battery_full_once_per_session( void ) { 82 - notifier::BatteryState st; 83 - (void)notifier::decideBattery( st, 50, false ); 84 - (void)notifier::decideBattery( st, 50, true ); // CHARGING 85 - TEST_ASSERT_EQUAL( static_cast<int>( Event::BatteryFull ), 86 - static_cast<int>( notifier::decideBattery( st, 99, true ) ) ); 87 - // Holding at 100 % charging → no refire. 88 - TEST_ASSERT_EQUAL( static_cast<int>( Event::NONE ), 89 - static_cast<int>( notifier::decideBattery( st, 100, true ) ) ); 90 - } 91 - 92 - // --------------------------------------------------------------------------- 93 - // BLE host 94 - // --------------------------------------------------------------------------- 95 - static void test_ble_host_edges( void ) { 96 - notifier::HostState st; 97 - (void)notifier::decideBleHost( st, false ); // prime 98 - TEST_ASSERT_EQUAL( static_cast<int>( Event::BleHostConnected ), 99 - static_cast<int>( notifier::decideBleHost( st, true ) ) ); 100 - TEST_ASSERT_EQUAL( static_cast<int>( Event::NONE ), 101 - static_cast<int>( notifier::decideBleHost( st, true ) ) ); 102 - TEST_ASSERT_EQUAL( static_cast<int>( Event::BleHostDisconnected ), 103 - static_cast<int>( notifier::decideBleHost( st, false ) ) ); 104 - } 105 - 106 - // --------------------------------------------------------------------------- 107 - // Admin client 108 - // --------------------------------------------------------------------------- 109 - static void test_admin_client_fires_on_increase( void ) { 110 - notifier::AdminClientState st; 111 - TEST_ASSERT_EQUAL( static_cast<int>( Event::NONE ), 112 - static_cast<int>( notifier::decideAdminClient( st, 0 ) ) ); 113 - TEST_ASSERT_EQUAL( static_cast<int>( Event::AdminClientConnected ), 114 - static_cast<int>( notifier::decideAdminClient( st, 1 ) ) ); 115 - TEST_ASSERT_EQUAL( static_cast<int>( Event::NONE ), 116 - static_cast<int>( notifier::decideAdminClient( st, 1 ) ) ); 117 - TEST_ASSERT_EQUAL( static_cast<int>( Event::NONE ), 118 - static_cast<int>( notifier::decideAdminClient( st, 0 ) ) ); 119 - TEST_ASSERT_EQUAL( static_cast<int>( Event::AdminClientConnected ), 120 - static_cast<int>( notifier::decideAdminClient( st, 1 ) ) ); 121 - } 122 - 123 - // --------------------------------------------------------------------------- 124 - // Admin idle warn + timeout 125 - // --------------------------------------------------------------------------- 126 - static void test_admin_idle_warn_then_timeout( void ) { 127 - notifier::AdminIdleState st; 128 - constexpr uint32_t TOTAL = 300000; 129 - constexpr uint32_t WARN = 30000; 130 - TEST_ASSERT_EQUAL( static_cast<int>( Event::NONE ), 131 - static_cast<int>( notifier::decideAdminIdle( st, 100000, TOTAL, WARN ) ) ); 132 - TEST_ASSERT_EQUAL( 133 - static_cast<int>( Event::AdminIdleWarning ), 134 - static_cast<int>( notifier::decideAdminIdle( st, TOTAL - WARN, TOTAL, WARN ) ) ); 135 - TEST_ASSERT_EQUAL( static_cast<int>( Event::NONE ), static_cast<int>( notifier::decideAdminIdle( 136 - st, TOTAL - 10000, TOTAL, WARN ) ) ); 137 - TEST_ASSERT_EQUAL( static_cast<int>( Event::AdminTimeout ), 138 - static_cast<int>( notifier::decideAdminIdle( st, TOTAL, TOTAL, WARN ) ) ); 139 - TEST_ASSERT_EQUAL( static_cast<int>( Event::NONE ), static_cast<int>( notifier::decideAdminIdle( 140 - st, TOTAL + 5000, TOTAL, WARN ) ) ); 141 - } 142 - 143 - static void test_admin_idle_activity_rearm( void ) { 144 - notifier::AdminIdleState st; 145 - constexpr uint32_t TOTAL = 300000; 146 - constexpr uint32_t WARN = 30000; 147 - (void)notifier::decideAdminIdle( st, TOTAL - WARN, TOTAL, WARN ); // warn fired 148 - (void)notifier::decideAdminIdle( st, 0, TOTAL, WARN ); // activity reset 149 - // Next time we cross the warn boundary the event must fire again. 150 - TEST_ASSERT_EQUAL( 151 - static_cast<int>( Event::AdminIdleWarning ), 152 - static_cast<int>( notifier::decideAdminIdle( st, TOTAL - WARN, TOTAL, WARN ) ) ); 153 - } 154 - 155 - // --------------------------------------------------------------------------- 156 - // Auto-lock 157 - // --------------------------------------------------------------------------- 158 - static void test_auto_lock_fires_once_rearms_when_far( void ) { 159 - notifier::AutoLockState st; 160 - TEST_ASSERT_EQUAL( static_cast<int>( Event::NONE ), 161 - static_cast<int>( notifier::decideAutoLock( st, 10000, 5000 ) ) ); 162 - TEST_ASSERT_EQUAL( static_cast<int>( Event::AutoLockCountdown ), 163 - static_cast<int>( notifier::decideAutoLock( st, 5000, 5000 ) ) ); 164 - TEST_ASSERT_EQUAL( static_cast<int>( Event::NONE ), 165 - static_cast<int>( notifier::decideAutoLock( st, 3000, 5000 ) ) ); 166 - // Activity reset (ms grows past window). 167 - TEST_ASSERT_EQUAL( static_cast<int>( Event::NONE ), 168 - static_cast<int>( notifier::decideAutoLock( st, 60000, 5000 ) ) ); 169 - // Countdown again → fires fresh. 170 - TEST_ASSERT_EQUAL( static_cast<int>( Event::AutoLockCountdown ), 171 - static_cast<int>( notifier::decideAutoLock( st, 4000, 5000 ) ) ); 172 - } 173 - 174 - // --------------------------------------------------------------------------- 175 - // OTA applied 176 - // --------------------------------------------------------------------------- 177 - static void test_ota_applied_one_shot( void ) { 178 - notifier::OtaAppliedState st; 179 - TEST_ASSERT_EQUAL( static_cast<int>( Event::NONE ), 180 - static_cast<int>( notifier::decideOtaApplied( st, false ) ) ); 181 - TEST_ASSERT_EQUAL( static_cast<int>( Event::OtaApplied ), 182 - static_cast<int>( notifier::decideOtaApplied( st, true ) ) ); 183 - TEST_ASSERT_EQUAL( static_cast<int>( Event::NONE ), 184 - static_cast<int>( notifier::decideOtaApplied( st, true ) ) ); 185 - } 186 - 187 - // --------------------------------------------------------------------------- 188 - void setUp() {} 189 - void tearDown() {} 190 - 191 - int main( int, char** ) { 192 - UNITY_BEGIN(); 193 - RUN_TEST( test_battery_prime_does_not_fire ); 194 - RUN_TEST( test_battery_low_fires_once_then_holds ); 195 - RUN_TEST( test_battery_low_hysteresis_19_21_no_spam ); 196 - RUN_TEST( test_battery_low_rearms_after_exit_threshold ); 197 - RUN_TEST( test_battery_critical_fires_once_per_boot ); 198 - RUN_TEST( test_battery_charging_edge ); 199 - RUN_TEST( test_battery_full_once_per_session ); 200 - RUN_TEST( test_ble_host_edges ); 201 - RUN_TEST( test_admin_client_fires_on_increase ); 202 - RUN_TEST( test_admin_idle_warn_then_timeout ); 203 - RUN_TEST( test_admin_idle_activity_rearm ); 204 - RUN_TEST( test_auto_lock_fires_once_rearms_when_far ); 205 - RUN_TEST( test_ota_applied_one_shot ); 206 - return UNITY_END(); 207 - }
-144
test/test_totp/test_totp.cpp
··· 1 - // Kleidos — Native test: TOTP Generator (RFC 6238 test vectors) 2 - 3 - #include <cstdint> 4 - #include <cstring> 5 - 6 - #include <unity.h> 7 - 8 - // millis() stub required because HoldButton.cpp is linked in native env 9 - static uint32_t fakeMillis = 0; 10 - extern "C" uint32_t millis() { 11 - return fakeMillis; 12 - } 13 - 14 - #include "totp/totp_generator.h" 15 - 16 - // RFC 6238 Appendix B test secret (ASCII "12345678901234567890" = 20 bytes) 17 - static const uint8_t RFC_SECRET[] = { 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x30, 18 - 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x30 }; 19 - static constexpr size_t RFC_SECRET_LEN = 20; 20 - 21 - // --------------------------------------------------------------------------- 22 - // RFC 6238 test vectors (SHA1 only) 23 - // --------------------------------------------------------------------------- 24 - void test_totp_sha1_time59() { 25 - uint32_t code = TotpGenerator::generate( RFC_SECRET, RFC_SECRET_LEN, 59 ); 26 - TEST_ASSERT_EQUAL_UINT32( 287082, code ); 27 - } 28 - 29 - void test_totp_sha1_time1111111109() { 30 - uint32_t code = TotpGenerator::generate( RFC_SECRET, RFC_SECRET_LEN, 1111111109ULL ); 31 - TEST_ASSERT_EQUAL_UINT32( 81804, code ); 32 - } 33 - 34 - void test_totp_sha1_time1111111111() { 35 - uint32_t code = TotpGenerator::generate( RFC_SECRET, RFC_SECRET_LEN, 1111111111ULL ); 36 - TEST_ASSERT_EQUAL_UINT32( 50471, code ); 37 - } 38 - 39 - void test_totp_sha1_time1234567890() { 40 - uint32_t code = TotpGenerator::generate( RFC_SECRET, RFC_SECRET_LEN, 1234567890ULL ); 41 - TEST_ASSERT_EQUAL_UINT32( 5924, code ); 42 - } 43 - 44 - void test_totp_sha1_time2000000000() { 45 - uint32_t code = TotpGenerator::generate( RFC_SECRET, RFC_SECRET_LEN, 2000000000ULL ); 46 - TEST_ASSERT_EQUAL_UINT32( 279037, code ); 47 - } 48 - 49 - void test_totp_sha1_time20000000000() { 50 - uint32_t code = TotpGenerator::generate( RFC_SECRET, RFC_SECRET_LEN, 20000000000ULL ); 51 - TEST_ASSERT_EQUAL_UINT32( 353130, code ); 52 - } 53 - 54 - // --------------------------------------------------------------------------- 55 - // Base32 decode tests 56 - // --------------------------------------------------------------------------- 57 - void test_base32_decode_standard() { 58 - uint8_t out[20]; 59 - // "JBSWY3DPEHPK3PXP" decodes to "Hello!\xDE\xAD\xBE\xEF" 60 - // Actually: JBSWY3DPEHPK3PXP → 48656c6c6f21deadbeef (but check carefully) 61 - // JBSWY3DP → "Hello!" is wrong, let's use known vectors: 62 - // "MFRGG===" decodes to "abc" 63 - size_t len = TotpGenerator::base32Decode( "MFRGG", 5, out, sizeof( out ) ); 64 - TEST_ASSERT_EQUAL( 3, len ); 65 - TEST_ASSERT_EQUAL( 0x61, out[0] ); // 'a' 66 - TEST_ASSERT_EQUAL( 0x62, out[1] ); // 'b' 67 - TEST_ASSERT_EQUAL( 0x63, out[2] ); // 'c' 68 - } 69 - 70 - void test_base32_decode_with_padding() { 71 - uint8_t out[20]; 72 - size_t len = TotpGenerator::base32Decode( "MFRGG===", 8, out, sizeof( out ) ); 73 - TEST_ASSERT_EQUAL( 3, len ); 74 - TEST_ASSERT_EQUAL( 'a', out[0] ); 75 - TEST_ASSERT_EQUAL( 'b', out[1] ); 76 - TEST_ASSERT_EQUAL( 'c', out[2] ); 77 - } 78 - 79 - void test_base32_decode_lowercase() { 80 - uint8_t out[20]; 81 - size_t len = TotpGenerator::base32Decode( "mfrgg", 5, out, sizeof( out ) ); 82 - TEST_ASSERT_EQUAL( 3, len ); 83 - TEST_ASSERT_EQUAL( 'a', out[0] ); 84 - } 85 - 86 - void test_base32_decode_empty() { 87 - uint8_t out[4]; 88 - size_t len = TotpGenerator::base32Decode( "", 0, out, sizeof( out ) ); 89 - TEST_ASSERT_EQUAL( 0, len ); 90 - } 91 - 92 - void test_base32_decode_invalid_char() { 93 - uint8_t out[20]; 94 - size_t len = TotpGenerator::base32Decode( "MFRG1", 5, out, sizeof( out ) ); 95 - TEST_ASSERT_EQUAL( 0, len ); // '1' is invalid in base32 96 - } 97 - 98 - void test_base32_decode_jbswy3dpehpk3pxp() { 99 - // "JBSWY3DPEHPK3PXP" → 48 65 6c 6c 6f 21 de ad be ef 100 - uint8_t out[20]; 101 - size_t len = TotpGenerator::base32Decode( "JBSWY3DPEHPK3PXP", 16, out, sizeof( out ) ); 102 - TEST_ASSERT_EQUAL( 10, len ); 103 - TEST_ASSERT_EQUAL( 0x48, out[0] ); // 'H' 104 - TEST_ASSERT_EQUAL( 0x65, out[1] ); // 'e' 105 - TEST_ASSERT_EQUAL( 0x6c, out[2] ); // 'l' 106 - TEST_ASSERT_EQUAL( 0x6c, out[3] ); // 'l' 107 - TEST_ASSERT_EQUAL( 0x6f, out[4] ); // 'o' 108 - TEST_ASSERT_EQUAL( 0x21, out[5] ); // '!' 109 - TEST_ASSERT_EQUAL( 0xde, out[6] ); 110 - TEST_ASSERT_EQUAL( 0xad, out[7] ); 111 - TEST_ASSERT_EQUAL( 0xbe, out[8] ); 112 - TEST_ASSERT_EQUAL( 0xef, out[9] ); 113 - } 114 - 115 - // --------------------------------------------------------------------------- 116 - // Remaining seconds 117 - // --------------------------------------------------------------------------- 118 - void test_remaining_at_boundary() { 119 - TEST_ASSERT_EQUAL( 1, TotpGenerator::remainingSeconds( 29 ) ); 120 - TEST_ASSERT_EQUAL( 30, TotpGenerator::remainingSeconds( 30 ) ); 121 - TEST_ASSERT_EQUAL( 30, TotpGenerator::remainingSeconds( 0 ) ); 122 - TEST_ASSERT_EQUAL( 15, TotpGenerator::remainingSeconds( 15 ) ); 123 - } 124 - 125 - // --------------------------------------------------------------------------- 126 - // Runner 127 - // --------------------------------------------------------------------------- 128 - int main( int argc, char** argv ) { 129 - UNITY_BEGIN(); 130 - RUN_TEST( test_totp_sha1_time59 ); 131 - RUN_TEST( test_totp_sha1_time1111111109 ); 132 - RUN_TEST( test_totp_sha1_time1111111111 ); 133 - RUN_TEST( test_totp_sha1_time1234567890 ); 134 - RUN_TEST( test_totp_sha1_time2000000000 ); 135 - RUN_TEST( test_totp_sha1_time20000000000 ); 136 - RUN_TEST( test_base32_decode_standard ); 137 - RUN_TEST( test_base32_decode_with_padding ); 138 - RUN_TEST( test_base32_decode_lowercase ); 139 - RUN_TEST( test_base32_decode_empty ); 140 - RUN_TEST( test_base32_decode_invalid_char ); 141 - RUN_TEST( test_base32_decode_jbswy3dpehpk3pxp ); 142 - RUN_TEST( test_remaining_at_boundary ); 143 - return UNITY_END(); 144 - }
-248
test/test_totp_extended/test_totp_extended.cpp
··· 1 - // Kleidos — Native test: TOTP Generator extended 2 - // Tests non-standard periods, 8-digit codes, edge cases, 3 - // and base32 decode corner cases beyond the base test suite. 4 - 5 - #include <cstdint> 6 - #include <cstring> 7 - 8 - #include <unity.h> 9 - 10 - // millis() stub 11 - static uint32_t fakeMillis = 0; 12 - extern "C" uint32_t millis() { 13 - return fakeMillis; 14 - } 15 - 16 - #include "totp/totp_generator.h" 17 - 18 - // RFC 6238 test secret "12345678901234567890" 19 - static const uint8_t RFC_SECRET[] = { 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x30, 20 - 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x30 }; 21 - static constexpr size_t RFC_SECRET_LEN = 20; 22 - 23 - // --------------------------------------------------------------------------- 24 - // Period boundary tests — verify counter increments correctly 25 - // --------------------------------------------------------------------------- 26 - void test_code_changes_at_period_boundary() { 27 - // At t=29 and t=30 the counter changes: 29/30=0, 30/30=1 28 - uint32_t code_29 = TotpGenerator::generate( RFC_SECRET, RFC_SECRET_LEN, 29 ); 29 - uint32_t code_30 = TotpGenerator::generate( RFC_SECRET, RFC_SECRET_LEN, 30 ); 30 - TEST_ASSERT_NOT_EQUAL( code_29, code_30 ); 31 - } 32 - 33 - void test_code_stable_within_period() { 34 - // t=0..29 should all have the same counter (0) 35 - uint32_t code_0 = TotpGenerator::generate( RFC_SECRET, RFC_SECRET_LEN, 0 ); 36 - uint32_t code_15 = TotpGenerator::generate( RFC_SECRET, RFC_SECRET_LEN, 15 ); 37 - uint32_t code_29 = TotpGenerator::generate( RFC_SECRET, RFC_SECRET_LEN, 29 ); 38 - TEST_ASSERT_EQUAL_UINT32( code_0, code_15 ); 39 - TEST_ASSERT_EQUAL_UINT32( code_0, code_29 ); 40 - } 41 - 42 - void test_code_at_time_zero() { 43 - uint32_t code = TotpGenerator::generate( RFC_SECRET, RFC_SECRET_LEN, 0 ); 44 - // Counter = 0/30 = 0 → deterministic result 45 - TEST_ASSERT_TRUE( code < 1000000 ); 46 - } 47 - 48 - // --------------------------------------------------------------------------- 49 - // Code range validation 50 - // --------------------------------------------------------------------------- 51 - void test_code_always_6_digits_or_less() { 52 - // Generate many codes and verify they're all < 1,000,000 53 - uint64_t testTimes[] = { 0, 1, 29, 30, 59, 60, 100, 1000, 999999, 1000000, UINT32_MAX }; 54 - for ( auto t : testTimes ) { 55 - uint32_t code = TotpGenerator::generate( RFC_SECRET, RFC_SECRET_LEN, t ); 56 - TEST_ASSERT_TRUE( code < 1000000 ); 57 - } 58 - } 59 - 60 - // --------------------------------------------------------------------------- 61 - // Different secrets produce different codes 62 - // --------------------------------------------------------------------------- 63 - void test_different_secrets_different_codes() { 64 - uint8_t secret2[] = { 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 65 - 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0x10, 0x11, 0x12, 0x13 }; 66 - uint32_t code1 = TotpGenerator::generate( RFC_SECRET, RFC_SECRET_LEN, 59 ); 67 - uint32_t code2 = TotpGenerator::generate( secret2, 20, 59 ); 68 - TEST_ASSERT_NOT_EQUAL( code1, code2 ); 69 - } 70 - 71 - // --------------------------------------------------------------------------- 72 - // Short and long secrets 73 - // --------------------------------------------------------------------------- 74 - void test_minimum_secret_length() { 75 - // 1-byte secret — should still produce a valid code 76 - uint8_t shortSecret[] = { 0x42 }; 77 - uint32_t code = TotpGenerator::generate( shortSecret, 1, 59 ); 78 - TEST_ASSERT_TRUE( code < 1000000 ); 79 - } 80 - 81 - void test_max_secret_length() { 82 - // 64-byte secret (TOTP_SECRET_MAX_LEN) 83 - uint8_t longSecret[64]; 84 - memset( longSecret, 0xAB, sizeof( longSecret ) ); 85 - uint32_t code = TotpGenerator::generate( longSecret, 64, 59 ); 86 - TEST_ASSERT_TRUE( code < 1000000 ); 87 - } 88 - 89 - // --------------------------------------------------------------------------- 90 - // Large timestamps (far future) 91 - // --------------------------------------------------------------------------- 92 - void test_large_timestamp() { 93 - // Year 2100+ timestamps 94 - uint32_t code = TotpGenerator::generate( RFC_SECRET, RFC_SECRET_LEN, 4102444800ULL ); 95 - TEST_ASSERT_TRUE( code < 1000000 ); 96 - } 97 - 98 - // --------------------------------------------------------------------------- 99 - // remainingSeconds edge cases 100 - // --------------------------------------------------------------------------- 101 - void test_remaining_at_exact_period() { 102 - // t=0: elapsed=0, remaining=30 103 - TEST_ASSERT_EQUAL( 30, TotpGenerator::remainingSeconds( 0 ) ); 104 - } 105 - 106 - void test_remaining_at_1_second() { 107 - // t=1: elapsed=1, remaining=29 108 - TEST_ASSERT_EQUAL( 29, TotpGenerator::remainingSeconds( 1 ) ); 109 - } 110 - 111 - void test_remaining_at_29() { 112 - // t=29: elapsed=29, remaining=1 113 - TEST_ASSERT_EQUAL( 1, TotpGenerator::remainingSeconds( 29 ) ); 114 - } 115 - 116 - void test_remaining_at_30() { 117 - // t=30: elapsed=0 (new period), remaining=30 118 - TEST_ASSERT_EQUAL( 30, TotpGenerator::remainingSeconds( 30 ) ); 119 - } 120 - 121 - void test_remaining_large_timestamp() { 122 - // t=1234567890 → 1234567890 % 30 = 0 → remaining = 30 123 - TEST_ASSERT_EQUAL( 30, TotpGenerator::remainingSeconds( 1234567890 ) ); 124 - } 125 - 126 - // --------------------------------------------------------------------------- 127 - // Base32 decode extended tests 128 - // --------------------------------------------------------------------------- 129 - void test_base32_decode_single_char() { 130 - // "A" in base32 → 5 bits, not enough for 1 byte → 0 output bytes 131 - uint8_t out[4] = {}; 132 - size_t len = TotpGenerator::base32Decode( "A", 1, out, sizeof( out ) ); 133 - TEST_ASSERT_EQUAL( 0, len ); 134 - } 135 - 136 - void test_base32_decode_two_chars() { 137 - // "MY" → M=12, Y=24 → 0b01100_11000 → 0x0C<<3 | 0x18>>2 = 0x63 = 'c' 138 - // Actually: M=12(01100), Y=24(11000) → 01100_11000 → byte: 01100110 = 0x66 = 'f' 139 - // Let's just check it produces 1 byte 140 - uint8_t out[4] = {}; 141 - size_t len = TotpGenerator::base32Decode( "MY", 2, out, sizeof( out ) ); 142 - TEST_ASSERT_EQUAL( 1, len ); 143 - } 144 - 145 - void test_base32_decode_full_block() { 146 - // "MFRGGZDF" → 5 bytes: "abcde" (well-known) 147 - // M=12,F=5,R=17,G=6,G=6,Z=25,D=3,F=5 148 - // Actually let's use a known vector: "GEZDGNBV" → "1234" 149 - // G=6,E=4,Z=25,D=3,G=6,N=13,B=1,V=21 150 - // Hmm, let's just use "ME" → 'a' + partial 151 - // Better: ORSXG5A= → "test" 152 - uint8_t out[10] = {}; 153 - size_t len = TotpGenerator::base32Decode( "ORSXG5A", 7, out, sizeof( out ) ); 154 - TEST_ASSERT_EQUAL( 4, len ); 155 - TEST_ASSERT_EQUAL( 't', out[0] ); 156 - TEST_ASSERT_EQUAL( 'e', out[1] ); 157 - TEST_ASSERT_EQUAL( 's', out[2] ); 158 - TEST_ASSERT_EQUAL( 't', out[3] ); 159 - } 160 - 161 - void test_base32_decode_mixed_case() { 162 - uint8_t out1[10] = {}, out2[10] = {}; 163 - size_t len1 = TotpGenerator::base32Decode( "ORSXG5A", 7, out1, sizeof( out1 ) ); 164 - size_t len2 = TotpGenerator::base32Decode( "orsxg5a", 7, out2, sizeof( out2 ) ); 165 - TEST_ASSERT_EQUAL( len1, len2 ); 166 - TEST_ASSERT_EQUAL_MEMORY( out1, out2, len1 ); 167 - } 168 - 169 - void test_base32_buffer_too_small() { 170 - // Buffer of 1 byte, but "MFRGG" decodes to 3 bytes → should fail (overflow) 171 - uint8_t out[1] = {}; 172 - size_t len = TotpGenerator::base32Decode( "MFRGG", 5, out, sizeof( out ) ); 173 - TEST_ASSERT_EQUAL( 0, len ); // overflow protection 174 - } 175 - 176 - void test_base32_decode_with_number_1() { 177 - // '1' is not valid base32 (valid: A-Z, 2-7) 178 - uint8_t out[4] = {}; 179 - size_t len = TotpGenerator::base32Decode( "M1RGG", 5, out, sizeof( out ) ); 180 - TEST_ASSERT_EQUAL( 0, len ); 181 - } 182 - 183 - void test_base32_decode_with_number_0() { 184 - uint8_t out[4] = {}; 185 - size_t len = TotpGenerator::base32Decode( "M0RGG", 5, out, sizeof( out ) ); 186 - TEST_ASSERT_EQUAL( 0, len ); 187 - } 188 - 189 - void test_base32_decode_with_number_8() { 190 - uint8_t out[4] = {}; 191 - size_t len = TotpGenerator::base32Decode( "M8RGG", 5, out, sizeof( out ) ); 192 - TEST_ASSERT_EQUAL( 0, len ); 193 - } 194 - 195 - void test_base32_decode_null_input() { 196 - uint8_t out[4] = {}; 197 - size_t len = TotpGenerator::base32Decode( nullptr, 0, out, sizeof( out ) ); 198 - TEST_ASSERT_EQUAL( 0, len ); 199 - } 200 - 201 - void test_base32_decode_null_output() { 202 - size_t len = TotpGenerator::base32Decode( "MFRGG", 5, nullptr, 0 ); 203 - TEST_ASSERT_EQUAL( 0, len ); 204 - } 205 - 206 - // --------------------------------------------------------------------------- 207 - // Runner 208 - // --------------------------------------------------------------------------- 209 - int main( int argc, char** argv ) { 210 - UNITY_BEGIN(); 211 - 212 - // Period boundary 213 - RUN_TEST( test_code_changes_at_period_boundary ); 214 - RUN_TEST( test_code_stable_within_period ); 215 - RUN_TEST( test_code_at_time_zero ); 216 - 217 - // Code range 218 - RUN_TEST( test_code_always_6_digits_or_less ); 219 - 220 - // Secret variations 221 - RUN_TEST( test_different_secrets_different_codes ); 222 - RUN_TEST( test_minimum_secret_length ); 223 - RUN_TEST( test_max_secret_length ); 224 - 225 - // Large timestamps 226 - RUN_TEST( test_large_timestamp ); 227 - 228 - // remainingSeconds 229 - RUN_TEST( test_remaining_at_exact_period ); 230 - RUN_TEST( test_remaining_at_1_second ); 231 - RUN_TEST( test_remaining_at_29 ); 232 - RUN_TEST( test_remaining_at_30 ); 233 - RUN_TEST( test_remaining_large_timestamp ); 234 - 235 - // Base32 extended 236 - RUN_TEST( test_base32_decode_single_char ); 237 - RUN_TEST( test_base32_decode_two_chars ); 238 - RUN_TEST( test_base32_decode_full_block ); 239 - RUN_TEST( test_base32_decode_mixed_case ); 240 - RUN_TEST( test_base32_buffer_too_small ); 241 - RUN_TEST( test_base32_decode_with_number_1 ); 242 - RUN_TEST( test_base32_decode_with_number_0 ); 243 - RUN_TEST( test_base32_decode_with_number_8 ); 244 - RUN_TEST( test_base32_decode_null_input ); 245 - RUN_TEST( test_base32_decode_null_output ); 246 - 247 - return UNITY_END(); 248 - }
+93 -23
test/test_vault_detail_logic/test_vault_detail_logic.cpp test/states/test_cred_detail_logic/test_cred_detail_logic.cpp
··· 1 - // Kleidos — Native tests for the credential detail sub-state FSM. 2 - // 3 - // Exercises CredDetailLogic in isolation: open, reveal timeout, delete 4 - // confirm/cancel, and back transitions. 1 + // Kleidos — Native test: CredDetailLogic (credential detail sub-state FSM) 2 + // Build: pio test -e native -f states/test_cred_detail_logic 3 + 4 + // --- File header ----------------------------------------------------------- 5 5 6 + // Group 1: module under test 6 7 #include "states/cred_detail_logic.h" 7 8 9 + // Group 3: stdlib + Unity 8 10 #include <cstdint> 9 11 10 12 #include <unity.h> 11 13 12 - extern "C" uint32_t millis() { 13 - return 0; 14 - } 14 + // --- setUp / tearDown ------------------------------------------------------ 15 + 16 + void setUp() { /* intentionally empty */ } 17 + 18 + void tearDown() { /* intentionally empty */ } 19 + 20 + // --- Tests ----------------------------------------------------------------- 15 21 16 - void test_initial_step_is_list() { 22 + static void test_initial_step_is_list() { 23 + // ARRANGE + ACT 17 24 CredDetailLogic l; 25 + 26 + // ASSERT 18 27 TEST_ASSERT_EQUAL( static_cast<int>( CredDetailStep::LIST ), static_cast<int>( l.step() ) ); 19 28 TEST_ASSERT_EQUAL( 0, l.credId() ); 20 29 } 21 30 22 - void test_open_moves_to_detail() { 31 + static void test_open_moves_to_detail() { 32 + // ARRANGE 23 33 CredDetailLogic l; 24 - auto r = l.open( 7 ); 34 + 35 + // ACT 36 + auto r = l.open( 7 ); 37 + 38 + // ASSERT 25 39 TEST_ASSERT_EQUAL( static_cast<int>( CredDetailResult::CONTINUE ), static_cast<int>( r ) ); 26 40 TEST_ASSERT_EQUAL( static_cast<int>( CredDetailStep::DETAIL ), static_cast<int>( l.step() ) ); 27 41 TEST_ASSERT_EQUAL( 7, l.credId() ); 28 42 } 29 43 30 - void test_open_ignored_outside_list() { 44 + static void test_open_ignored_outside_list() { 45 + // ARRANGE 31 46 CredDetailLogic l; 32 47 l.open( 3 ); 33 - l.open( 9 ); // no-op 48 + 49 + // ACT 50 + l.open( 9 ); // no-op from DETAIL step 51 + 52 + // ASSERT 34 53 TEST_ASSERT_EQUAL( 3, l.credId() ); 35 54 } 36 55 37 - void test_begin_reveal_from_detail() { 56 + static void test_begin_reveal_from_detail() { 57 + // ARRANGE 38 58 CredDetailLogic l; 39 59 l.open( 1 ); 60 + 61 + // ACT 40 62 l.beginReveal( 1000, 3000 ); 63 + 64 + // ASSERT 41 65 TEST_ASSERT_EQUAL( static_cast<int>( CredDetailStep::REVEAL ), static_cast<int>( l.step() ) ); 42 66 TEST_ASSERT_TRUE( l.revealActive() ); 43 67 TEST_ASSERT_EQUAL( 3000, l.revealRemaining( 1000 ) ); 44 68 TEST_ASSERT_EQUAL( 1500, l.revealRemaining( 2500 ) ); 45 69 } 46 70 47 - void test_reveal_timeout_returns_to_detail() { 71 + static void test_reveal_timeout_returns_to_detail() { 72 + // ARRANGE 48 73 CredDetailLogic l; 49 74 l.open( 2 ); 50 75 l.beginReveal( 0, 3000 ); 76 + 77 + // ACT 51 78 l.tick( 1000 ); 79 + 80 + // ASSERT (before timeout boundary) 52 81 TEST_ASSERT_EQUAL( static_cast<int>( CredDetailStep::REVEAL ), static_cast<int>( l.step() ) ); 53 - // Boundary: exactly at timeout. 82 + 83 + // ACT (at timeout boundary) 54 84 l.tick( 3000 ); 85 + 86 + // ASSERT (after timeout) 55 87 TEST_ASSERT_EQUAL( static_cast<int>( CredDetailStep::DETAIL ), static_cast<int>( l.step() ) ); 56 88 TEST_ASSERT_FALSE( l.revealActive() ); 57 89 TEST_ASSERT_EQUAL( 0, l.revealRemaining( 3000 ) ); 58 90 } 59 91 60 - void test_reveal_ignored_outside_detail() { 92 + static void test_reveal_ignored_outside_detail() { 93 + // ARRANGE 61 94 CredDetailLogic l; 62 - // No open — still LIST. 95 + // Still at LIST — no open() called. 96 + 97 + // ACT 63 98 l.beginReveal( 0, 3000 ); 99 + 100 + // ASSERT 64 101 TEST_ASSERT_EQUAL( static_cast<int>( CredDetailStep::LIST ), static_cast<int>( l.step() ) ); 65 102 } 66 103 67 - void test_delete_confirm_cancel_returns_to_detail() { 104 + static void test_delete_confirm_cancel_returns_to_detail() { 105 + // ARRANGE 68 106 CredDetailLogic l; 69 107 l.open( 4 ); 70 108 l.requestDelete(); 109 + 110 + // ASSERT (delete confirm step entered) 71 111 TEST_ASSERT_EQUAL( static_cast<int>( CredDetailStep::DeleteConfirm ), 72 112 static_cast<int>( l.step() ) ); 113 + 114 + // ACT 73 115 auto r = l.confirmDelete( false ); 116 + 117 + // ASSERT 74 118 TEST_ASSERT_EQUAL( static_cast<int>( CredDetailResult::CONTINUE ), static_cast<int>( r ) ); 75 119 TEST_ASSERT_EQUAL( static_cast<int>( CredDetailStep::DETAIL ), static_cast<int>( l.step() ) ); 76 120 } 77 121 78 - void test_delete_confirmed_returns_to_list_and_signals_deleted() { 122 + static void test_delete_confirmed_returns_to_list_and_signals_deleted() { 123 + // ARRANGE 79 124 CredDetailLogic l; 80 125 l.open( 5 ); 81 126 l.requestDelete(); 127 + 128 + // ACT 82 129 auto r = l.confirmDelete( true ); 130 + 131 + // ASSERT 83 132 TEST_ASSERT_EQUAL( static_cast<int>( CredDetailResult::DELETED ), static_cast<int>( r ) ); 84 133 TEST_ASSERT_EQUAL( static_cast<int>( CredDetailStep::LIST ), static_cast<int>( l.step() ) ); 85 134 } 86 135 87 - void test_back_from_detail_closes() { 136 + static void test_back_from_detail_closes() { 137 + // ARRANGE 88 138 CredDetailLogic l; 89 139 l.open( 6 ); 140 + 141 + // ACT 90 142 auto r = l.back(); 143 + 144 + // ASSERT 91 145 TEST_ASSERT_EQUAL( static_cast<int>( CredDetailResult::CLOSED ), static_cast<int>( r ) ); 92 146 TEST_ASSERT_EQUAL( static_cast<int>( CredDetailStep::LIST ), static_cast<int>( l.step() ) ); 93 147 TEST_ASSERT_EQUAL( 0, l.credId() ); 94 148 } 95 149 96 - void test_back_ignored_during_delete_confirm() { 150 + static void test_back_ignored_during_delete_confirm() { 151 + // ARRANGE 97 152 CredDetailLogic l; 98 153 l.open( 8 ); 99 154 l.requestDelete(); 155 + 156 + // ACT 100 157 auto r = l.back(); 158 + 159 + // ASSERT 101 160 TEST_ASSERT_EQUAL( static_cast<int>( CredDetailResult::CONTINUE ), static_cast<int>( r ) ); 102 161 TEST_ASSERT_EQUAL( static_cast<int>( CredDetailStep::DeleteConfirm ), 103 162 static_cast<int>( l.step() ) ); 104 163 } 105 164 106 - void test_reveal_then_delete_then_confirm_transitions() { 165 + static void test_reveal_then_delete_then_confirm_transitions() { 166 + // ARRANGE 107 167 CredDetailLogic l; 108 168 l.open( 10 ); 109 169 l.beginReveal( 0, 3000 ); 170 + 171 + // ACT 110 172 l.requestDelete(); // reveal → delete_confirm directly 173 + 174 + // ASSERT 111 175 TEST_ASSERT_EQUAL( static_cast<int>( CredDetailStep::DeleteConfirm ), 112 176 static_cast<int>( l.step() ) ); 177 + 178 + // ACT 113 179 l.confirmDelete( true ); 180 + 181 + // ASSERT 114 182 TEST_ASSERT_EQUAL( static_cast<int>( CredDetailStep::LIST ), static_cast<int>( l.step() ) ); 115 183 } 116 184 117 - int main( int, char** ) { 185 + // --- main() ---------------------------------------------------------------- 186 + 187 + int main( int /*argc*/, char** /*argv*/ ) { 118 188 UNITY_BEGIN(); 119 189 RUN_TEST( test_initial_step_is_list ); 120 190 RUN_TEST( test_open_moves_to_detail );
-315
test/test_vault_json/test_vault_json.cpp
··· 1 - // Kleidos — Native test: Vault JSON serialization 2 - // Tests credential JSON serialization/deserialization without hardware. 3 - // Uses ArduinoJson directly since VaultStore depends on LittleFS/mbedTLS. 4 - 5 - #include <ArduinoJson.h> 6 - 7 - #include <cstring> 8 - 9 - #include <unity.h> 10 - 11 - // millis() stub required because HoldButton.cpp is linked in native env 12 - static uint32_t fakeMillis = 0; 13 - extern "C" uint32_t millis() { 14 - return fakeMillis; 15 - } 16 - 17 - // Replicate credential constants from VaultStore.h 18 - static constexpr size_t MAX_CREDENTIALS = 50; // default for native tests 19 - static constexpr size_t MAX_NAME_LEN = 32; 20 - static constexpr size_t MAX_USER_LEN = 64; 21 - static constexpr size_t MAX_PASS_LEN = 128; 22 - static constexpr size_t MAX_URL_LEN = 128; 23 - 24 - struct TestCredential { 25 - char name[MAX_NAME_LEN + 1]; 26 - char user[MAX_USER_LEN + 1]; 27 - char pass[MAX_PASS_LEN + 1]; 28 - char url[MAX_URL_LEN + 1]; 29 - }; 30 - 31 - // --------------------------------------------------------------------------- 32 - // Tests 33 - // --------------------------------------------------------------------------- 34 - void test_credential_serialize() { 35 - TestCredential c = {}; 36 - strncpy( c.name, "GitHub", sizeof( c.name ) - 1 ); 37 - strncpy( c.user, "user@test.com", sizeof( c.user ) - 1 ); 38 - strncpy( c.pass, "s3cret!Pass", sizeof( c.pass ) - 1 ); 39 - strncpy( c.url, "https://github.com", sizeof( c.url ) - 1 ); 40 - 41 - JsonDocument doc; 42 - doc["name"] = c.name; 43 - doc["user"] = c.user; 44 - doc["pass"] = c.pass; 45 - doc["url"] = c.url; 46 - 47 - char buf[512]; 48 - size_t len = serializeJson( doc, buf, sizeof( buf ) ); 49 - TEST_ASSERT_GREATER_THAN( 0, len ); 50 - 51 - // Deserialize back 52 - JsonDocument doc2; 53 - DeserializationError err = deserializeJson( doc2, buf, len ); 54 - TEST_ASSERT_TRUE( err == DeserializationError::Ok ); 55 - 56 - TEST_ASSERT_EQUAL_STRING( "GitHub", doc2["name"] ); 57 - TEST_ASSERT_EQUAL_STRING( "user@test.com", doc2["user"] ); 58 - TEST_ASSERT_EQUAL_STRING( "s3cret!Pass", doc2["pass"] ); 59 - TEST_ASSERT_EQUAL_STRING( "https://github.com", doc2["url"] ); 60 - } 61 - 62 - void test_max_credential_name_length() { 63 - // 32 chars exactly should work 64 - char longName[MAX_NAME_LEN + 1]; 65 - memset( longName, 'A', MAX_NAME_LEN ); 66 - longName[MAX_NAME_LEN] = '\0'; 67 - 68 - JsonDocument doc; 69 - doc["name"] = longName; 70 - doc["user"] = ""; 71 - doc["pass"] = ""; 72 - doc["url"] = ""; 73 - 74 - char buf[512]; 75 - size_t len = serializeJson( doc, buf, sizeof( buf ) ); 76 - TEST_ASSERT_GREATER_THAN( 0, len ); 77 - 78 - JsonDocument doc2; 79 - deserializeJson( doc2, buf, len ); 80 - TEST_ASSERT_EQUAL_STRING( longName, doc2["name"] ); 81 - } 82 - 83 - void test_empty_fields() { 84 - JsonDocument doc; 85 - doc["name"] = ""; 86 - doc["user"] = ""; 87 - doc["pass"] = ""; 88 - doc["url"] = ""; 89 - 90 - char buf[256]; 91 - size_t len = serializeJson( doc, buf, sizeof( buf ) ); 92 - 93 - JsonDocument doc2; 94 - DeserializationError err = deserializeJson( doc2, buf, len ); 95 - TEST_ASSERT_TRUE( err == DeserializationError::Ok ); 96 - 97 - TEST_ASSERT_EQUAL_STRING( "", doc2["name"] ); 98 - TEST_ASSERT_EQUAL_STRING( "", doc2["user"] ); 99 - TEST_ASSERT_EQUAL_STRING( "", doc2["pass"] ); 100 - TEST_ASSERT_EQUAL_STRING( "", doc2["url"] ); 101 - } 102 - 103 - void test_special_characters() { 104 - JsonDocument doc; 105 - doc["name"] = "Test \"Quotes\" & <Tags>"; 106 - doc["user"] = "user+tag@example.com"; 107 - doc["pass"] = "p@$$w0rd!#%^&*(){}[]"; 108 - doc["url"] = "https://example.com/path?q=1&b=2"; 109 - 110 - char buf[512]; 111 - size_t len = serializeJson( doc, buf, sizeof( buf ) ); 112 - 113 - JsonDocument doc2; 114 - DeserializationError err = deserializeJson( doc2, buf, len ); 115 - TEST_ASSERT_TRUE( err == DeserializationError::Ok ); 116 - 117 - TEST_ASSERT_EQUAL_STRING( "Test \"Quotes\" & <Tags>", doc2["name"] ); 118 - TEST_ASSERT_EQUAL_STRING( "p@$$w0rd!#%^&*(){}[]", doc2["pass"] ); 119 - } 120 - 121 - void test_unicode_utf8() { 122 - JsonDocument doc; 123 - doc["name"] = "Contraseña"; 124 - doc["user"] = "用户@例子.com"; 125 - doc["pass"] = "🔑secret"; 126 - doc["url"] = ""; 127 - 128 - char buf[512]; 129 - size_t len = serializeJson( doc, buf, sizeof( buf ) ); 130 - 131 - JsonDocument doc2; 132 - DeserializationError err = deserializeJson( doc2, buf, len ); 133 - TEST_ASSERT_TRUE( err == DeserializationError::Ok ); 134 - TEST_ASSERT_EQUAL_STRING( "Contraseña", doc2["name"] ); 135 - } 136 - 137 - void test_missing_optional_fields() { 138 - // Simulate receiving JSON with only "name" — other fields use default 139 - const char* json = "{\"name\":\"OnlyName\"}"; 140 - 141 - JsonDocument doc; 142 - deserializeJson( doc, json ); 143 - 144 - TestCredential c = {}; 145 - strncpy( c.name, doc["name"] | "", sizeof( c.name ) - 1 ); 146 - strncpy( c.user, doc["user"] | "", sizeof( c.user ) - 1 ); 147 - strncpy( c.pass, doc["pass"] | "", sizeof( c.pass ) - 1 ); 148 - strncpy( c.url, doc["url"] | "", sizeof( c.url ) - 1 ); 149 - 150 - TEST_ASSERT_EQUAL_STRING( "OnlyName", c.name ); 151 - TEST_ASSERT_EQUAL_STRING( "", c.user ); 152 - TEST_ASSERT_EQUAL_STRING( "", c.pass ); 153 - TEST_ASSERT_EQUAL_STRING( "", c.url ); 154 - } 155 - 156 - void test_credential_list_json() { 157 - // Simulate credential list response 158 - JsonDocument doc; 159 - JsonArray arr = doc.to<JsonArray>(); 160 - 161 - for ( int i = 0; i < 5; i++ ) { 162 - JsonObject obj = arr.add<JsonObject>(); 163 - obj["id"] = i; 164 - char name[16]; 165 - snprintf( name, sizeof( name ), "Cred_%d", i ); 166 - obj["name"] = name; 167 - } 168 - 169 - char buf[512]; 170 - size_t len = serializeJson( doc, buf, sizeof( buf ) ); 171 - 172 - JsonDocument doc2; 173 - deserializeJson( doc2, buf, len ); 174 - JsonArray arr2 = doc2.as<JsonArray>(); 175 - TEST_ASSERT_EQUAL( 5, arr2.size() ); 176 - TEST_ASSERT_EQUAL( 0, arr2[0]["id"].as<int>() ); 177 - TEST_ASSERT_EQUAL_STRING( "Cred_0", arr2[0]["name"] ); 178 - TEST_ASSERT_EQUAL( 4, arr2[4]["id"].as<int>() ); 179 - } 180 - 181 - // --------------------------------------------------------------------------- 182 - // Phase 2: TOTP JSON tests 183 - // --------------------------------------------------------------------------- 184 - void test_totp_entry_serialize() { 185 - JsonDocument doc; 186 - doc["name"] = "GitHub TOTP"; 187 - doc["secret"] = "JBSWY3DPEHPK3PXP"; 188 - doc["digits"] = 6; 189 - doc["period"] = 30; 190 - 191 - char buf[256]; 192 - size_t len = serializeJson( doc, buf, sizeof( buf ) ); 193 - TEST_ASSERT_GREATER_THAN( 0, len ); 194 - 195 - JsonDocument doc2; 196 - DeserializationError err = deserializeJson( doc2, buf, len ); 197 - TEST_ASSERT_TRUE( err == DeserializationError::Ok ); 198 - TEST_ASSERT_EQUAL_STRING( "GitHub TOTP", doc2["name"] ); 199 - TEST_ASSERT_EQUAL_STRING( "JBSWY3DPEHPK3PXP", doc2["secret"] ); 200 - TEST_ASSERT_EQUAL( 6, doc2["digits"].as<int>() ); 201 - TEST_ASSERT_EQUAL( 30, doc2["period"].as<int>() ); 202 - } 203 - 204 - void test_totp_8_digits() { 205 - JsonDocument doc; 206 - doc["name"] = "8-digit TOTP"; 207 - doc["secret"] = "MFRGG"; 208 - doc["digits"] = 8; 209 - doc["period"] = 60; 210 - 211 - char buf[256]; 212 - size_t len = serializeJson( doc, buf, sizeof( buf ) ); 213 - 214 - JsonDocument doc2; 215 - deserializeJson( doc2, buf, len ); 216 - TEST_ASSERT_EQUAL( 8, doc2["digits"].as<int>() ); 217 - TEST_ASSERT_EQUAL( 60, doc2["period"].as<int>() ); 218 - } 219 - 220 - void test_max_credentials_array() { 221 - JsonDocument doc; 222 - JsonArray arr = doc.to<JsonArray>(); 223 - 224 - for ( int i = 0; i < (int)MAX_CREDENTIALS; i++ ) { 225 - JsonObject obj = arr.add<JsonObject>(); 226 - char name[16]; 227 - snprintf( name, sizeof( name ), "C%d", i ); 228 - obj["name"] = name; 229 - obj["id"] = i; 230 - } 231 - 232 - char buf[8192]; 233 - size_t len = serializeJson( doc, buf, sizeof( buf ) ); 234 - TEST_ASSERT_GREATER_THAN( 0, len ); 235 - 236 - JsonDocument doc2; 237 - DeserializationError err = deserializeJson( doc2, buf, len ); 238 - TEST_ASSERT_TRUE( err == DeserializationError::Ok ); 239 - TEST_ASSERT_EQUAL( (int)MAX_CREDENTIALS, (int)doc2.as<JsonArray>().size() ); 240 - } 241 - 242 - void test_csv_line_parsing() { 243 - // Simulate CSV import: "name,user,pass,url" 244 - const char* csvLine = "GitHub,john@test.com,MyP@ss!,https://github.com"; 245 - 246 - // Parse CSV by splitting on commas 247 - char line[256]; 248 - strncpy( line, csvLine, sizeof( line ) - 1 ); 249 - line[sizeof( line ) - 1] = '\0'; 250 - 251 - char* fields[4] = {}; 252 - int idx = 0; 253 - char* tok = strtok( line, "," ); 254 - while ( tok && idx < 4 ) { 255 - fields[idx++] = tok; 256 - tok = strtok( nullptr, "," ); 257 - } 258 - 259 - TEST_ASSERT_EQUAL( 4, idx ); 260 - TEST_ASSERT_EQUAL_STRING( "GitHub", fields[0] ); 261 - TEST_ASSERT_EQUAL_STRING( "john@test.com", fields[1] ); 262 - TEST_ASSERT_EQUAL_STRING( "MyP@ss!", fields[2] ); 263 - TEST_ASSERT_EQUAL_STRING( "https://github.com", fields[3] ); 264 - } 265 - 266 - void test_backup_json_format() { 267 - // Simulate backup: array of { filename, data (base64) } 268 - JsonDocument doc; 269 - JsonArray arr = doc.to<JsonArray>(); 270 - 271 - JsonObject f1 = arr.add<JsonObject>(); 272 - f1["file"] = "cred_00.bin"; 273 - f1["data"] = "AQIDBA=="; // base64 of {1,2,3,4} 274 - 275 - JsonObject f2 = arr.add<JsonObject>(); 276 - f2["file"] = "cred_01.bin"; 277 - f2["data"] = "BQYHCA=="; 278 - 279 - char buf[512]; 280 - size_t len = serializeJson( doc, buf, sizeof( buf ) ); 281 - 282 - JsonDocument doc2; 283 - deserializeJson( doc2, buf, len ); 284 - JsonArray arr2 = doc2.as<JsonArray>(); 285 - TEST_ASSERT_EQUAL( 2, arr2.size() ); 286 - TEST_ASSERT_EQUAL_STRING( "cred_00.bin", arr2[0]["file"] ); 287 - } 288 - 289 - void test_invalid_json_returns_error() { 290 - const char* badJson = "{name: missing quotes}"; 291 - JsonDocument doc; 292 - DeserializationError err = deserializeJson( doc, badJson ); 293 - TEST_ASSERT_FALSE( err == DeserializationError::Ok ); 294 - } 295 - 296 - // --------------------------------------------------------------------------- 297 - // Runner 298 - // --------------------------------------------------------------------------- 299 - int main( int argc, char** argv ) { 300 - UNITY_BEGIN(); 301 - RUN_TEST( test_credential_serialize ); 302 - RUN_TEST( test_max_credential_name_length ); 303 - RUN_TEST( test_empty_fields ); 304 - RUN_TEST( test_special_characters ); 305 - RUN_TEST( test_unicode_utf8 ); 306 - RUN_TEST( test_missing_optional_fields ); 307 - RUN_TEST( test_credential_list_json ); 308 - RUN_TEST( test_totp_entry_serialize ); 309 - RUN_TEST( test_totp_8_digits ); 310 - RUN_TEST( test_max_credentials_array ); 311 - RUN_TEST( test_csv_line_parsing ); 312 - RUN_TEST( test_backup_json_format ); 313 - RUN_TEST( test_invalid_json_returns_error ); 314 - return UNITY_END(); 315 - }
+172 -135
test/test_vault_menu/test_vault_menu.cpp test/states/test_vault_state/test_vault_state.cpp
··· 1 - // Kleidos — Native unit tests for vault menu logic 2 - // Tests menu selection cycling, scroll window, credential icon classification. 3 - // Run: pio test -e native --filter test_vault_menu 1 + // Kleidos — Native test: VaultState (menu selection, scroll, icon classification, layout) 2 + // Build: pio test -e native -f states/test_vault_state 4 3 4 + // --- File header ----------------------------------------------------------- 5 + 6 + // Group 3: stdlib + Unity 5 7 #include <cstdint> 6 8 #include <cstring> 7 9 8 10 #include <unity.h> 9 11 10 - // --------------------------------------------------------------------------- 11 - // millis() stub 12 - // --------------------------------------------------------------------------- 13 - static uint32_t fakeMillis = 0; 14 - extern "C" uint32_t millis() { 15 - return fakeMillis; 16 - } 12 + // --- Constants ------------------------------------------------------------- 17 13 18 - // --------------------------------------------------------------------------- 19 - // Replicate vault menu constants from AppContext.h 20 - // --------------------------------------------------------------------------- 21 14 static constexpr uint8_t MAX_MENU_VISIBLE = 8; 22 15 23 - // --------------------------------------------------------------------------- 24 - // Replicate scroll window logic from VaultState.cpp::drawVaultMenu() 25 - // --------------------------------------------------------------------------- 26 - static uint8_t computeStartIdx( uint8_t menuSelected ) { 27 - uint8_t startIdx = 0; 28 - if ( menuSelected >= MAX_MENU_VISIBLE ) { 29 - startIdx = menuSelected - MAX_MENU_VISIBLE + 1; 30 - } 31 - return startIdx; 32 - } 33 - 34 - // --------------------------------------------------------------------------- 35 - // Replicate credential icon type detection from VaultState.cpp 36 - // --------------------------------------------------------------------------- 37 16 enum class IconType { 38 17 GITHUB, 39 18 GOOGLE, ··· 42 21 GENERIC 43 22 }; 44 23 45 - /// Case-insensitive substring search (mirrors VaultState.cpp::containsCI) 46 - static bool containsCI( const char* haystack, const char* needle ) { 47 - if ( !haystack || !needle ) 48 - return false; 49 - size_t nLen = strlen( needle ); 50 - if ( nLen == 0 ) 51 - return true; 52 - for ( const char* p = haystack; *p; p++ ) { 53 - if ( strncasecmp( p, needle, nLen ) == 0 ) 54 - return true; 55 - } 56 - return false; 57 - } 24 + // --- State variables ------------------------------------------------------- 25 + 26 + static uint32_t fakeMillis = 0; 27 + 28 + // --- Function declarations ------------------------------------------------- 29 + 30 + static uint8_t computeStartIdx( uint8_t menuSelected ); 31 + static bool containsCI( const char* haystack, const char* needle ); 32 + static IconType classifyCredentialIcon( const char* name ); 58 33 59 - static IconType classifyCredentialIcon( const char* name ) { 60 - if ( containsCI( name, "github" ) ) 61 - return IconType::GITHUB; 62 - if ( containsCI( name, "google" ) || containsCI( name, "gmail" ) ) 63 - return IconType::GOOGLE; 64 - if ( containsCI( name, "wifi" ) ) 65 - return IconType::WIFI; 66 - if ( strstr( name, "@" ) ) 67 - return IconType::EMAIL; 68 - return IconType::GENERIC; 34 + // --- setUp / tearDown ------------------------------------------------------ 35 + 36 + void setUp() { 37 + fakeMillis = 0; 69 38 } 70 39 71 - // =========================================================================== 72 - // Tests: Menu selection cycling 73 - // =========================================================================== 40 + void tearDown() { /* intentionally empty */ } 41 + 42 + // --- Tests ----------------------------------------------------------------- 74 43 75 - void test_menu_selection_wraps_at_total() { 44 + static void test_menu_selection_wraps_at_total() { 45 + // ARRANGE 76 46 uint8_t totalItems = 3; 77 47 uint8_t selected = 2; // last item 78 - selected = ( selected + 1 ) % totalItems; 48 + 49 + // ACT 50 + selected = ( selected + 1 ) % totalItems; 51 + 52 + // ASSERT 79 53 TEST_ASSERT_EQUAL_UINT8( 0, selected ); 80 54 } 81 55 82 - void test_menu_selection_increments() { 56 + static void test_menu_selection_increments() { 57 + // ARRANGE 83 58 uint8_t totalItems = 5; 84 59 uint8_t selected = 0; 85 - selected = ( selected + 1 ) % totalItems; 60 + 61 + // ACT + ASSERT 62 + selected = ( selected + 1 ) % totalItems; 86 63 TEST_ASSERT_EQUAL_UINT8( 1, selected ); 87 64 88 65 selected = ( selected + 1 ) % totalItems; 89 66 TEST_ASSERT_EQUAL_UINT8( 2, selected ); 90 67 } 91 68 92 - void test_menu_selection_single_item() { 69 + static void test_menu_selection_single_item() { 70 + // ARRANGE 93 71 uint8_t totalItems = 1; 94 72 uint8_t selected = 0; 95 - selected = ( selected + 1 ) % totalItems; 73 + 74 + // ACT 75 + selected = ( selected + 1 ) % totalItems; 76 + 77 + // ASSERT 96 78 TEST_ASSERT_EQUAL_UINT8( 0, selected ); // stays at 0 97 79 } 98 80 99 - void test_menu_selection_with_totp() { 100 - // 3 creds + 2 TOTP = 5 total items 81 + static void test_menu_selection_with_totp() { 82 + // ARRANGE (3 creds + 2 TOTP = 5 total items) 101 83 uint8_t credCount = 3; 102 84 uint8_t totpCount = 2; 103 85 uint8_t totalItems = credCount + totpCount; 104 86 uint8_t selected = 2; // last credential 105 87 88 + // ACT + ASSERT (advance into TOTP range) 106 89 selected = ( selected + 1 ) % totalItems; 107 90 TEST_ASSERT_EQUAL_UINT8( 3, selected ); 108 91 bool isTotp = selected >= credCount; ··· 113 96 isTotp = selected >= credCount; 114 97 TEST_ASSERT_TRUE( isTotp ); 115 98 116 - // Wrap back to first credential 99 + // ACT + ASSERT (wrap back to first credential) 117 100 selected = ( selected + 1 ) % totalItems; 118 101 TEST_ASSERT_EQUAL_UINT8( 0, selected ); 119 102 isTotp = selected >= credCount; 120 103 TEST_ASSERT_FALSE( isTotp ); 121 104 } 122 105 123 - // =========================================================================== 124 - // Tests: Scroll window (startIdx) 125 - // =========================================================================== 106 + static void test_scroll_no_offset_when_few_items() { 107 + // ARRANGE + ACT (Items 0..7 visible without scroll — MAX_MENU_VISIBLE=8) 126 108 127 - void test_scroll_no_offset_when_few_items() { 128 - // Items 0..7 visible without scroll (MAX_MENU_VISIBLE=8) 109 + // ASSERT 129 110 for ( uint8_t sel = 0; sel < MAX_MENU_VISIBLE; sel++ ) { 130 111 TEST_ASSERT_EQUAL_UINT8( 0, computeStartIdx( sel ) ); 131 112 } 132 113 } 133 114 134 - void test_scroll_starts_when_selected_exceeds_visible() { 135 - // menuSelected=8 → startIdx=1 (items 1..8 visible) 136 - TEST_ASSERT_EQUAL_UINT8( 1, computeStartIdx( 8 ) ); 137 - // menuSelected=9 → startIdx=2 138 - TEST_ASSERT_EQUAL_UINT8( 2, computeStartIdx( 9 ) ); 139 - // menuSelected=15 → startIdx=8 140 - TEST_ASSERT_EQUAL_UINT8( 8, computeStartIdx( 15 ) ); 115 + static void test_scroll_starts_when_selected_exceeds_visible() { 116 + // ARRANGE + ACT (inline inputs) 117 + 118 + // ASSERT 119 + TEST_ASSERT_EQUAL_UINT8( 1, computeStartIdx( 8 ) ); // menuSelected=8 → startIdx=1 120 + TEST_ASSERT_EQUAL_UINT8( 2, computeStartIdx( 9 ) ); // menuSelected=9 → startIdx=2 121 + TEST_ASSERT_EQUAL_UINT8( 8, computeStartIdx( 15 ) ); // menuSelected=15 → startIdx=8 141 122 } 142 123 143 - void test_scroll_selected_always_visible() { 144 - // For any selection, it should be within [startIdx, startIdx + MAX_MENU_VISIBLE) 124 + static void test_scroll_selected_always_visible() { 125 + // ARRANGE + ACT (for any selection, it should be within the visible window) 126 + 127 + // ASSERT 145 128 for ( uint8_t sel = 0; sel < 50; sel++ ) { 146 129 uint8_t startIdx = computeStartIdx( sel ); 147 130 TEST_ASSERT_TRUE( sel >= startIdx ); ··· 149 132 } 150 133 } 151 134 152 - void test_scroll_indicators_logic() { 135 + static void test_scroll_indicators_logic() { 136 + // ARRANGE 153 137 uint8_t totalItems = 12; 154 138 155 - // At top: no up arrow, has down arrow 139 + // ACT (at top: selected=0) 156 140 uint8_t startIdx = computeStartIdx( 0 ); 141 + bool showUp = ( startIdx > 0 ); 142 + bool showDown = ( startIdx + MAX_MENU_VISIBLE < totalItems ); 143 + 144 + // ASSERT (at top: no up arrow, has down arrow) 157 145 TEST_ASSERT_EQUAL_UINT8( 0, startIdx ); 158 - bool showUp = ( startIdx > 0 ); 159 - bool showDown = ( startIdx + MAX_MENU_VISIBLE < totalItems ); 160 146 TEST_ASSERT_FALSE( showUp ); 161 147 TEST_ASSERT_TRUE( showDown ); 162 148 163 - // In middle: both arrows 149 + // ACT (in middle: selected=9) 164 150 startIdx = computeStartIdx( 9 ); // startIdx=2 165 151 showUp = ( startIdx > 0 ); 166 152 showDown = ( startIdx + MAX_MENU_VISIBLE < totalItems ); 153 + 154 + // ASSERT (in middle: both arrows) 167 155 TEST_ASSERT_TRUE( showUp ); 168 156 TEST_ASSERT_TRUE( showDown ); 169 157 170 - // At bottom: up arrow, no down arrow 158 + // ACT (at bottom: selected=11) 171 159 startIdx = computeStartIdx( 11 ); // startIdx=4, visible 4..11 172 160 showUp = ( startIdx > 0 ); 173 161 showDown = ( startIdx + MAX_MENU_VISIBLE < totalItems ); 162 + 163 + // ASSERT (at bottom: up arrow, no down arrow) 174 164 TEST_ASSERT_TRUE( showUp ); 175 165 TEST_ASSERT_FALSE( showDown ); 176 166 } 177 167 178 - // =========================================================================== 179 - // Tests: Credential icon classification 180 - // =========================================================================== 168 + static void test_icon_github_exact() { 169 + // ARRANGE + ACT (inline string inputs) 181 170 182 - void test_icon_github_exact() { 171 + // ASSERT 183 172 TEST_ASSERT_EQUAL( IconType::GITHUB, classifyCredentialIcon( "GitHub" ) ); 184 173 TEST_ASSERT_EQUAL( IconType::GITHUB, classifyCredentialIcon( "github" ) ); 185 174 TEST_ASSERT_EQUAL( IconType::GITHUB, classifyCredentialIcon( "My GitHub Account" ) ); 186 175 TEST_ASSERT_EQUAL( IconType::GITHUB, classifyCredentialIcon( "github.com" ) ); 187 176 } 188 177 189 - void test_icon_github_case_insensitive() { 178 + static void test_icon_github_case_insensitive() { 179 + // ARRANGE + ACT (inline string inputs) 180 + 181 + // ASSERT 190 182 TEST_ASSERT_EQUAL( IconType::GITHUB, classifyCredentialIcon( "GITHUB" ) ); 191 183 TEST_ASSERT_EQUAL( IconType::GITHUB, classifyCredentialIcon( "Github" ) ); 192 184 TEST_ASSERT_EQUAL( IconType::GITHUB, classifyCredentialIcon( "my GITHUB token" ) ); 193 185 } 194 186 195 - void test_icon_google_exact() { 187 + static void test_icon_google_exact() { 188 + // ARRANGE + ACT (inline string inputs) 189 + 190 + // ASSERT 196 191 TEST_ASSERT_EQUAL( IconType::GOOGLE, classifyCredentialIcon( "Google" ) ); 197 192 TEST_ASSERT_EQUAL( IconType::GOOGLE, classifyCredentialIcon( "google" ) ); 198 193 TEST_ASSERT_EQUAL( IconType::GOOGLE, classifyCredentialIcon( "Google Drive" ) ); 199 194 } 200 195 201 - void test_icon_gmail_maps_to_google() { 202 - // Gmail should get the Google "G" icon 196 + static void test_icon_gmail_maps_to_google() { 197 + // ARRANGE + ACT (inline string inputs) 198 + 199 + // ASSERT (Gmail should get the Google "G" icon) 203 200 TEST_ASSERT_EQUAL( IconType::GOOGLE, classifyCredentialIcon( "Gmail" ) ); 204 201 TEST_ASSERT_EQUAL( IconType::GOOGLE, classifyCredentialIcon( "gmail" ) ); 205 202 TEST_ASSERT_EQUAL( IconType::GOOGLE, classifyCredentialIcon( "My Gmail" ) ); 206 203 TEST_ASSERT_EQUAL( IconType::GOOGLE, classifyCredentialIcon( "GMAIL" ) ); 207 204 } 208 205 209 - void test_icon_wifi() { 206 + static void test_icon_wifi() { 207 + // ARRANGE + ACT (inline string inputs) 208 + 209 + // ASSERT 210 210 TEST_ASSERT_EQUAL( IconType::WIFI, classifyCredentialIcon( "WiFi Home" ) ); 211 211 TEST_ASSERT_EQUAL( IconType::WIFI, classifyCredentialIcon( "Office wifi" ) ); 212 212 TEST_ASSERT_EQUAL( IconType::WIFI, classifyCredentialIcon( "WIFI" ) ); 213 213 TEST_ASSERT_EQUAL( IconType::WIFI, classifyCredentialIcon( "Wifi router" ) ); 214 214 } 215 215 216 - void test_icon_email() { 216 + static void test_icon_email() { 217 + // ARRANGE + ACT (inline string inputs) 218 + 219 + // ASSERT 217 220 TEST_ASSERT_EQUAL( IconType::EMAIL, classifyCredentialIcon( "user@company.org" ) ); 218 221 TEST_ASSERT_EQUAL( IconType::EMAIL, classifyCredentialIcon( "admin@server.net" ) ); 219 222 } 220 223 221 - void test_icon_generic_fallback() { 224 + static void test_icon_generic_fallback() { 225 + // ARRANGE + ACT (inline string inputs) 226 + 227 + // ASSERT 222 228 TEST_ASSERT_EQUAL( IconType::GENERIC, classifyCredentialIcon( "AWS Console" ) ); 223 229 TEST_ASSERT_EQUAL( IconType::GENERIC, classifyCredentialIcon( "Steam" ) ); 224 230 TEST_ASSERT_EQUAL( IconType::GENERIC, classifyCredentialIcon( "Netflix" ) ); 225 231 TEST_ASSERT_EQUAL( IconType::GENERIC, classifyCredentialIcon( "" ) ); 226 232 } 227 233 228 - void test_icon_priority_github_over_email() { 229 - // "GitHub" matches before "@" check 234 + static void test_icon_priority_github_over_email() { 235 + // ARRANGE + ACT (inline string inputs) 236 + 237 + // ASSERT ("GitHub" matches before "@" check) 230 238 TEST_ASSERT_EQUAL( IconType::GITHUB, classifyCredentialIcon( "github@work" ) ); 231 239 } 232 240 233 - void test_icon_priority_google_over_email() { 241 + static void test_icon_priority_google_over_email() { 242 + // ARRANGE + ACT (inline string inputs) 243 + 244 + // ASSERT 234 245 TEST_ASSERT_EQUAL( IconType::GOOGLE, classifyCredentialIcon( "google@work" ) ); 235 246 } 236 247 237 - void test_icon_priority_gmail_over_email() { 238 - // "gmail" in name matches Google before "@" check 248 + static void test_icon_priority_gmail_over_email() { 249 + // ARRANGE + ACT (inline string inputs) 250 + 251 + // ASSERT ("gmail" in name matches Google before "@" check) 239 252 TEST_ASSERT_EQUAL( IconType::GOOGLE, classifyCredentialIcon( "gmail@personal" ) ); 240 253 } 241 254 242 - // =========================================================================== 243 - // Tests: Item Y position calculation (verified against screenshots) 244 - // =========================================================================== 245 - 246 - void test_item_y_position_calculation() { 255 + static void test_item_y_position_calculation() { 256 + // ARRANGE 247 257 // From drawVaultMenu(): int16_t y = 22 + i * 22; 248 258 // where i is 0-based visible index (not absolute) 249 259 auto itemY = []( uint8_t visibleIndex ) -> int16_t { return 22 + visibleIndex * 22; }; 250 260 261 + // ASSERT 251 262 // First item at y=22 (verified: screenshot 03 shows NAVY at y22-41) 252 263 TEST_ASSERT_EQUAL_INT16( 22, itemY( 0 ) ); 253 264 // Second item at y=44 (screenshot 03: white text at y47) ··· 256 267 TEST_ASSERT_EQUAL_INT16( 66, itemY( 2 ) ); 257 268 } 258 269 259 - void test_selection_highlight_height() { 270 + static void test_selection_highlight_height() { 271 + // ARRANGE 260 272 // From drawVaultMenu(): fillRect(0, y, SCR_W, 20, TFT_NAVY) 261 273 // NAVY rect is 20px tall, gap of 2px between items (22-20) 262 274 constexpr int16_t ITEM_HEIGHT = 20; 263 275 constexpr int16_t ITEM_SPACING = 22; 276 + 277 + // ASSERT 264 278 TEST_ASSERT_EQUAL( 2, ITEM_SPACING - ITEM_HEIGHT ); 265 279 } 266 280 267 - // =========================================================================== 268 - // Tests: Header layout (verified against screenshots) 269 - // =========================================================================== 270 - 271 - void test_header_height() { 281 + static void test_header_height() { 282 + // ARRANGE 272 283 // drawVaultMenu(): fillRect(0, 0, SCR_W, 18, TFT_DARKGREEN) 273 284 // Screenshot analysis: green header y0-17 = 18px tall 274 285 constexpr int16_t HEADER_HEIGHT = 18; 275 286 constexpr int16_t FIRST_ITEM_Y = 22; 276 - int16_t gap = FIRST_ITEM_Y - HEADER_HEIGHT; 287 + 288 + // ACT 289 + int16_t gap = FIRST_ITEM_Y - HEADER_HEIGHT; 290 + 291 + // ASSERT 277 292 TEST_ASSERT_EQUAL( 4, gap ); // 4px gap between header and first item 278 293 } 279 294 280 - // =========================================================================== 281 - // Tests: Footer position 282 - // =========================================================================== 283 - 284 - void test_footer_positions() { 295 + static void test_footer_positions() { 296 + // ARRANGE 285 297 // drawVaultMenu(): drawString("A: nav / send", SCR_CX, 220) 286 298 // drawString("B: lock / wifi", SCR_CX, 232) 287 299 // Screenshot: gray text at y217-222 and y228-234 288 300 constexpr int16_t FOOTER_LINE1_Y = 220; 289 301 constexpr int16_t FOOTER_LINE2_Y = 232; 290 302 constexpr int16_t SCR_H = 240; 303 + 304 + // ASSERT 291 305 TEST_ASSERT_TRUE( FOOTER_LINE2_Y < SCR_H ); // footer fits on screen 292 306 TEST_ASSERT_EQUAL( 12, FOOTER_LINE2_Y - FOOTER_LINE1_Y ); // line spacing 293 307 } 294 308 295 - // =========================================================================== 296 - // Tests: MAX_MENU_VISIBLE vs screen space 297 - // =========================================================================== 298 - 299 - void test_all_visible_items_fit_on_screen() { 309 + static void test_all_visible_items_fit_on_screen() { 310 + // ARRANGE 300 311 constexpr int16_t FIRST_ITEM_Y = 22; 301 312 constexpr int16_t ITEM_SPACING = 22; 302 313 constexpr int16_t FOOTER_Y = 220; 303 314 315 + // ACT 304 316 int16_t lastItemBottom = FIRST_ITEM_Y + MAX_MENU_VISIBLE * ITEM_SPACING; 317 + 318 + // ASSERT 305 319 TEST_ASSERT_TRUE( lastItemBottom <= FOOTER_Y ); 306 320 } 307 321 308 - // =========================================================================== 309 - // Entry point 310 - // =========================================================================== 311 - void setUp() {} 312 - void tearDown() {} 322 + // --- State variables ------------------------------------------------------- 313 323 314 - int main() { 315 - UNITY_BEGIN(); 324 + static uint8_t computeStartIdx( uint8_t menuSelected ) { 325 + uint8_t startIdx = 0; 326 + if ( menuSelected >= MAX_MENU_VISIBLE ) { 327 + startIdx = menuSelected - MAX_MENU_VISIBLE + 1; 328 + } 329 + return startIdx; 330 + } 316 331 317 - // Menu selection cycling 332 + /// Case-insensitive substring search (mirrors VaultState.cpp::containsCI) 333 + static bool containsCI( const char* haystack, const char* needle ) { 334 + if ( !haystack || !needle ) 335 + return false; 336 + size_t nLen = strlen( needle ); 337 + if ( nLen == 0 ) 338 + return true; 339 + for ( const char* p = haystack; *p; p++ ) { 340 + if ( strncasecmp( p, needle, nLen ) == 0 ) 341 + return true; 342 + } 343 + return false; 344 + } 345 + 346 + static IconType classifyCredentialIcon( const char* name ) { 347 + if ( containsCI( name, "github" ) ) 348 + return IconType::GITHUB; 349 + if ( containsCI( name, "google" ) || containsCI( name, "gmail" ) ) 350 + return IconType::GOOGLE; 351 + if ( containsCI( name, "wifi" ) ) 352 + return IconType::WIFI; 353 + if ( strstr( name, "@" ) ) 354 + return IconType::EMAIL; 355 + return IconType::GENERIC; 356 + } 357 + 358 + // --- main() ---------------------------------------------------------------- 359 + 360 + int main( int /*argc*/, char** /*argv*/ ) { 361 + UNITY_BEGIN(); 318 362 RUN_TEST( test_menu_selection_wraps_at_total ); 319 363 RUN_TEST( test_menu_selection_increments ); 320 364 RUN_TEST( test_menu_selection_single_item ); 321 365 RUN_TEST( test_menu_selection_with_totp ); 322 - 323 - // Scroll window 324 366 RUN_TEST( test_scroll_no_offset_when_few_items ); 325 367 RUN_TEST( test_scroll_starts_when_selected_exceeds_visible ); 326 368 RUN_TEST( test_scroll_selected_always_visible ); 327 369 RUN_TEST( test_scroll_indicators_logic ); 328 - 329 - // Icon classification 330 370 RUN_TEST( test_icon_github_exact ); 331 371 RUN_TEST( test_icon_github_case_insensitive ); 332 372 RUN_TEST( test_icon_google_exact ); ··· 337 377 RUN_TEST( test_icon_priority_github_over_email ); 338 378 RUN_TEST( test_icon_priority_google_over_email ); 339 379 RUN_TEST( test_icon_priority_gmail_over_email ); 340 - 341 - // Layout verification (validated against device screenshots) 342 380 RUN_TEST( test_item_y_position_calculation ); 343 381 RUN_TEST( test_selection_highlight_height ); 344 382 RUN_TEST( test_header_height ); 345 383 RUN_TEST( test_footer_positions ); 346 384 RUN_TEST( test_all_visible_items_fit_on_screen ); 347 - 348 385 return UNITY_END(); 349 386 }
+64 -49
test/test_vault_task/test_vault_task.cpp test/vault/test_vault_task/test_vault_task.cpp
··· 1 - // Kleidos — VaultTask host-side unit tests 1 + // Kleidos — Native test: Vault Task (host-side direct invocation fallback) 2 + // Build: pio test -e native -f vault/test_vault_task 2 3 // 3 4 // The native build has no FreeRTOS, so kleidos::vault::execute() degrades 4 5 // to direct invocation: every call runs on the caller's thread, and ··· 10 11 // on-device test suite; they cannot run in host without stubbing the 11 12 // entire FreeRTOS API. 12 13 13 - #include "../../src/vault/vault_task.h" 14 + // --- Includes -------------------------------------------------------------- 14 15 16 + // Group 1: module(s) under test 17 + #include "vault/vault_task.h" 18 + 19 + // Group 3: stdlib + Unity 15 20 #include <unity.h> 16 21 17 - // HoldButton.cpp / VirtualButtonInput.cpp are pulled in by the native env's 18 - // build_src_filter; both reference Arduino's millis(). Provide a minimal 19 - // stub so the linker is happy for tests that do not exercise time at all. 20 - extern "C" uint32_t millis() { 21 - return 0; 22 - } 22 + // --- State variables ------------------------------------------------------- 23 23 24 - namespace { 24 + static struct { 25 + int callCount; 26 + } spy; 25 27 26 - int s_callCount = 0; 28 + // --- Function declarations ------------------------------------------------- 27 29 28 - bool incrementCallback() { 29 - ++s_callCount; 30 - return true; 31 - } 30 + // Fakes 31 + static bool incrementCallback(); 32 + static bool failingCallback(); 32 33 33 - bool failingCallback() { 34 - return false; 35 - } 34 + // --- setUp / tearDown ------------------------------------------------------ 36 35 37 - void setUpHook() { 38 - s_callCount = 0; 36 + void setUp() { 37 + spy = {}; 39 38 } 40 39 41 - } // namespace 40 + void tearDown() { /* intentionally empty */ } 42 41 43 - void setUp( void ) { 44 - setUpHook(); 45 - } 46 - void tearDown( void ) {} 42 + // --- Tests ----------------------------------------------------------------- 47 43 48 - // --------------------------------------------------------------------------- 49 44 // onWorker() — host build always reports "on worker" so shims never recurse. 50 - // --------------------------------------------------------------------------- 51 - void test_native_onWorker_always_true() { 45 + static void test_native_onWorker_always_true() { 46 + // ACT + ASSERT 52 47 TEST_ASSERT_TRUE( kleidos::vault::onWorker() ); 53 48 } 54 49 55 - // --------------------------------------------------------------------------- 56 50 // execute() with a captureless function pointer — runs the body inline. 57 - // --------------------------------------------------------------------------- 58 - void test_native_execute_runs_inline() { 51 + static void test_native_execute_runs_inline() { 52 + // ACT 59 53 bool ok = kleidos::vault::execute( incrementCallback ); 54 + 55 + // ASSERT 60 56 TEST_ASSERT_TRUE( ok ); 61 - TEST_ASSERT_EQUAL_INT( 1, s_callCount ); 57 + TEST_ASSERT_EQUAL_INT( 1, spy.callCount ); 62 58 } 63 59 64 - // --------------------------------------------------------------------------- 65 60 // execute() with a capturing lambda — captures by reference must work since 66 61 // the host fallback invokes the lambda synchronously on the same thread. 67 - // --------------------------------------------------------------------------- 68 - void test_native_execute_capturing_lambda() { 69 - int local = 41; 70 - bool ok = kleidos::vault::execute( [&]() -> bool { 62 + static void test_native_execute_capturing_lambda() { 63 + // ARRANGE 64 + int local = 41; 65 + 66 + // ACT 67 + bool ok = kleidos::vault::execute( [&]() -> bool { 71 68 local += 1; 72 69 return true; 73 70 } ); 71 + 72 + // ASSERT 74 73 TEST_ASSERT_TRUE( ok ); 75 74 TEST_ASSERT_EQUAL_INT( 42, local ); 76 75 } 77 76 78 - // --------------------------------------------------------------------------- 79 77 // execute() returns the lambda's bool verbatim — false propagates so callers 80 78 // that wrap a `return false` body get the expected failure code. 81 - // --------------------------------------------------------------------------- 82 - void test_native_execute_propagates_failure() { 79 + static void test_native_execute_propagates_failure() { 80 + // ACT 83 81 bool ok = kleidos::vault::execute( failingCallback ); 82 + 83 + // ASSERT 84 84 TEST_ASSERT_FALSE( ok ); 85 85 } 86 86 87 - // --------------------------------------------------------------------------- 88 87 // Typed convenience wrappers (listRequest, readEntryRequest, ...) all defer 89 88 // to execute(). Verify each one runs the body and returns its result. 90 - // --------------------------------------------------------------------------- 91 - void test_native_typed_wrappers_dispatch_to_execute() { 89 + static void test_native_typed_wrappers_dispatch_to_execute() { 90 + // ARRANGE 92 91 int hits = 0; 93 92 auto bump = [&]() -> bool { 94 93 ++hits; 95 94 return true; 96 95 }; 97 96 97 + // ACT + ASSERT 98 98 TEST_ASSERT_TRUE( kleidos::vault::listRequest( bump ) ); 99 99 TEST_ASSERT_TRUE( kleidos::vault::readEntryRequest( bump ) ); 100 100 TEST_ASSERT_TRUE( kleidos::vault::writeEntryRequest( bump ) ); ··· 103 103 TEST_ASSERT_TRUE( kleidos::vault::loadAllRequest( bump ) ); 104 104 TEST_ASSERT_TRUE( kleidos::vault::exportCsvRequest( bump ) ); 105 105 TEST_ASSERT_TRUE( kleidos::vault::importCsvRequest( bump ) ); 106 - 107 106 TEST_ASSERT_EQUAL_INT( 8, hits ); 108 107 } 109 108 110 - // --------------------------------------------------------------------------- 111 109 // Re-entrant calls under host build are simple recursion: the inner lambda 112 110 // runs to completion before the outer returns. 113 - // --------------------------------------------------------------------------- 114 - void test_native_execute_reentrant() { 115 - int order[3] = { 0, 0, 0 }; 116 - int idx = 0; 117 - bool ok = kleidos::vault::execute( [&]() -> bool { 111 + static void test_native_execute_reentrant() { 112 + // ARRANGE 113 + int order[3] = { 0, 0, 0 }; 114 + int idx = 0; 115 + 116 + // ACT 117 + bool ok = kleidos::vault::execute( [&]() -> bool { 118 118 order[idx++] = 1; 119 119 bool inner = kleidos::vault::execute( [&]() -> bool { 120 120 order[idx++] = 2; ··· 123 123 order[idx++] = inner ? 3 : -3; 124 124 return inner; 125 125 } ); 126 + 127 + // ASSERT 126 128 TEST_ASSERT_TRUE( ok ); 127 129 TEST_ASSERT_EQUAL_INT( 1, order[0] ); 128 130 TEST_ASSERT_EQUAL_INT( 2, order[1] ); 129 131 TEST_ASSERT_EQUAL_INT( 3, order[2] ); 130 132 } 133 + 134 + // --- State variables ------------------------------------------------------- 135 + 136 + static bool incrementCallback() { 137 + ++spy.callCount; 138 + return true; 139 + } 140 + 141 + static bool failingCallback() { 142 + return false; 143 + } 144 + 145 + // --- main() ---------------------------------------------------------------- 131 146 132 147 int main( int /*argc*/, char** /*argv*/ ) { 133 148 UNITY_BEGIN();
-823
test/test_vault_v2/test_vault_v2.cpp
··· 1 - // Kleidos — Native test: VaultStore v2 meta logic 2 - // Tests v2 meta structure, PIN verifier computation, favorites bitfield, 3 - // credential ordering, HMAC integrity, and CSV edge cases. 4 - // Uses VaultCrypto directly (real mbedTLS) and ArduinoJson. 5 - 6 - #include <ArduinoJson.h> 7 - 8 - #include <algorithm> 9 - #include <cstdint> 10 - #include <cstring> 11 - 12 - #include <unity.h> 13 - 14 - // millis() stub 15 - static uint32_t fakeMillis = 0; 16 - extern "C" uint32_t millis() { 17 - return fakeMillis; 18 - } 19 - 20 - #include "crypto/vault_crypto.h" 21 - 22 - // --------------------------------------------------------------------------- 23 - // Replicated constants and structures from VaultStore.h 24 - // --------------------------------------------------------------------------- 25 - static constexpr size_t MAX_CREDENTIALS = 50; 26 - static constexpr size_t MAX_TOTP = 20; 27 - static constexpr size_t MAX_NAME_LEN = 32; 28 - static constexpr size_t MAX_USER_LEN = 64; 29 - static constexpr size_t MAX_PASS_LEN = 128; 30 - static constexpr size_t MAX_URL_LEN = 128; 31 - 32 - static constexpr uint8_t META_V2_MAGIC[3] = { 0x53, 0x50, 0x02 }; 33 - static constexpr char PIN_VERIFY_TAG[] = "kleidos-pin-v2"; 34 - 35 - struct VaultMetaV2 { 36 - uint8_t magic[3]; 37 - uint8_t kdfSalt[kSaltSize]; 38 - uint8_t pinVerifier[kPinHashSize]; 39 - uint8_t hmacSalt[kSaltSize]; 40 - }; 41 - 42 - struct VaultMeta { 43 - uint8_t pinSalt[kSaltSize]; 44 - uint8_t pinHash[kPinHashSize]; 45 - uint8_t encSalt[kSaltSize]; 46 - uint8_t hmacSalt[kSaltSize]; 47 - }; 48 - 49 - struct Credential { 50 - char name[MAX_NAME_LEN + 1]; 51 - char user[MAX_USER_LEN + 1]; 52 - char pass[MAX_PASS_LEN + 1]; 53 - char url[MAX_URL_LEN + 1]; 54 - }; 55 - 56 - struct CredentialEntry { 57 - uint8_t id; 58 - char name[MAX_NAME_LEN + 1]; 59 - bool favorite; 60 - uint8_t sortOrder; 61 - }; 62 - 63 - // --------------------------------------------------------------------------- 64 - // V2 Meta Structure Tests 65 - // --------------------------------------------------------------------------- 66 - void test_v2_magic_bytes() { 67 - TEST_ASSERT_EQUAL_HEX8( 0x53, META_V2_MAGIC[0] ); // 'S' 68 - TEST_ASSERT_EQUAL_HEX8( 0x50, META_V2_MAGIC[1] ); // 'P' 69 - TEST_ASSERT_EQUAL_HEX8( 0x02, META_V2_MAGIC[2] ); // v2 70 - } 71 - 72 - void test_v2_meta_size() { 73 - // magic(3) + kdfSalt(16) + pinVerifier(32) + hmacSalt(16) = 67 74 - TEST_ASSERT_EQUAL( 67, sizeof( VaultMetaV2 ) ); 75 - } 76 - 77 - void test_v1_meta_size() { 78 - // pinSalt(16) + pinHash(32) + encSalt(16) + hmacSalt(16) = 80 79 - TEST_ASSERT_EQUAL( 80, sizeof( VaultMeta ) ); 80 - } 81 - 82 - void test_v2_is_distinguishable_from_v1() { 83 - // V2 has a unique size (67) vs V1 (80) — they don't overlap 84 - TEST_ASSERT_NOT_EQUAL( sizeof( VaultMetaV2 ), sizeof( VaultMeta ) ); 85 - // V2 starts with magic bytes, V1 starts with random salt — check first 3 bytes 86 - VaultMeta v1; 87 - memset( &v1, 0, sizeof( v1 ) ); 88 - // V1 salt is random, very unlikely to match magic (SP\x02) 89 - TEST_ASSERT_NOT_EQUAL( META_V2_MAGIC[0], v1.pinSalt[0] ); 90 - } 91 - 92 - // --------------------------------------------------------------------------- 93 - // PIN Verifier Computation (v2 unlock logic, replicated locally) 94 - // --------------------------------------------------------------------------- 95 - static bool simulateProvision( const char* pin, size_t pinLen, VaultMetaV2& meta, 96 - uint8_t* keyOut ) { 97 - memcpy( meta.magic, META_V2_MAGIC, sizeof( META_V2_MAGIC ) ); 98 - VaultCrypto::generateRandom( meta.kdfSalt, kSaltSize ); 99 - VaultCrypto::generateRandom( meta.hmacSalt, kSaltSize ); 100 - 101 - if ( !VaultCrypto::deriveKey( pin, pinLen, meta.kdfSalt, kSaltSize, keyOut, kAesKeySize ) ) 102 - return false; 103 - 104 - if ( !VaultCrypto::hmacSha256( keyOut, kAesKeySize, 105 - reinterpret_cast<const uint8_t*>( PIN_VERIFY_TAG ), 106 - sizeof( PIN_VERIFY_TAG ) - 1, meta.pinVerifier ) ) 107 - return false; 108 - 109 - return true; 110 - } 111 - 112 - static bool simulateUnlock( const char* pin, size_t pinLen, const VaultMetaV2& meta, 113 - uint8_t* keyOut ) { 114 - if ( !VaultCrypto::deriveKey( pin, pinLen, meta.kdfSalt, kSaltSize, keyOut, kAesKeySize ) ) 115 - return false; 116 - 117 - uint8_t computed[kPinHashSize]; 118 - if ( !VaultCrypto::hmacSha256( keyOut, kAesKeySize, 119 - reinterpret_cast<const uint8_t*>( PIN_VERIFY_TAG ), 120 - sizeof( PIN_VERIFY_TAG ) - 1, computed ) ) 121 - return false; 122 - 123 - bool ok = VaultCrypto::constantTimeEqual( computed, meta.pinVerifier, kPinHashSize ); 124 - VaultCrypto::secureWipe( computed, sizeof( computed ) ); 125 - 126 - if ( !ok ) 127 - VaultCrypto::secureWipe( keyOut, kAesKeySize ); 128 - return ok; 129 - } 130 - 131 - void test_v2_provision_and_unlock_correct_pin() { 132 - VaultMetaV2 meta; 133 - uint8_t provKey[kAesKeySize]; 134 - TEST_ASSERT_TRUE( simulateProvision( "1234", 4, meta, provKey ) ); 135 - 136 - uint8_t unlockKey[kAesKeySize]; 137 - TEST_ASSERT_TRUE( simulateUnlock( "1234", 4, meta, unlockKey ) ); 138 - 139 - // Keys must match 140 - TEST_ASSERT_EQUAL_MEMORY( provKey, unlockKey, kAesKeySize ); 141 - } 142 - 143 - void test_v2_unlock_wrong_pin_fails() { 144 - VaultMetaV2 meta; 145 - uint8_t provKey[kAesKeySize]; 146 - TEST_ASSERT_TRUE( simulateProvision( "1234", 4, meta, provKey ) ); 147 - 148 - uint8_t unlockKey[kAesKeySize]; 149 - TEST_ASSERT_FALSE( simulateUnlock( "9999", 4, meta, unlockKey ) ); 150 - } 151 - 152 - void test_v2_unlock_empty_pin_fails() { 153 - VaultMetaV2 meta; 154 - uint8_t provKey[kAesKeySize]; 155 - TEST_ASSERT_TRUE( simulateProvision( "5678", 4, meta, provKey ) ); 156 - 157 - uint8_t unlockKey[kAesKeySize]; 158 - TEST_ASSERT_FALSE( simulateUnlock( "", 0, meta, unlockKey ) ); 159 - } 160 - 161 - void test_v2_different_pins_different_keys() { 162 - VaultMetaV2 meta1, meta2; 163 - uint8_t key1[kAesKeySize], key2[kAesKeySize]; 164 - TEST_ASSERT_TRUE( simulateProvision( "1111", 4, meta1, key1 ) ); 165 - TEST_ASSERT_TRUE( simulateProvision( "2222", 4, meta2, key2 ) ); 166 - 167 - // Different PINs with different salts produce different keys 168 - TEST_ASSERT_FALSE( VaultCrypto::constantTimeEqual( key1, key2, kAesKeySize ) ); 169 - } 170 - 171 - void test_v2_same_pin_same_salt_same_key() { 172 - // Simulate same salt → deterministic key derivation 173 - VaultMetaV2 meta; 174 - memcpy( meta.magic, META_V2_MAGIC, sizeof( META_V2_MAGIC ) ); 175 - memset( meta.kdfSalt, 0xAA, kSaltSize ); 176 - memset( meta.hmacSalt, 0xBB, kSaltSize ); 177 - 178 - uint8_t key1[kAesKeySize], key2[kAesKeySize]; 179 - VaultCrypto::deriveKey( "test", 4, meta.kdfSalt, kSaltSize, key1, kAesKeySize ); 180 - VaultCrypto::deriveKey( "test", 4, meta.kdfSalt, kSaltSize, key2, kAesKeySize ); 181 - 182 - TEST_ASSERT_EQUAL_MEMORY( key1, key2, kAesKeySize ); 183 - } 184 - 185 - void test_v2_pin_verifier_is_deterministic() { 186 - VaultMetaV2 meta; 187 - memcpy( meta.magic, META_V2_MAGIC, sizeof( META_V2_MAGIC ) ); 188 - memset( meta.kdfSalt, 0xCC, kSaltSize ); 189 - 190 - uint8_t key[kAesKeySize]; 191 - VaultCrypto::deriveKey( "1234", 4, meta.kdfSalt, kSaltSize, key, kAesKeySize ); 192 - 193 - uint8_t verifier1[kPinHashSize], verifier2[kPinHashSize]; 194 - VaultCrypto::hmacSha256( key, kAesKeySize, reinterpret_cast<const uint8_t*>( PIN_VERIFY_TAG ), 195 - sizeof( PIN_VERIFY_TAG ) - 1, verifier1 ); 196 - VaultCrypto::hmacSha256( key, kAesKeySize, reinterpret_cast<const uint8_t*>( PIN_VERIFY_TAG ), 197 - sizeof( PIN_VERIFY_TAG ) - 1, verifier2 ); 198 - 199 - TEST_ASSERT_EQUAL_MEMORY( verifier1, verifier2, kPinHashSize ); 200 - } 201 - 202 - // --------------------------------------------------------------------------- 203 - // V1 → V2 Migration Logic 204 - // --------------------------------------------------------------------------- 205 - void test_v1_to_v2_migration_preserves_key() { 206 - // Simulate: v1 has encSalt, we derive a masterKey from it. 207 - // Migration to v2 uses the same salt as kdfSalt, so the key stays the same. 208 - uint8_t encSalt[kSaltSize]; 209 - memset( encSalt, 0xDD, kSaltSize ); 210 - 211 - uint8_t v1Key[kAesKeySize]; 212 - VaultCrypto::deriveKey( "4321", 4, encSalt, kSaltSize, v1Key, kAesKeySize ); 213 - 214 - // Build v2 meta with same salt 215 - VaultMetaV2 v2; 216 - memcpy( v2.magic, META_V2_MAGIC, sizeof( META_V2_MAGIC ) ); 217 - memcpy( v2.kdfSalt, encSalt, kSaltSize ); 218 - VaultCrypto::hmacSha256( v1Key, kAesKeySize, reinterpret_cast<const uint8_t*>( PIN_VERIFY_TAG ), 219 - sizeof( PIN_VERIFY_TAG ) - 1, v2.pinVerifier ); 220 - 221 - // Unlock with v2 should produce the same key 222 - uint8_t v2Key[kAesKeySize]; 223 - TEST_ASSERT_TRUE( simulateUnlock( "4321", 4, v2, v2Key ) ); 224 - TEST_ASSERT_EQUAL_MEMORY( v1Key, v2Key, kAesKeySize ); 225 - } 226 - 227 - // --------------------------------------------------------------------------- 228 - // Favorites Bitfield Logic 229 - // --------------------------------------------------------------------------- 230 - static void setFavoriteBit( uint8_t* favBits, uint8_t id, bool fav ) { 231 - if ( fav ) { 232 - favBits[id / 8] |= ( 1 << ( id % 8 ) ); 233 - } else { 234 - favBits[id / 8] &= ~( 1 << ( id % 8 ) ); 235 - } 236 - } 237 - 238 - static bool getFavoriteBit( const uint8_t* favBits, uint8_t id ) { 239 - return ( favBits[id / 8] >> ( id % 8 ) ) & 1; 240 - } 241 - 242 - void test_favorites_initially_all_off() { 243 - uint8_t favBits[( MAX_CREDENTIALS + 7 ) / 8] = {}; 244 - for ( uint8_t i = 0; i < MAX_CREDENTIALS; i++ ) { 245 - TEST_ASSERT_FALSE( getFavoriteBit( favBits, i ) ); 246 - } 247 - } 248 - 249 - void test_favorites_set_and_get() { 250 - uint8_t favBits[( MAX_CREDENTIALS + 7 ) / 8] = {}; 251 - setFavoriteBit( favBits, 0, true ); 252 - setFavoriteBit( favBits, 7, true ); 253 - setFavoriteBit( favBits, 49, true ); 254 - 255 - TEST_ASSERT_TRUE( getFavoriteBit( favBits, 0 ) ); 256 - TEST_ASSERT_TRUE( getFavoriteBit( favBits, 7 ) ); 257 - TEST_ASSERT_TRUE( getFavoriteBit( favBits, 49 ) ); 258 - TEST_ASSERT_FALSE( getFavoriteBit( favBits, 1 ) ); 259 - TEST_ASSERT_FALSE( getFavoriteBit( favBits, 48 ) ); 260 - } 261 - 262 - void test_favorites_clear() { 263 - uint8_t favBits[( MAX_CREDENTIALS + 7 ) / 8] = {}; 264 - setFavoriteBit( favBits, 5, true ); 265 - TEST_ASSERT_TRUE( getFavoriteBit( favBits, 5 ) ); 266 - setFavoriteBit( favBits, 5, false ); 267 - TEST_ASSERT_FALSE( getFavoriteBit( favBits, 5 ) ); 268 - } 269 - 270 - void test_favorites_adjacent_bits_independent() { 271 - uint8_t favBits[( MAX_CREDENTIALS + 7 ) / 8] = {}; 272 - setFavoriteBit( favBits, 8, true ); 273 - TEST_ASSERT_TRUE( getFavoriteBit( favBits, 8 ) ); 274 - TEST_ASSERT_FALSE( getFavoriteBit( favBits, 7 ) ); 275 - TEST_ASSERT_FALSE( getFavoriteBit( favBits, 9 ) ); 276 - } 277 - 278 - // --------------------------------------------------------------------------- 279 - // Credential Ordering Logic 280 - // --------------------------------------------------------------------------- 281 - static void buildOrderArray( const uint8_t* ids, uint8_t count, uint8_t* order ) { 282 - memset( order, 0xFF, MAX_CREDENTIALS ); 283 - for ( uint8_t i = 0; i < count && i < MAX_CREDENTIALS; i++ ) { 284 - if ( ids[i] < MAX_CREDENTIALS ) { 285 - order[ids[i]] = i; 286 - } 287 - } 288 - } 289 - 290 - void test_order_unset_is_0xFF() { 291 - uint8_t order[MAX_CREDENTIALS]; 292 - memset( order, 0xFF, sizeof( order ) ); 293 - for ( uint8_t i = 0; i < MAX_CREDENTIALS; i++ ) { 294 - TEST_ASSERT_EQUAL_HEX8( 0xFF, order[i] ); 295 - } 296 - } 297 - 298 - void test_order_set_custom() { 299 - uint8_t ids[] = { 5, 2, 0, 10 }; 300 - uint8_t order[MAX_CREDENTIALS]; 301 - buildOrderArray( ids, 4, order ); 302 - 303 - TEST_ASSERT_EQUAL( 2, order[0] ); // id 0 is at position 2 304 - TEST_ASSERT_EQUAL( 1, order[2] ); // id 2 is at position 1 305 - TEST_ASSERT_EQUAL( 0, order[5] ); // id 5 is at position 0 306 - TEST_ASSERT_EQUAL( 3, order[10] ); // id 10 is at position 3 307 - TEST_ASSERT_EQUAL_HEX8( 0xFF, order[1] ); // id 1 not in order 308 - } 309 - 310 - void test_order_empty_list() { 311 - uint8_t order[MAX_CREDENTIALS]; 312 - buildOrderArray( nullptr, 0, order ); 313 - for ( uint8_t i = 0; i < MAX_CREDENTIALS; i++ ) { 314 - TEST_ASSERT_EQUAL_HEX8( 0xFF, order[i] ); 315 - } 316 - } 317 - 318 - // --------------------------------------------------------------------------- 319 - // Sorting Logic (favorites first, then by sortOrder) 320 - // --------------------------------------------------------------------------- 321 - static void sortEntries( CredentialEntry* entries, uint8_t n ) { 322 - for ( uint8_t i = 1; i < n; i++ ) { 323 - for ( uint8_t j = i; j > 0; j-- ) { 324 - bool swap = false; 325 - if ( entries[j].favorite && !entries[j - 1].favorite ) { 326 - swap = true; 327 - } else if ( entries[j].favorite == entries[j - 1].favorite ) { 328 - if ( entries[j].sortOrder != 0xFF && entries[j - 1].sortOrder == 0xFF ) { 329 - swap = true; 330 - } else if ( entries[j].sortOrder != 0xFF && entries[j - 1].sortOrder != 0xFF 331 - && entries[j].sortOrder < entries[j - 1].sortOrder ) { 332 - swap = true; 333 - } 334 - } 335 - if ( swap ) { 336 - CredentialEntry tmp = entries[j]; 337 - entries[j] = entries[j - 1]; 338 - entries[j - 1] = tmp; 339 - } else { 340 - break; 341 - } 342 - } 343 - } 344 - } 345 - 346 - void test_sort_favorites_first() { 347 - CredentialEntry entries[3]; 348 - memset( entries, 0, sizeof( entries ) ); 349 - 350 - strcpy( entries[0].name, "Bravo" ); 351 - entries[0].id = 0; 352 - entries[0].favorite = false; 353 - entries[0].sortOrder = 0xFF; 354 - strcpy( entries[1].name, "Alpha" ); 355 - entries[1].id = 1; 356 - entries[1].favorite = true; 357 - entries[1].sortOrder = 0xFF; 358 - strcpy( entries[2].name, "Charlie" ); 359 - entries[2].id = 2; 360 - entries[2].favorite = false; 361 - entries[2].sortOrder = 0xFF; 362 - 363 - sortEntries( entries, 3 ); 364 - 365 - TEST_ASSERT_EQUAL( 1, entries[0].id ); // Alpha (favorite) first 366 - } 367 - 368 - void test_sort_by_order_within_same_fav_status() { 369 - CredentialEntry entries[3]; 370 - memset( entries, 0, sizeof( entries ) ); 371 - 372 - strcpy( entries[0].name, "C" ); 373 - entries[0].id = 0; 374 - entries[0].favorite = false; 375 - entries[0].sortOrder = 2; 376 - strcpy( entries[1].name, "A" ); 377 - entries[1].id = 1; 378 - entries[1].favorite = false; 379 - entries[1].sortOrder = 0; 380 - strcpy( entries[2].name, "B" ); 381 - entries[2].id = 2; 382 - entries[2].favorite = false; 383 - entries[2].sortOrder = 1; 384 - 385 - sortEntries( entries, 3 ); 386 - 387 - TEST_ASSERT_EQUAL( 1, entries[0].id ); // order 0 388 - TEST_ASSERT_EQUAL( 2, entries[1].id ); // order 1 389 - TEST_ASSERT_EQUAL( 0, entries[2].id ); // order 2 390 - } 391 - 392 - void test_sort_ordered_before_unordered() { 393 - CredentialEntry entries[3]; 394 - memset( entries, 0, sizeof( entries ) ); 395 - 396 - strcpy( entries[0].name, "X" ); 397 - entries[0].id = 0; 398 - entries[0].favorite = false; 399 - entries[0].sortOrder = 0xFF; 400 - strcpy( entries[1].name, "Y" ); 401 - entries[1].id = 1; 402 - entries[1].favorite = false; 403 - entries[1].sortOrder = 0; 404 - strcpy( entries[2].name, "Z" ); 405 - entries[2].id = 2; 406 - entries[2].favorite = false; 407 - entries[2].sortOrder = 0xFF; 408 - 409 - sortEntries( entries, 3 ); 410 - 411 - TEST_ASSERT_EQUAL( 1, entries[0].id ); // ordered entry comes first 412 - } 413 - 414 - void test_sort_favorites_with_order() { 415 - CredentialEntry entries[4]; 416 - memset( entries, 0, sizeof( entries ) ); 417 - 418 - entries[0] = { 3, "D", false, 0xFF }; 419 - entries[1] = { 1, "B", true, 1 }; 420 - entries[2] = { 0, "A", true, 0 }; 421 - entries[3] = { 2, "C", false, 0 }; 422 - 423 - sortEntries( entries, 4 ); 424 - 425 - // Favorites first (sorted by order), then non-favorites (sorted by order) 426 - TEST_ASSERT_EQUAL( 0, entries[0].id ); // fav, order 0 427 - TEST_ASSERT_EQUAL( 1, entries[1].id ); // fav, order 1 428 - TEST_ASSERT_EQUAL( 2, entries[2].id ); // non-fav, order 0 429 - TEST_ASSERT_EQUAL( 3, entries[3].id ); // non-fav, unordered 430 - } 431 - 432 - // --------------------------------------------------------------------------- 433 - // Export/Import JSON Format 434 - // --------------------------------------------------------------------------- 435 - void test_export_json_structure() { 436 - // Simulate the export format used by VaultStore::exportAll 437 - JsonDocument doc; 438 - doc["version"] = 1; 439 - doc["meta"] = "U1AB"; // base64 of some meta bytes 440 - 441 - JsonArray creds = doc["credentials"].to<JsonArray>(); 442 - JsonObject c0 = creds.add<JsonObject>(); 443 - c0["id"] = 0; 444 - c0["data"] = "AQIDBA=="; // base64 of {1,2,3,4} 445 - 446 - char buf[512]; 447 - size_t len = serializeJson( doc, buf, sizeof( buf ) ); 448 - TEST_ASSERT_GREATER_THAN( 0, len ); 449 - 450 - // Parse back 451 - JsonDocument doc2; 452 - DeserializationError err = deserializeJson( doc2, buf, len ); 453 - TEST_ASSERT_TRUE( err == DeserializationError::Ok ); 454 - TEST_ASSERT_EQUAL( 1, doc2["version"].as<int>() ); 455 - TEST_ASSERT_EQUAL_STRING( "U1AB", doc2["meta"] ); 456 - 457 - JsonArray creds2 = doc2["credentials"]; 458 - TEST_ASSERT_FALSE( creds2.isNull() ); 459 - TEST_ASSERT_EQUAL( 1, creds2.size() ); 460 - TEST_ASSERT_EQUAL( 0, creds2[0]["id"].as<int>() ); 461 - TEST_ASSERT_EQUAL_STRING( "AQIDBA==", creds2[0]["data"] ); 462 - } 463 - 464 - void test_import_rejects_wrong_version() { 465 - const char* json = "{\"version\":99,\"meta\":\"AA==\",\"credentials\":[]}"; 466 - JsonDocument doc; 467 - DeserializationError err = deserializeJson( doc, json ); 468 - TEST_ASSERT_TRUE( err == DeserializationError::Ok ); 469 - int version = doc["version"] | 0; 470 - TEST_ASSERT_NOT_EQUAL( 1, version ); // would be rejected 471 - } 472 - 473 - void test_import_rejects_missing_version() { 474 - const char* json = "{\"meta\":\"AA==\",\"credentials\":[]}"; 475 - JsonDocument doc; 476 - deserializeJson( doc, json ); 477 - int version = doc["version"] | 0; 478 - TEST_ASSERT_EQUAL( 0, version ); // default 0, not version 1 → rejected 479 - } 480 - 481 - void test_import_skips_invalid_credential_ids() { 482 - // IDs >= MAX_CREDENTIALS should be skipped 483 - JsonDocument doc; 484 - doc["version"] = 1; 485 - JsonArray creds = doc["credentials"].to<JsonArray>(); 486 - 487 - JsonObject good = creds.add<JsonObject>(); 488 - good["id"] = 0; 489 - good["data"] = "AQID"; 490 - 491 - JsonObject bad1 = creds.add<JsonObject>(); 492 - bad1["id"] = 0xFF; // >= MAX_CREDENTIALS 493 - bad1["data"] = "BQYH"; 494 - 495 - JsonObject bad2 = creds.add<JsonObject>(); 496 - bad2["id"] = MAX_CREDENTIALS; // exactly at limit 497 - bad2["data"] = "CAQE"; 498 - 499 - // Verify only id=0 would pass the check 500 - for ( JsonObject obj : creds ) { 501 - uint8_t id = obj["id"] | 0xFF; 502 - if ( id < MAX_CREDENTIALS ) { 503 - TEST_ASSERT_EQUAL( 0, id ); 504 - } 505 - } 506 - } 507 - 508 - void test_import_empty_credentials_array() { 509 - const char* json = "{\"version\":1,\"meta\":\"AAAA\",\"credentials\":[]}"; 510 - JsonDocument doc; 511 - DeserializationError err = deserializeJson( doc, json ); 512 - TEST_ASSERT_TRUE( err == DeserializationError::Ok ); 513 - 514 - JsonArray creds = doc["credentials"]; 515 - TEST_ASSERT_FALSE( creds.isNull() ); 516 - TEST_ASSERT_EQUAL( 0, creds.size() ); 517 - } 518 - 519 - // --------------------------------------------------------------------------- 520 - // CSV Parsing Edge Cases 521 - // --------------------------------------------------------------------------- 522 - void test_csv_with_commas_in_password() { 523 - // Quoted CSV: name,"user","pass,with,commas","url" 524 - // Simple strtok fails here — but Kleidos uses simple CSV. 525 - // This test documents the limitation. 526 - const char* csvLine = "Site,user,simplepass,https://example.com"; 527 - char line[256]; 528 - strncpy( line, csvLine, sizeof( line ) - 1 ); 529 - line[sizeof( line ) - 1] = '\0'; 530 - 531 - char* fields[4] = {}; 532 - int idx = 0; 533 - char* tok = strtok( line, "," ); 534 - while ( tok && idx < 4 ) { 535 - fields[idx++] = tok; 536 - tok = strtok( nullptr, "," ); 537 - } 538 - 539 - TEST_ASSERT_EQUAL( 4, idx ); 540 - TEST_ASSERT_EQUAL_STRING( "Site", fields[0] ); 541 - TEST_ASSERT_EQUAL_STRING( "user", fields[1] ); 542 - TEST_ASSERT_EQUAL_STRING( "simplepass", fields[2] ); 543 - TEST_ASSERT_EQUAL_STRING( "https://example.com", fields[3] ); 544 - } 545 - 546 - void test_csv_empty_fields() { 547 - const char* csvLine = "Site,,,"; 548 - char line[256]; 549 - strncpy( line, csvLine, sizeof( line ) - 1 ); 550 - line[sizeof( line ) - 1] = '\0'; 551 - 552 - char* fields[4] = {}; 553 - int idx = 0; 554 - char* tok = strtok( line, "," ); 555 - while ( tok && idx < 4 ) { 556 - fields[idx++] = tok; 557 - tok = strtok( nullptr, "," ); 558 - } 559 - 560 - // strtok skips consecutive delimiters — only 1 field found 561 - TEST_ASSERT_EQUAL( 1, idx ); 562 - TEST_ASSERT_EQUAL_STRING( "Site", fields[0] ); 563 - } 564 - 565 - void test_csv_url_with_multiple_colons() { 566 - // URL like https://user:pass@host:8080/path 567 - const char* csvLine = "MyServer,admin,secret,https://host:8080/path"; 568 - char line[256]; 569 - strncpy( line, csvLine, sizeof( line ) - 1 ); 570 - line[sizeof( line ) - 1] = '\0'; 571 - 572 - // Split only on first 3 commas (name,user,pass,rest_is_url) 573 - char* fields[4] = {}; 574 - int idx = 0; 575 - char* p = line; 576 - for ( int i = 0; i < 3 && p; i++ ) { 577 - fields[idx++] = p; 578 - char* comma = strchr( p, ',' ); 579 - if ( comma ) { 580 - *comma = '\0'; 581 - p = comma + 1; 582 - } else { 583 - p = nullptr; 584 - } 585 - } 586 - if ( p ) 587 - fields[idx++] = p; // remainder is URL 588 - 589 - TEST_ASSERT_EQUAL( 4, idx ); 590 - TEST_ASSERT_EQUAL_STRING( "MyServer", fields[0] ); 591 - TEST_ASSERT_EQUAL_STRING( "admin", fields[1] ); 592 - TEST_ASSERT_EQUAL_STRING( "secret", fields[2] ); 593 - TEST_ASSERT_EQUAL_STRING( "https://host:8080/path", fields[3] ); 594 - } 595 - 596 - // --------------------------------------------------------------------------- 597 - // Credential Wipe 598 - // --------------------------------------------------------------------------- 599 - void test_credential_wipe_zeros_all_fields() { 600 - Credential c; 601 - strcpy( c.name, "Secret" ); 602 - strcpy( c.user, "admin" ); 603 - strcpy( c.pass, "p@ssw0rd!" ); 604 - strcpy( c.url, "https://example.com" ); 605 - 606 - VaultCrypto::secureWipe( &c, sizeof( c ) ); 607 - 608 - // All bytes should be zero after wipe 609 - uint8_t* bytes = reinterpret_cast<uint8_t*>( &c ); 610 - bool allZero = true; 611 - for ( size_t i = 0; i < sizeof( c ); i++ ) { 612 - if ( bytes[i] != 0 ) { 613 - allZero = false; 614 - break; 615 - } 616 - } 617 - TEST_ASSERT_TRUE( allZero ); 618 - } 619 - 620 - // --------------------------------------------------------------------------- 621 - // Encrypt/Decrypt Roundtrip with Credential JSON 622 - // --------------------------------------------------------------------------- 623 - void test_encrypt_decrypt_credential_json() { 624 - // Simulate the full credential save/load cycle 625 - Credential cred; 626 - strcpy( cred.name, "TestSite" ); 627 - strcpy( cred.user, "user@test.com" ); 628 - strcpy( cred.pass, "MyP@ss123!" ); 629 - strcpy( cred.url, "https://test.com" ); 630 - 631 - // Serialize to JSON 632 - JsonDocument doc; 633 - doc["name"] = cred.name; 634 - doc["user"] = cred.user; 635 - doc["pass"] = cred.pass; 636 - doc["url"] = cred.url; 637 - 638 - uint8_t jsonBuf[512]; 639 - size_t jsonLen = serializeJson( doc, reinterpret_cast<char*>( jsonBuf ), sizeof( jsonBuf ) ); 640 - TEST_ASSERT_GREATER_THAN( 0, jsonLen ); 641 - 642 - // Derive key 643 - uint8_t salt[kSaltSize] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16 }; 644 - uint8_t key[kAesKeySize]; 645 - TEST_ASSERT_TRUE( VaultCrypto::deriveKey( "1234", 4, salt, kSaltSize, key, kAesKeySize ) ); 646 - 647 - // Encrypt 648 - uint8_t iv[kIvSize]; 649 - uint8_t cipherBuf[512 + kAesBlockSize]; 650 - size_t cipherLen = 0; 651 - TEST_ASSERT_TRUE( VaultCrypto::encrypt( key, jsonBuf, jsonLen, iv, cipherBuf, &cipherLen ) ); 652 - 653 - // Decrypt 654 - uint8_t plainBuf[512]; 655 - size_t plainLen = 0; 656 - TEST_ASSERT_TRUE( VaultCrypto::decrypt( key, iv, cipherBuf, cipherLen, plainBuf, &plainLen ) ); 657 - TEST_ASSERT_EQUAL( jsonLen, plainLen ); 658 - 659 - // Parse back 660 - JsonDocument doc2; 661 - DeserializationError err = deserializeJson( doc2, plainBuf, plainLen ); 662 - TEST_ASSERT_TRUE( err == DeserializationError::Ok ); 663 - 664 - TEST_ASSERT_EQUAL_STRING( "TestSite", doc2["name"] ); 665 - TEST_ASSERT_EQUAL_STRING( "user@test.com", doc2["user"] ); 666 - TEST_ASSERT_EQUAL_STRING( "MyP@ss123!", doc2["pass"] ); 667 - TEST_ASSERT_EQUAL_STRING( "https://test.com", doc2["url"] ); 668 - } 669 - 670 - void test_decrypt_wrong_key_fails_json_parse() { 671 - // Encrypt with key1, decrypt with key2 → invalid padding or garbled JSON 672 - uint8_t salt[kSaltSize] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16 }; 673 - uint8_t key1[kAesKeySize], key2[kAesKeySize]; 674 - VaultCrypto::deriveKey( "1111", 4, salt, kSaltSize, key1, kAesKeySize ); 675 - VaultCrypto::deriveKey( "2222", 4, salt, kSaltSize, key2, kAesKeySize ); 676 - 677 - const char* json = "{\"name\":\"Test\"}"; 678 - uint8_t iv[kIvSize]; 679 - uint8_t cipherBuf[256]; 680 - size_t cipherLen = 0; 681 - TEST_ASSERT_TRUE( VaultCrypto::encrypt( key1, reinterpret_cast<const uint8_t*>( json ), 682 - strlen( json ), iv, cipherBuf, &cipherLen ) ); 683 - 684 - uint8_t plainBuf[256]; 685 - size_t plainLen = 0; 686 - // Decrypt with wrong key — should fail (invalid padding) 687 - bool decryptOk = VaultCrypto::decrypt( key2, iv, cipherBuf, cipherLen, plainBuf, &plainLen ); 688 - if ( decryptOk ) { 689 - // Even if padding accidentally valid, JSON won't parse correctly 690 - JsonDocument doc; 691 - DeserializationError err = deserializeJson( doc, plainBuf, plainLen ); 692 - // Most likely garbled — but if by extreme chance it parses, name won't match 693 - if ( err == DeserializationError::Ok ) { 694 - TEST_ASSERT_NOT_EQUAL( 0, strcmp( "Test", doc["name"] | "" ) ); 695 - } 696 - } 697 - // If decrypt fails, that's the expected path 698 - TEST_ASSERT_TRUE( true ); // test passes either way 699 - } 700 - 701 - // --------------------------------------------------------------------------- 702 - // HMAC Integrity Verification Logic 703 - // --------------------------------------------------------------------------- 704 - void test_hmac_detects_tampered_data() { 705 - uint8_t key[kAesKeySize]; 706 - memset( key, 0x42, kAesKeySize ); 707 - 708 - uint8_t data[] = { 1, 2, 3, 4, 5, 6, 7, 8 }; 709 - uint8_t hmac1[kHmacSize]; 710 - TEST_ASSERT_TRUE( VaultCrypto::hmacSha256( key, kAesKeySize, data, sizeof( data ), hmac1 ) ); 711 - 712 - // Tamper with data 713 - data[3] = 99; 714 - uint8_t hmac2[kHmacSize]; 715 - TEST_ASSERT_TRUE( VaultCrypto::hmacSha256( key, kAesKeySize, data, sizeof( data ), hmac2 ) ); 716 - 717 - TEST_ASSERT_FALSE( VaultCrypto::constantTimeEqual( hmac1, hmac2, kHmacSize ) ); 718 - } 719 - 720 - void test_hmac_different_keys_different_mac() { 721 - uint8_t key1[kAesKeySize], key2[kAesKeySize]; 722 - memset( key1, 0x11, kAesKeySize ); 723 - memset( key2, 0x22, kAesKeySize ); 724 - 725 - uint8_t data[] = { 10, 20, 30 }; 726 - uint8_t hmac1[kHmacSize], hmac2[kHmacSize]; 727 - VaultCrypto::hmacSha256( key1, kAesKeySize, data, sizeof( data ), hmac1 ); 728 - VaultCrypto::hmacSha256( key2, kAesKeySize, data, sizeof( data ), hmac2 ); 729 - 730 - TEST_ASSERT_FALSE( VaultCrypto::constantTimeEqual( hmac1, hmac2, kHmacSize ) ); 731 - } 732 - 733 - // --------------------------------------------------------------------------- 734 - // TOTP JSON Serialization 735 - // --------------------------------------------------------------------------- 736 - void test_totp_hex_secret_roundtrip() { 737 - // Simulate the hex encoding used by saveTotp 738 - uint8_t secret[] = { 0xDE, 0xAD, 0xBE, 0xEF, 0x42 }; 739 - uint8_t secretLen = 5; 740 - 741 - char hexSecret[129]; 742 - for ( uint8_t i = 0; i < secretLen; i++ ) { 743 - snprintf( hexSecret + i * 2, 3, "%02x", secret[i] ); 744 - } 745 - hexSecret[secretLen * 2] = '\0'; 746 - 747 - TEST_ASSERT_EQUAL_STRING( "deadbeef42", hexSecret ); 748 - 749 - // Decode back 750 - uint8_t decoded[64]; 751 - size_t hexLen = strlen( hexSecret ); 752 - for ( size_t i = 0; i < hexLen / 2 && i < 64; i++ ) { 753 - char hex[3] = { hexSecret[i * 2], hexSecret[i * 2 + 1], '\0' }; 754 - decoded[i] = static_cast<uint8_t>( strtoul( hex, nullptr, 16 ) ); 755 - } 756 - 757 - TEST_ASSERT_EQUAL_MEMORY( secret, decoded, secretLen ); 758 - } 759 - 760 - // --------------------------------------------------------------------------- 761 - // Runner 762 - // --------------------------------------------------------------------------- 763 - int main( int argc, char** argv ) { 764 - UNITY_BEGIN(); 765 - 766 - // V2 meta structure 767 - RUN_TEST( test_v2_magic_bytes ); 768 - RUN_TEST( test_v2_meta_size ); 769 - RUN_TEST( test_v1_meta_size ); 770 - RUN_TEST( test_v2_is_distinguishable_from_v1 ); 771 - 772 - // V2 provision/unlock 773 - RUN_TEST( test_v2_provision_and_unlock_correct_pin ); 774 - RUN_TEST( test_v2_unlock_wrong_pin_fails ); 775 - RUN_TEST( test_v2_unlock_empty_pin_fails ); 776 - RUN_TEST( test_v2_different_pins_different_keys ); 777 - RUN_TEST( test_v2_same_pin_same_salt_same_key ); 778 - RUN_TEST( test_v2_pin_verifier_is_deterministic ); 779 - 780 - // V1→V2 migration 781 - RUN_TEST( test_v1_to_v2_migration_preserves_key ); 782 - 783 - // Favorites 784 - RUN_TEST( test_favorites_initially_all_off ); 785 - RUN_TEST( test_favorites_set_and_get ); 786 - RUN_TEST( test_favorites_clear ); 787 - RUN_TEST( test_favorites_adjacent_bits_independent ); 788 - 789 - // Ordering 790 - RUN_TEST( test_order_unset_is_0xFF ); 791 - RUN_TEST( test_order_set_custom ); 792 - RUN_TEST( test_order_empty_list ); 793 - 794 - // Sorting 795 - RUN_TEST( test_sort_favorites_first ); 796 - RUN_TEST( test_sort_by_order_within_same_fav_status ); 797 - RUN_TEST( test_sort_ordered_before_unordered ); 798 - RUN_TEST( test_sort_favorites_with_order ); 799 - 800 - // Export/Import JSON 801 - RUN_TEST( test_export_json_structure ); 802 - RUN_TEST( test_import_rejects_wrong_version ); 803 - RUN_TEST( test_import_rejects_missing_version ); 804 - RUN_TEST( test_import_skips_invalid_credential_ids ); 805 - RUN_TEST( test_import_empty_credentials_array ); 806 - 807 - // CSV edge cases 808 - RUN_TEST( test_csv_with_commas_in_password ); 809 - RUN_TEST( test_csv_empty_fields ); 810 - RUN_TEST( test_csv_url_with_multiple_colons ); 811 - 812 - // Credential integrity 813 - RUN_TEST( test_credential_wipe_zeros_all_fields ); 814 - RUN_TEST( test_encrypt_decrypt_credential_json ); 815 - RUN_TEST( test_decrypt_wrong_key_fails_json_parse ); 816 - RUN_TEST( test_hmac_detects_tampered_data ); 817 - RUN_TEST( test_hmac_different_keys_different_mac ); 818 - 819 - // TOTP 820 - RUN_TEST( test_totp_hex_secret_roundtrip ); 821 - 822 - return UNITY_END(); 823 - }
+193
test/totp/test_totp_generator/test_totp_generator.cpp
··· 1 + // Kleidos — Native test: TOTP Generator (RFC 6238 SHA-1 test vectors, base32, remaining) 2 + // Build: pio test -e native -f totp/test_totp_generator 3 + 4 + // --- File header ----------------------------------------------------------- 5 + // Group 1: module under test 6 + #include "totp/totp_generator.h" 7 + 8 + // Group 3: stdlib + Unity 9 + #include <cstdint> 10 + #include <cstring> 11 + 12 + #include <unity.h> 13 + 14 + // --- Constants ------------------------------------------------------------- 15 + static constexpr size_t kRfcSecretLen = 20; 16 + 17 + // --- State variables ------------------------------------------------------- 18 + // RFC 6238 Appendix B test secret (ASCII "12345678901234567890" = 20 bytes) 19 + static const uint8_t RFC_SECRET[] = { 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x30, 20 + 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x30 }; 21 + 22 + // --- Function declarations ------------------------------------------------- 23 + // HoldButton.cpp calls millis() directly; stub satisfies linker 24 + 25 + // --- setUp / tearDown ------------------------------------------------------ 26 + void setUp() {} 27 + void tearDown() { /* intentionally empty */ } 28 + 29 + // --- Tests ----------------------------------------------------------------- 30 + // RFC 6238 test vectors (SHA-1 only) 31 + static void test_totp_sha1_time59() { 32 + // ARRANGE + ACT 33 + uint32_t code = TotpGenerator::generate( RFC_SECRET, kRfcSecretLen, 59 ); 34 + 35 + // ASSERT 36 + TEST_ASSERT_EQUAL_UINT32( 287082, code ); 37 + } 38 + 39 + static void test_totp_sha1_time1111111109() { 40 + // ARRANGE + ACT 41 + uint32_t code = TotpGenerator::generate( RFC_SECRET, kRfcSecretLen, 1111111109ULL ); 42 + 43 + // ASSERT 44 + TEST_ASSERT_EQUAL_UINT32( 81804, code ); 45 + } 46 + 47 + static void test_totp_sha1_time1111111111() { 48 + // ARRANGE + ACT 49 + uint32_t code = TotpGenerator::generate( RFC_SECRET, kRfcSecretLen, 1111111111ULL ); 50 + 51 + // ASSERT 52 + TEST_ASSERT_EQUAL_UINT32( 50471, code ); 53 + } 54 + 55 + static void test_totp_sha1_time1234567890() { 56 + // ARRANGE + ACT 57 + uint32_t code = TotpGenerator::generate( RFC_SECRET, kRfcSecretLen, 1234567890ULL ); 58 + 59 + // ASSERT 60 + TEST_ASSERT_EQUAL_UINT32( 5924, code ); 61 + } 62 + 63 + static void test_totp_sha1_time2000000000() { 64 + // ARRANGE + ACT 65 + uint32_t code = TotpGenerator::generate( RFC_SECRET, kRfcSecretLen, 2000000000ULL ); 66 + 67 + // ASSERT 68 + TEST_ASSERT_EQUAL_UINT32( 279037, code ); 69 + } 70 + 71 + static void test_totp_sha1_time20000000000() { 72 + // ARRANGE + ACT 73 + uint32_t code = TotpGenerator::generate( RFC_SECRET, kRfcSecretLen, 20000000000ULL ); 74 + 75 + // ASSERT 76 + TEST_ASSERT_EQUAL_UINT32( 353130, code ); 77 + } 78 + 79 + // Base32 decode tests 80 + static void test_base32_decode_standard() { 81 + // ARRANGE 82 + uint8_t out[20]; 83 + 84 + // ACT ("MFRGG" decodes to "abc") 85 + size_t len = TotpGenerator::base32Decode( "MFRGG", 5, out, sizeof( out ) ); 86 + 87 + // ASSERT 88 + TEST_ASSERT_EQUAL( 3, len ); 89 + TEST_ASSERT_EQUAL( 0x61, out[0] ); // 'a' 90 + TEST_ASSERT_EQUAL( 0x62, out[1] ); // 'b' 91 + TEST_ASSERT_EQUAL( 0x63, out[2] ); // 'c' 92 + } 93 + 94 + static void test_base32_decode_with_padding() { 95 + // ARRANGE 96 + uint8_t out[20]; 97 + 98 + // ACT 99 + size_t len = TotpGenerator::base32Decode( "MFRGG===", 8, out, sizeof( out ) ); 100 + 101 + // ASSERT 102 + TEST_ASSERT_EQUAL( 3, len ); 103 + TEST_ASSERT_EQUAL( 'a', out[0] ); 104 + TEST_ASSERT_EQUAL( 'b', out[1] ); 105 + TEST_ASSERT_EQUAL( 'c', out[2] ); 106 + } 107 + 108 + static void test_base32_decode_lowercase() { 109 + // ARRANGE 110 + uint8_t out[20]; 111 + 112 + // ACT 113 + size_t len = TotpGenerator::base32Decode( "mfrgg", 5, out, sizeof( out ) ); 114 + 115 + // ASSERT 116 + TEST_ASSERT_EQUAL( 3, len ); 117 + TEST_ASSERT_EQUAL( 'a', out[0] ); 118 + } 119 + 120 + static void test_base32_decode_empty() { 121 + // ARRANGE 122 + uint8_t out[4]; 123 + 124 + // ACT 125 + size_t len = TotpGenerator::base32Decode( "", 0, out, sizeof( out ) ); 126 + 127 + // ASSERT 128 + TEST_ASSERT_EQUAL( 0, len ); 129 + } 130 + 131 + static void test_base32_decode_invalid_char() { 132 + // ARRANGE 133 + uint8_t out[20]; 134 + 135 + // ACT ('1' is invalid in base32) 136 + size_t len = TotpGenerator::base32Decode( "MFRG1", 5, out, sizeof( out ) ); 137 + 138 + // ASSERT 139 + TEST_ASSERT_EQUAL( 0, len ); 140 + } 141 + 142 + static void test_base32_decode_jbswy3dpehpk3pxp() { 143 + // ARRANGE 144 + uint8_t out[20]; 145 + 146 + // ACT ("JBSWY3DPEHPK3PXP" decodes to 10 bytes: 48 65 6c 6c 6f 21 de ad be ef) 147 + size_t len = TotpGenerator::base32Decode( "JBSWY3DPEHPK3PXP", 16, out, sizeof( out ) ); 148 + 149 + // ASSERT 150 + TEST_ASSERT_EQUAL( 10, len ); 151 + TEST_ASSERT_EQUAL( 0x48, out[0] ); // 'H' 152 + TEST_ASSERT_EQUAL( 0x65, out[1] ); // 'e' 153 + TEST_ASSERT_EQUAL( 0x6c, out[2] ); // 'l' 154 + TEST_ASSERT_EQUAL( 0x6c, out[3] ); // 'l' 155 + TEST_ASSERT_EQUAL( 0x6f, out[4] ); // 'o' 156 + TEST_ASSERT_EQUAL( 0x21, out[5] ); // '!' 157 + TEST_ASSERT_EQUAL( 0xde, out[6] ); 158 + TEST_ASSERT_EQUAL( 0xad, out[7] ); 159 + TEST_ASSERT_EQUAL( 0xbe, out[8] ); 160 + TEST_ASSERT_EQUAL( 0xef, out[9] ); 161 + } 162 + 163 + // Remaining seconds 164 + static void test_remaining_at_boundary() { 165 + // ARRANGE + ACT + ASSERT (boundary values for the 30-second window) 166 + TEST_ASSERT_EQUAL( 1, TotpGenerator::remainingSeconds( 29 ) ); 167 + TEST_ASSERT_EQUAL( 30, TotpGenerator::remainingSeconds( 30 ) ); 168 + TEST_ASSERT_EQUAL( 30, TotpGenerator::remainingSeconds( 0 ) ); 169 + TEST_ASSERT_EQUAL( 15, TotpGenerator::remainingSeconds( 15 ) ); 170 + } 171 + 172 + // --- Fake function definitions --------------------------------------------- 173 + 174 + // --- Helper definitions ---------------------------------------------------- 175 + 176 + // --- main() ---------------------------------------------------------------- 177 + int main( int /*argc*/, char** /*argv*/ ) { 178 + UNITY_BEGIN(); 179 + RUN_TEST( test_totp_sha1_time59 ); 180 + RUN_TEST( test_totp_sha1_time1111111109 ); 181 + RUN_TEST( test_totp_sha1_time1111111111 ); 182 + RUN_TEST( test_totp_sha1_time1234567890 ); 183 + RUN_TEST( test_totp_sha1_time2000000000 ); 184 + RUN_TEST( test_totp_sha1_time20000000000 ); 185 + RUN_TEST( test_base32_decode_standard ); 186 + RUN_TEST( test_base32_decode_with_padding ); 187 + RUN_TEST( test_base32_decode_lowercase ); 188 + RUN_TEST( test_base32_decode_empty ); 189 + RUN_TEST( test_base32_decode_invalid_char ); 190 + RUN_TEST( test_base32_decode_jbswy3dpehpk3pxp ); 191 + RUN_TEST( test_remaining_at_boundary ); 192 + return UNITY_END(); 193 + }
+303
test/totp/test_totp_generator_extended/test_totp_generator_extended.cpp
··· 1 + // Kleidos — Native test: TOTP Generator extended (period boundaries, code range, large timestamps) 2 + // Build: pio test -e native -f totp/test_totp_generator_extended 3 + 4 + // --- File header ----------------------------------------------------------- 5 + // Group 1: module under test 6 + #include "totp/totp_generator.h" 7 + 8 + // Group 3: stdlib + Unity 9 + #include <cstdint> 10 + #include <cstring> 11 + 12 + #include <unity.h> 13 + 14 + // --- Constants ------------------------------------------------------------- 15 + static constexpr size_t kRfcSecretLen = 20; 16 + 17 + // --- State variables ------------------------------------------------------- 18 + // RFC 6238 test secret "12345678901234567890" 19 + static const uint8_t RFC_SECRET[] = { 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x30, 20 + 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x30 }; 21 + 22 + // --- Function declarations ------------------------------------------------- 23 + // HoldButton.cpp calls millis() directly; stub satisfies linker 24 + 25 + // --- setUp / tearDown ------------------------------------------------------ 26 + void setUp() {} 27 + void tearDown() { /* intentionally empty */ } 28 + 29 + // --- Tests ----------------------------------------------------------------- 30 + // Period boundary tests 31 + static void test_code_changes_at_period_boundary() { 32 + // ARRANGE + ACT (at t=29 and t=30 the counter changes: 29/30=0, 30/30=1) 33 + uint32_t code_29 = TotpGenerator::generate( RFC_SECRET, kRfcSecretLen, 29 ); 34 + uint32_t code_30 = TotpGenerator::generate( RFC_SECRET, kRfcSecretLen, 30 ); 35 + 36 + // ASSERT 37 + TEST_ASSERT_NOT_EQUAL( code_29, code_30 ); 38 + } 39 + 40 + static void test_code_stable_within_period() { 41 + // ARRANGE + ACT (t=0..29 should all have the same counter 0) 42 + uint32_t code_0 = TotpGenerator::generate( RFC_SECRET, kRfcSecretLen, 0 ); 43 + uint32_t code_15 = TotpGenerator::generate( RFC_SECRET, kRfcSecretLen, 15 ); 44 + uint32_t code_29 = TotpGenerator::generate( RFC_SECRET, kRfcSecretLen, 29 ); 45 + 46 + // ASSERT 47 + TEST_ASSERT_EQUAL_UINT32( code_0, code_15 ); 48 + TEST_ASSERT_EQUAL_UINT32( code_0, code_29 ); 49 + } 50 + 51 + static void test_code_at_time_zero() { 52 + // ARRANGE + ACT (counter = 0/30 = 0, deterministic result) 53 + uint32_t code = TotpGenerator::generate( RFC_SECRET, kRfcSecretLen, 0 ); 54 + 55 + // ASSERT 56 + TEST_ASSERT_TRUE( code < 1000000 ); 57 + } 58 + 59 + // Code range validation 60 + static void test_code_always_6_digits_or_less() { 61 + // ARRANGE 62 + uint64_t testTimes[] = { 0, 1, 29, 30, 59, 60, 100, 1000, 999999, 1000000, UINT32_MAX }; 63 + 64 + // ACT + ASSERT 65 + for ( auto t : testTimes ) { 66 + uint32_t code = TotpGenerator::generate( RFC_SECRET, kRfcSecretLen, t ); 67 + TEST_ASSERT_TRUE( code < 1000000 ); 68 + } 69 + } 70 + 71 + // Different secrets produce different codes 72 + static void test_different_secrets_different_codes() { 73 + // ARRANGE 74 + uint8_t secret2[] = { 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 75 + 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0x10, 0x11, 0x12, 0x13 }; 76 + 77 + // ACT 78 + uint32_t code1 = TotpGenerator::generate( RFC_SECRET, kRfcSecretLen, 59 ); 79 + uint32_t code2 = TotpGenerator::generate( secret2, 20, 59 ); 80 + 81 + // ASSERT 82 + TEST_ASSERT_NOT_EQUAL( code1, code2 ); 83 + } 84 + 85 + // Short and long secrets 86 + static void test_minimum_secret_length() { 87 + // ARRANGE 88 + uint8_t shortSecret[] = { 0x42 }; // 1-byte secret 89 + 90 + // ACT 91 + uint32_t code = TotpGenerator::generate( shortSecret, 1, 59 ); 92 + 93 + // ASSERT 94 + TEST_ASSERT_TRUE( code < 1000000 ); 95 + } 96 + 97 + static void test_max_secret_length() { 98 + // ARRANGE (64-byte secret = TOTP_SECRET_MAX_LEN) 99 + uint8_t longSecret[64]; 100 + memset( longSecret, 0xAB, sizeof( longSecret ) ); 101 + 102 + // ACT 103 + uint32_t code = TotpGenerator::generate( longSecret, 64, 59 ); 104 + 105 + // ASSERT 106 + TEST_ASSERT_TRUE( code < 1000000 ); 107 + } 108 + 109 + // Large timestamps (far future) 110 + static void test_large_timestamp() { 111 + // ARRANGE + ACT (year 2100+) 112 + uint32_t code = TotpGenerator::generate( RFC_SECRET, kRfcSecretLen, 4102444800ULL ); 113 + 114 + // ASSERT 115 + TEST_ASSERT_TRUE( code < 1000000 ); 116 + } 117 + 118 + // remainingSeconds edge cases 119 + static void test_remaining_at_exact_period() { 120 + // ARRANGE + ACT + ASSERT (t=0: elapsed=0, remaining=30) 121 + TEST_ASSERT_EQUAL( 30, TotpGenerator::remainingSeconds( 0 ) ); 122 + } 123 + 124 + static void test_remaining_at_1_second() { 125 + // ARRANGE + ACT + ASSERT (t=1: elapsed=1, remaining=29) 126 + TEST_ASSERT_EQUAL( 29, TotpGenerator::remainingSeconds( 1 ) ); 127 + } 128 + 129 + static void test_remaining_at_29() { 130 + // ARRANGE + ACT + ASSERT (t=29: elapsed=29, remaining=1) 131 + TEST_ASSERT_EQUAL( 1, TotpGenerator::remainingSeconds( 29 ) ); 132 + } 133 + 134 + static void test_remaining_at_30() { 135 + // ARRANGE + ACT + ASSERT (t=30: elapsed=0 new period, remaining=30) 136 + TEST_ASSERT_EQUAL( 30, TotpGenerator::remainingSeconds( 30 ) ); 137 + } 138 + 139 + static void test_remaining_large_timestamp() { 140 + // ARRANGE + ACT + ASSERT (t=1234567890 % 30 = 0, remaining=30) 141 + TEST_ASSERT_EQUAL( 30, TotpGenerator::remainingSeconds( 1234567890 ) ); 142 + } 143 + 144 + // --- Tests ----------------------------------------------------------------- 145 + // Base32 decode extended tests 146 + static void test_base32_decode_single_char() { 147 + // ARRANGE 148 + uint8_t out[4] = {}; 149 + 150 + // ACT ("A" = 5 bits, not enough for 1 byte) 151 + size_t len = TotpGenerator::base32Decode( "A", 1, out, sizeof( out ) ); 152 + 153 + // ASSERT 154 + TEST_ASSERT_EQUAL( 0, len ); 155 + } 156 + 157 + static void test_base32_decode_two_chars() { 158 + // ARRANGE 159 + uint8_t out[4] = {}; 160 + 161 + // ACT ("MY": 10 bits, enough for exactly 1 byte) 162 + size_t len = TotpGenerator::base32Decode( "MY", 2, out, sizeof( out ) ); 163 + 164 + // ASSERT 165 + TEST_ASSERT_EQUAL( 1, len ); 166 + } 167 + 168 + static void test_base32_decode_full_block() { 169 + // ARRANGE 170 + uint8_t out[10] = {}; 171 + 172 + // ACT ("ORSXG5A" decodes to "test") 173 + size_t len = TotpGenerator::base32Decode( "ORSXG5A", 7, out, sizeof( out ) ); 174 + 175 + // ASSERT 176 + TEST_ASSERT_EQUAL( 4, len ); 177 + TEST_ASSERT_EQUAL( 't', out[0] ); 178 + TEST_ASSERT_EQUAL( 'e', out[1] ); 179 + TEST_ASSERT_EQUAL( 's', out[2] ); 180 + TEST_ASSERT_EQUAL( 't', out[3] ); 181 + } 182 + 183 + static void test_base32_decode_mixed_case() { 184 + // ARRANGE 185 + uint8_t out1[10] = {}, out2[10] = {}; 186 + 187 + // ACT 188 + size_t len1 = TotpGenerator::base32Decode( "ORSXG5A", 7, out1, sizeof( out1 ) ); 189 + size_t len2 = TotpGenerator::base32Decode( "orsxg5a", 7, out2, sizeof( out2 ) ); 190 + 191 + // ASSERT 192 + TEST_ASSERT_EQUAL( len1, len2 ); 193 + TEST_ASSERT_EQUAL_MEMORY( out1, out2, len1 ); 194 + } 195 + 196 + static void test_base32_buffer_too_small() { 197 + // ARRANGE 198 + uint8_t out[1] = {}; // buffer of 1 byte; "MFRGG" decodes to 3 199 + 200 + // ACT 201 + size_t len = TotpGenerator::base32Decode( "MFRGG", 5, out, sizeof( out ) ); 202 + 203 + // ASSERT (overflow protection) 204 + TEST_ASSERT_EQUAL( 0, len ); 205 + } 206 + 207 + static void test_base32_decode_with_number_1() { 208 + // ARRANGE 209 + uint8_t out[4] = {}; 210 + 211 + // ACT ('1' is not valid base32; valid chars: A-Z, 2-7) 212 + size_t len = TotpGenerator::base32Decode( "M1RGG", 5, out, sizeof( out ) ); 213 + 214 + // ASSERT 215 + TEST_ASSERT_EQUAL( 0, len ); 216 + } 217 + 218 + static void test_base32_decode_with_number_0() { 219 + // ARRANGE 220 + uint8_t out[4] = {}; 221 + 222 + // ACT 223 + size_t len = TotpGenerator::base32Decode( "M0RGG", 5, out, sizeof( out ) ); 224 + 225 + // ASSERT 226 + TEST_ASSERT_EQUAL( 0, len ); 227 + } 228 + 229 + static void test_base32_decode_with_number_8() { 230 + // ARRANGE 231 + uint8_t out[4] = {}; 232 + 233 + // ACT 234 + size_t len = TotpGenerator::base32Decode( "M8RGG", 5, out, sizeof( out ) ); 235 + 236 + // ASSERT 237 + TEST_ASSERT_EQUAL( 0, len ); 238 + } 239 + 240 + static void test_base32_decode_null_input() { 241 + // ARRANGE 242 + uint8_t out[4] = {}; 243 + 244 + // ACT 245 + size_t len = TotpGenerator::base32Decode( nullptr, 0, out, sizeof( out ) ); 246 + 247 + // ASSERT 248 + TEST_ASSERT_EQUAL( 0, len ); 249 + } 250 + 251 + static void test_base32_decode_null_output() { 252 + // ARRANGE + ACT 253 + size_t len = TotpGenerator::base32Decode( "MFRGG", 5, nullptr, 0 ); 254 + 255 + // ASSERT 256 + TEST_ASSERT_EQUAL( 0, len ); 257 + } 258 + 259 + // --- Fake function definitions --------------------------------------------- 260 + 261 + // --- Helper definitions ---------------------------------------------------- 262 + 263 + // --- main() ---------------------------------------------------------------- 264 + int main( int /*argc*/, char** /*argv*/ ) { 265 + UNITY_BEGIN(); 266 + 267 + // Period boundary 268 + RUN_TEST( test_code_changes_at_period_boundary ); 269 + RUN_TEST( test_code_stable_within_period ); 270 + RUN_TEST( test_code_at_time_zero ); 271 + 272 + // Code range 273 + RUN_TEST( test_code_always_6_digits_or_less ); 274 + 275 + // Secret variations 276 + RUN_TEST( test_different_secrets_different_codes ); 277 + RUN_TEST( test_minimum_secret_length ); 278 + RUN_TEST( test_max_secret_length ); 279 + 280 + // Large timestamps 281 + RUN_TEST( test_large_timestamp ); 282 + 283 + // remainingSeconds 284 + RUN_TEST( test_remaining_at_exact_period ); 285 + RUN_TEST( test_remaining_at_1_second ); 286 + RUN_TEST( test_remaining_at_29 ); 287 + RUN_TEST( test_remaining_at_30 ); 288 + RUN_TEST( test_remaining_large_timestamp ); 289 + 290 + // Base32 extended 291 + RUN_TEST( test_base32_decode_single_char ); 292 + RUN_TEST( test_base32_decode_two_chars ); 293 + RUN_TEST( test_base32_decode_full_block ); 294 + RUN_TEST( test_base32_decode_mixed_case ); 295 + RUN_TEST( test_base32_buffer_too_small ); 296 + RUN_TEST( test_base32_decode_with_number_1 ); 297 + RUN_TEST( test_base32_decode_with_number_0 ); 298 + RUN_TEST( test_base32_decode_with_number_8 ); 299 + RUN_TEST( test_base32_decode_null_input ); 300 + RUN_TEST( test_base32_decode_null_output ); 301 + 302 + return UNITY_END(); 303 + }
+253
test/ui/test_brand_registry/test_brand_registry.cpp
··· 1 + // Kleidos — Native test: BrandRegistry (lookup, canonicalization, RLE decode) 2 + // Build: pio test -e native -f ui/test_brand_registry 3 + 4 + // --- File header ----------------------------------------------------------- 5 + // Includes 6 + 7 + // Group 1: modules under test 8 + #include "ui/brands/brand_bitmap.h" 9 + #include "ui/brands/brand_registry.h" 10 + 11 + // Group 3: stdlib + Unity 12 + #include <unity.h> 13 + 14 + using namespace kleidos::ui::brands; 15 + 16 + // --- Constants ------------------------------------------------------------- 17 + 18 + // (none) 19 + 20 + // --- State variables ------------------------------------------------------- 21 + 22 + // (none — all test state is local to each test function) 23 + 24 + // --- Function declarations ------------------------------------------------- 25 + 26 + // --- setUp / tearDown ------------------------------------------------------ 27 + // setUp / tearDown 28 + 29 + void setUp() {} 30 + void tearDown() { /* intentionally empty */ } 31 + 32 + // --- Tests ----------------------------------------------------------------- 33 + // Test functions 34 + 35 + static void test_sizePixels_all_variants_return_correct_pixel_count() { 36 + // ARRANGE + ACT 37 + 38 + // ASSERT 39 + TEST_ASSERT_EQUAL_UINT8( 16, sizePixels( Size::S16 ) ); 40 + TEST_ASSERT_EQUAL_UINT8( 24, sizePixels( Size::S24 ) ); 41 + TEST_ASSERT_EQUAL_UINT8( 32, sizePixels( Size::S32 ) ); 42 + TEST_ASSERT_EQUAL_UINT8( 48, sizePixels( Size::S48 ) ); 43 + } 44 + 45 + static void test_canonicalize_url_strips_scheme_user_www_port_and_path() { 46 + // ARRANGE 47 + char out[kMaxCanonicalDomainLen + 1] = {}; 48 + 49 + // ACT 50 + const bool ok = 51 + canonicalizeDomain( " HTTPS://user@WWW.GitHub.com:443/login?x=1 ", out, sizeof( out ) ); 52 + 53 + // ASSERT 54 + TEST_ASSERT_TRUE( ok ); 55 + TEST_ASSERT_EQUAL_STRING( "github.com", out ); 56 + } 57 + 58 + static void test_canonicalize_plain_service_name_lowercased() { 59 + // ARRANGE 60 + char out[kMaxCanonicalDomainLen + 1] = {}; 61 + 62 + // ACT 63 + const bool ok = canonicalizeDomain( "GitHub", out, sizeof( out ) ); 64 + 65 + // ASSERT 66 + TEST_ASSERT_TRUE( ok ); 67 + TEST_ASSERT_EQUAL_STRING( "github", out ); 68 + } 69 + 70 + static void test_canonicalize_whitespace_input_returns_false() { 71 + // ARRANGE 72 + char out[kMaxCanonicalDomainLen + 1] = {}; 73 + 74 + // ACT 75 + const bool ok = canonicalizeDomain( " ", out, sizeof( out ) ); 76 + 77 + // ASSERT 78 + TEST_ASSERT_FALSE( ok ); 79 + TEST_ASSERT_EQUAL_STRING( "", out ); 80 + } 81 + 82 + static void test_hashDomain_url_input_matches_canonical() { 83 + // ARRANGE + ACT 84 + const uint32_t direct = fnv1a32( "github.com" ); 85 + const uint32_t via_url = hashDomain( "https://www.github.com/settings" ); 86 + 87 + // ASSERT 88 + TEST_ASSERT_EQUAL_UINT32( direct, via_url ); 89 + } 90 + 91 + static void test_findBrand_exact_domain_returns_correct_entry() { 92 + // ARRANGE + ACT 93 + const BrandEntry* entry = findBrand( "github.com", Size::S24, Style::Auto ); 94 + 95 + // ASSERT 96 + TEST_ASSERT_NOT_NULL( entry ); 97 + TEST_ASSERT_EQUAL_UINT32( 0x49361819UL, entry->domainHash ); 98 + TEST_ASSERT_EQUAL_UINT8( 24, entry->width ); 99 + } 100 + 101 + static void test_findBrand_service_slug_returns_correct_entry() { 102 + // ARRANGE + ACT 103 + const BrandEntry* entry = findBrand( "GitHub", Size::S24, Style::Auto ); 104 + 105 + // ASSERT 106 + TEST_ASSERT_NOT_NULL( entry ); 107 + TEST_ASSERT_EQUAL_UINT32( 0x22DC7932UL, entry->domainHash ); 108 + } 109 + 110 + static void test_fixture_lookup_subdomain_falls_back_to_root() { 111 + // ARRANGE + ACT 112 + const BrandEntry* entry = findBrand( "mail.google.com", Size::S24, Style::Auto ); 113 + 114 + // ASSERT 115 + TEST_ASSERT_NOT_NULL( entry ); 116 + TEST_ASSERT_EQUAL_UINT32( 0x7796C245UL, entry->domainHash ); 117 + } 118 + 119 + static void test_findBrand_wrong_size_returns_null() { 120 + // ARRANGE + ACT + ASSERT 121 + TEST_ASSERT_NULL( findBrand( "github.com", Size::S32, Style::Auto ) ); 122 + } 123 + 124 + static void test_findBrand_wrong_style_returns_null() { 125 + // ARRANGE + ACT + ASSERT 126 + TEST_ASSERT_NULL( findBrand( "github.com", Size::S24, Style::Mono ) ); 127 + TEST_ASSERT_NOT_NULL( findBrand( "github.com", Size::S24, Style::HighContrast ) ); 128 + } 129 + 130 + static void test_hasBrand_known_domain_returns_true() { 131 + // ARRANGE + ACT + ASSERT 132 + TEST_ASSERT_TRUE( hasBrand( "https://github.com" ) ); 133 + TEST_ASSERT_FALSE( hasBrand( "example.invalid" ) ); 134 + } 135 + 136 + static void test_rle_decode_mixed_encoding_produces_correct_output() { 137 + // ARRANGE 138 + const uint8_t encoded[] = { 139 + 0x02, 0x10, 0x11, 0x12, 0x82, 0x7F, 0x01, 0x20, 0x21, 140 + }; 141 + const uint8_t expected[] = { 0x10, 0x11, 0x12, 0x7F, 0x7F, 0x7F, 0x20, 0x21 }; 142 + uint8_t decoded[8] = {}; 143 + 144 + // ACT 145 + const bitmap::DecodeResult result = 146 + bitmap::decodeRle( encoded, sizeof( encoded ), decoded, sizeof( decoded ) ); 147 + 148 + // ASSERT 149 + TEST_ASSERT_EQUAL_UINT8( static_cast<uint8_t>( bitmap::DecodeStatus::Ok ), 150 + static_cast<uint8_t>( result.status ) ); 151 + TEST_ASSERT_EQUAL_size_t( sizeof( encoded ), result.bytesRead ); 152 + TEST_ASSERT_EQUAL_size_t( sizeof( decoded ), result.bytesWritten ); 153 + TEST_ASSERT_EQUAL_UINT8_ARRAY( expected, decoded, sizeof( expected ) ); 154 + } 155 + 156 + static void test_rle_decode_rejects_truncated_run() { 157 + // ARRANGE 158 + const uint8_t encoded[] = { 0x82 }; 159 + uint8_t decoded[3] = {}; 160 + 161 + // ACT 162 + const bitmap::DecodeResult result = 163 + bitmap::decodeRle( encoded, sizeof( encoded ), decoded, sizeof( decoded ) ); 164 + 165 + // ASSERT 166 + TEST_ASSERT_EQUAL_UINT8( static_cast<uint8_t>( bitmap::DecodeStatus::InputTooShort ), 167 + static_cast<uint8_t>( result.status ) ); 168 + } 169 + 170 + static void test_decode_color_entry_composes_alpha_mask() { 171 + // ARRANGE 172 + const uint8_t atlas[] = { 173 + 0x07, 0xF8, 0x00, 0x07, 0xE0, 0x00, 0x1F, 0xFF, 0xFF, 0x00, 0xA0, 174 + }; 175 + const BrandEntry entry{ 0x12345678UL, 176 + 0, 177 + sizeof( atlas ), 178 + 0xF800, 179 + 2, 180 + 2, 181 + static_cast<uint8_t>( AssetFamily::Color ), 182 + kFlagHasAlphaMask }; 183 + uint16_t pixels[4] = {}; 184 + uint8_t scratch[bitmap::maskByteCount( 2, 2 )] = {}; 185 + 186 + // ACT 187 + const bool ok = bitmap::decodeEntryToRgb565( entry, atlas, sizeof( atlas ), 0x0000, 0xFFFF, 188 + false, scratch, sizeof( scratch ), pixels, 4 ); 189 + 190 + // ASSERT 191 + TEST_ASSERT_TRUE( ok ); 192 + TEST_ASSERT_EQUAL_HEX16( 0xF800, pixels[0] ); 193 + TEST_ASSERT_EQUAL_HEX16( 0x0000, pixels[1] ); 194 + TEST_ASSERT_EQUAL_HEX16( 0x001F, pixels[2] ); 195 + TEST_ASSERT_EQUAL_HEX16( 0x0000, pixels[3] ); 196 + } 197 + 198 + static void test_decode_mono_entry_tints_mask() { 199 + // ARRANGE 200 + const uint8_t atlas[] = { 0x00, 0x90 }; 201 + const BrandEntry entry{ 0x12345678UL, 202 + 0, 203 + sizeof( atlas ), 204 + 0xFFFF, 205 + 2, 206 + 2, 207 + static_cast<uint8_t>( AssetFamily::Mono ), 208 + 0 }; 209 + uint16_t pixels[4] = {}; 210 + uint8_t scratch[bitmap::maskByteCount( 2, 2 )] = {}; 211 + 212 + // ACT 213 + const bool ok = bitmap::decodeEntryToRgb565( entry, atlas, sizeof( atlas ), 0x1111, 0xEEEE, 214 + false, scratch, sizeof( scratch ), pixels, 4 ); 215 + 216 + // ASSERT 217 + TEST_ASSERT_TRUE( ok ); 218 + TEST_ASSERT_EQUAL_HEX16( 0xEEEE, pixels[0] ); 219 + TEST_ASSERT_EQUAL_HEX16( 0x1111, pixels[1] ); 220 + TEST_ASSERT_EQUAL_HEX16( 0x1111, pixels[2] ); 221 + TEST_ASSERT_EQUAL_HEX16( 0xEEEE, pixels[3] ); 222 + } 223 + 224 + // --- Fake function definitions --------------------------------------------- 225 + // Fake function definitions 226 + 227 + // --- Helper definitions ---------------------------------------------------- 228 + // Helper / arrange definitions 229 + 230 + // (none) 231 + 232 + // --- main() ---------------------------------------------------------------- 233 + // main 234 + 235 + int main( int /*argc*/, char** /*argv*/ ) { 236 + UNITY_BEGIN(); 237 + RUN_TEST( test_sizePixels_all_variants_return_correct_pixel_count ); 238 + RUN_TEST( test_canonicalize_url_strips_scheme_user_www_port_and_path ); 239 + RUN_TEST( test_canonicalize_plain_service_name_lowercased ); 240 + RUN_TEST( test_canonicalize_whitespace_input_returns_false ); 241 + RUN_TEST( test_hashDomain_url_input_matches_canonical ); 242 + RUN_TEST( test_findBrand_exact_domain_returns_correct_entry ); 243 + RUN_TEST( test_findBrand_service_slug_returns_correct_entry ); 244 + RUN_TEST( test_fixture_lookup_subdomain_falls_back_to_root ); 245 + RUN_TEST( test_findBrand_wrong_size_returns_null ); 246 + RUN_TEST( test_findBrand_wrong_style_returns_null ); 247 + RUN_TEST( test_hasBrand_known_domain_returns_true ); 248 + RUN_TEST( test_rle_decode_mixed_encoding_produces_correct_output ); 249 + RUN_TEST( test_rle_decode_rejects_truncated_run ); 250 + RUN_TEST( test_decode_color_entry_composes_alpha_mask ); 251 + RUN_TEST( test_decode_mono_entry_tints_mask ); 252 + return UNITY_END(); 253 + }
+347
test/ui/test_hold_button/test_hold_button.cpp
··· 1 + // Kleidos — Native test: HoldButton (state machine: tap, hold, arc) 2 + // Build: pio test -e native -f ui/test_hold_button 3 + 4 + // --- File header ----------------------------------------------------------- 5 + // Includes 6 + 7 + // Group 1: modules under test 8 + #include "ui/input/hold_button.h" 9 + #include "ui/input/virtual_button_input.h" 10 + 11 + // Group 2: other Kleidos headers 12 + #include "platform/clock.h" 13 + 14 + // Group 3: stdlib + Unity 15 + #include <cstdint> 16 + 17 + #include <unity.h> 18 + 19 + // --- Constants ------------------------------------------------------------- 20 + 21 + static constexpr uint32_t kHoldThresholdMs = 800U; 22 + static constexpr uint32_t kTapDurationMs = 50U; 23 + 24 + // --- State variables ------------------------------------------------------- 25 + 26 + struct { 27 + uint32_t fakeMs; 28 + } spy; 29 + 30 + // --- Function declarations ------------------------------------------------- 31 + 32 + static uint32_t fakeMillisProvider(); 33 + static void advanceTime( uint32_t ms ); 34 + 35 + // --- setUp / tearDown ------------------------------------------------------ 36 + // setUp / tearDown 37 + 38 + void setUp() { 39 + spy = {}; 40 + kleidos::platform::clock::setTestMillisProvider( fakeMillisProvider ); 41 + } 42 + 43 + void tearDown() { 44 + kleidos::platform::clock::clearTestMillisProvider(); 45 + } 46 + 47 + // --- Tests ----------------------------------------------------------------- 48 + // Test functions 49 + 50 + static void test_tap_on_short_press_is_detected() { 51 + // ARRANGE 52 + VirtualButtonInput btn; 53 + HoldButton hold; 54 + 55 + // ACT — press and verify not yet tapped 56 + btn.setState( true ); 57 + hold.update( btn ); 58 + TEST_ASSERT_FALSE( hold.tapped() ); 59 + 60 + advanceTime( kTapDurationMs ); 61 + btn.setState( false ); 62 + hold.update( btn ); 63 + 64 + // ASSERT 65 + TEST_ASSERT_TRUE( hold.tapped() ); 66 + } 67 + 68 + static void test_tap_flag_after_read_is_cleared() { 69 + // ARRANGE 70 + VirtualButtonInput btn; 71 + HoldButton hold; 72 + 73 + btn.setState( true ); 74 + hold.update( btn ); 75 + advanceTime( kTapDurationMs ); 76 + btn.setState( false ); 77 + hold.update( btn ); 78 + 79 + // ACT — read tap (consumes the flag) 80 + TEST_ASSERT_TRUE( hold.tapped() ); 81 + 82 + // ASSERT — next frame: flag cleared 83 + hold.update( btn ); 84 + TEST_ASSERT_FALSE( hold.tapped() ); 85 + } 86 + 87 + static void test_hold_at_threshold_fires() { 88 + // ARRANGE 89 + VirtualButtonInput btn; 90 + HoldButton hold; 91 + 92 + // ACT + ASSERT — step through time to threshold 93 + btn.setState( true ); 94 + hold.update( btn ); 95 + TEST_ASSERT_FALSE( hold.checkHold( kHoldThresholdMs ) ); 96 + 97 + advanceTime( 500 ); 98 + hold.update( btn ); 99 + TEST_ASSERT_FALSE( hold.checkHold( kHoldThresholdMs ) ); 100 + TEST_ASSERT_FALSE( hold.tapped() ); 101 + 102 + advanceTime( 300 ); // total 800 ms — threshold reached 103 + hold.update( btn ); 104 + TEST_ASSERT_TRUE( hold.checkHold( kHoldThresholdMs ) ); 105 + 106 + // ASSERT — must NOT fire again 107 + advanceTime( 100 ); 108 + hold.update( btn ); 109 + TEST_ASSERT_FALSE( hold.checkHold( kHoldThresholdMs ) ); 110 + } 111 + 112 + static void test_hold_on_release_suppresses_tap() { 113 + // ARRANGE 114 + VirtualButtonInput btn; 115 + HoldButton hold; 116 + 117 + btn.setState( true ); 118 + hold.update( btn ); 119 + advanceTime( 1000 ); 120 + hold.update( btn ); 121 + hold.checkHold( kHoldThresholdMs ); // fire hold event 122 + 123 + // ACT — release after hold fired 124 + advanceTime( 100 ); 125 + btn.setState( false ); 126 + hold.update( btn ); 127 + 128 + // ASSERT 129 + TEST_ASSERT_FALSE( hold.tapped() ); 130 + } 131 + 132 + static void test_release_before_threshold_is_tap() { 133 + // ARRANGE 134 + VirtualButtonInput btn; 135 + HoldButton hold; 136 + 137 + btn.setState( true ); 138 + hold.update( btn ); 139 + advanceTime( 400 ); 140 + hold.update( btn ); 141 + TEST_ASSERT_FALSE( hold.checkHold( kHoldThresholdMs ) ); // not yet at threshold 142 + 143 + // ACT — release before threshold 144 + btn.setState( false ); 145 + hold.update( btn ); 146 + 147 + // ASSERT 148 + TEST_ASSERT_TRUE( hold.tapped() ); 149 + } 150 + 151 + static void test_reset_with_pending_tap_clears_state() { 152 + // ARRANGE 153 + VirtualButtonInput btn; 154 + HoldButton hold; 155 + 156 + btn.setState( true ); 157 + hold.update( btn ); 158 + advanceTime( kTapDurationMs ); 159 + btn.setState( false ); 160 + hold.update( btn ); // tap pending 161 + 162 + // ACT 163 + hold.reset(); 164 + 165 + // ASSERT 166 + TEST_ASSERT_FALSE( hold.tapped() ); 167 + } 168 + 169 + static void test_elapsed_while_pressed_tracks_duration() { 170 + // ARRANGE 171 + VirtualButtonInput btn; 172 + HoldButton hold; 173 + 174 + TEST_ASSERT_EQUAL_UINT32( 0, hold.elapsed() ); 175 + 176 + // ACT 177 + btn.setState( true ); 178 + hold.update( btn ); 179 + advanceTime( 350 ); 180 + hold.update( btn ); 181 + 182 + // ASSERT — elapsed while pressed 183 + TEST_ASSERT_EQUAL_UINT32( 350, hold.elapsed() ); 184 + 185 + // ACT — release 186 + btn.setState( false ); 187 + hold.update( btn ); 188 + 189 + // ASSERT — elapsed reset after release 190 + TEST_ASSERT_EQUAL_UINT32( 0, hold.elapsed() ); 191 + } 192 + 193 + static void test_virtual_button_events_are_one_shot() { 194 + // ARRANGE + ACT 195 + VirtualButtonInput btn; 196 + btn.setState( true ); 197 + 198 + // ASSERT — press consumed on first read 199 + TEST_ASSERT_TRUE( btn.wasPressed() ); 200 + TEST_ASSERT_FALSE( btn.wasPressed() ); // consumed 201 + 202 + btn.setState( false ); 203 + 204 + // ASSERT — release consumed on first read 205 + TEST_ASSERT_TRUE( btn.wasReleased() ); 206 + TEST_ASSERT_FALSE( btn.wasReleased() ); // consumed 207 + } 208 + 209 + static void test_virtual_button_same_state_no_events() { 210 + // ARRANGE 211 + VirtualButtonInput btn; 212 + 213 + // ACT 214 + btn.setState( false ); 215 + 216 + // ASSERT — no events for initial false state 217 + TEST_ASSERT_FALSE( btn.wasPressed() ); 218 + TEST_ASSERT_FALSE( btn.wasReleased() ); 219 + 220 + btn.setState( true ); 221 + btn.wasPressed(); // consume 222 + btn.setState( true ); // same state again 223 + 224 + // ASSERT — no spurious events on repeated same state 225 + TEST_ASSERT_FALSE( btn.wasPressed() ); 226 + TEST_ASSERT_FALSE( btn.wasReleased() ); 227 + } 228 + 229 + static void test_multiple_tap_hold_cycles_state_resets() { 230 + // ARRANGE 231 + VirtualButtonInput btn; 232 + HoldButton hold; 233 + 234 + // ACT + ASSERT — cycle 1: tap 235 + btn.setState( true ); 236 + hold.update( btn ); 237 + advanceTime( 30 ); 238 + btn.setState( false ); 239 + hold.update( btn ); 240 + TEST_ASSERT_TRUE( hold.tapped() ); 241 + 242 + // Cycle 2: hold 243 + advanceTime( 100 ); 244 + btn.setState( true ); 245 + hold.update( btn ); 246 + advanceTime( 900 ); 247 + hold.update( btn ); 248 + TEST_ASSERT_TRUE( hold.checkHold( kHoldThresholdMs ) ); 249 + 250 + // Cycle 3: tap again (proves state resets between cycles) 251 + advanceTime( 100 ); 252 + btn.setState( false ); 253 + hold.update( btn ); 254 + advanceTime( 100 ); 255 + btn.setState( true ); 256 + hold.update( btn ); 257 + advanceTime( kTapDurationMs ); 258 + btn.setState( false ); 259 + hold.update( btn ); 260 + TEST_ASSERT_TRUE( hold.tapped() ); 261 + } 262 + 263 + static void test_hold_on_millis_rollover_fires_correctly() { 264 + // ARRANGE — set time near uint32_t max to test wrap 265 + spy.fakeMs = UINT32_MAX - 100U; 266 + VirtualButtonInput btn; 267 + HoldButton hold; 268 + 269 + // ACT 270 + btn.setState( true ); 271 + hold.update( btn ); 272 + spy.fakeMs = 50U; // wrap: 150 ms elapsed 273 + hold.update( btn ); 274 + 275 + // ASSERT 276 + TEST_ASSERT_TRUE( hold.elapsed() > 100 ); 277 + TEST_ASSERT_TRUE( hold.checkHold( 150 ) ); 278 + } 279 + 280 + static void test_zero_duration_press_is_tap() { 281 + // ARRANGE 282 + VirtualButtonInput btn; 283 + HoldButton hold; 284 + 285 + // ACT — press and release at the same millisecond 286 + btn.setState( true ); 287 + hold.update( btn ); 288 + btn.setState( false ); 289 + hold.update( btn ); 290 + 291 + // ASSERT 292 + TEST_ASSERT_TRUE( hold.tapped() ); 293 + } 294 + 295 + static void test_elapsed_while_pressed_is_nonzero() { 296 + // ARRANGE 297 + VirtualButtonInput btn; 298 + HoldButton hold; 299 + 300 + // ACT 301 + btn.setState( true ); 302 + hold.update( btn ); 303 + advanceTime( 100 ); 304 + hold.update( btn ); 305 + 306 + // ASSERT — elapsed > 0 while pressed 307 + TEST_ASSERT_TRUE( hold.elapsed() > 0 ); 308 + 309 + btn.setState( false ); 310 + hold.update( btn ); 311 + TEST_ASSERT_EQUAL_UINT32( 0, hold.elapsed() ); 312 + } 313 + 314 + // --- Fake function definitions --------------------------------------------- 315 + // Fake function definitions 316 + 317 + static uint32_t fakeMillisProvider() { 318 + return spy.fakeMs; 319 + } 320 + 321 + // --- Helper definitions ---------------------------------------------------- 322 + // Helper / arrange definitions 323 + 324 + static void advanceTime( uint32_t ms ) { 325 + spy.fakeMs += ms; 326 + } 327 + 328 + // --- main() ---------------------------------------------------------------- 329 + // main 330 + 331 + int main( int /*argc*/, char** /*argv*/ ) { 332 + UNITY_BEGIN(); 333 + RUN_TEST( test_tap_on_short_press_is_detected ); 334 + RUN_TEST( test_tap_flag_after_read_is_cleared ); 335 + RUN_TEST( test_hold_at_threshold_fires ); 336 + RUN_TEST( test_hold_on_release_suppresses_tap ); 337 + RUN_TEST( test_release_before_threshold_is_tap ); 338 + RUN_TEST( test_reset_with_pending_tap_clears_state ); 339 + RUN_TEST( test_elapsed_while_pressed_tracks_duration ); 340 + RUN_TEST( test_virtual_button_events_are_one_shot ); 341 + RUN_TEST( test_virtual_button_same_state_no_events ); 342 + RUN_TEST( test_multiple_tap_hold_cycles_state_resets ); 343 + RUN_TEST( test_hold_on_millis_rollover_fires_correctly ); 344 + RUN_TEST( test_zero_duration_press_is_tap ); 345 + RUN_TEST( test_elapsed_while_pressed_is_nonzero ); 346 + return UNITY_END(); 347 + }
+296
test/ui/test_popup_queue/test_popup_queue.cpp
··· 1 + // Kleidos — Native test: PopupQueue (FIFO semantics, timeouts, modal, spinner) 2 + // Build: pio test -e native -f ui/test_popup_queue 3 + 4 + // --- File header ----------------------------------------------------------- 5 + // Includes 6 + 7 + // Group 1: module under test 8 + #include "ui/popup/popup_queue.h" 9 + 10 + // Group 3: stdlib + Unity 11 + #include <unity.h> 12 + 13 + using namespace popup; 14 + 15 + // --- Constants ------------------------------------------------------------- 16 + 17 + static constexpr uint32_t kToastMs = 2000U; 18 + 19 + // --- State variables ------------------------------------------------------- 20 + 21 + static popup::PopupQueue s_q; // reset via s_q.reset() in setUp() 22 + 23 + // --- Function declarations ------------------------------------------------- 24 + 25 + // --- setUp / tearDown ------------------------------------------------------ 26 + // setUp / tearDown 27 + 28 + void setUp() { 29 + s_q.reset(); 30 + } 31 + void tearDown() { /* intentionally empty */ } 32 + 33 + // --- Tests ----------------------------------------------------------------- 34 + // Test functions 35 + 36 + static void test_push_toast_fifo_drops_oldest() { 37 + // ARRANGE 38 + s_q.pushToast( "A", "d1", Severity::INFO, 0, kToastMs ); 39 + s_q.pushToast( "B", "d2", Severity::SUCCESS, 0, kToastMs ); 40 + s_q.pushToast( "C", "d3", Severity::CAUTION, 0, kToastMs ); 41 + TEST_ASSERT_EQUAL_UINT8( 3, s_q.activeToasts() ); 42 + 43 + // ACT — push one more beyond capacity 44 + s_q.pushToast( "D", "d4", Severity::WARNING, 0, kToastMs ); 45 + 46 + // ASSERT — oldest ("A") dropped; B, C, D remain in order 47 + TEST_ASSERT_EQUAL_UINT8( 3, s_q.activeToasts() ); 48 + TEST_ASSERT_EQUAL_STRING( "B", s_q.toastAt( 0 )->title ); 49 + TEST_ASSERT_EQUAL_STRING( "C", s_q.toastAt( 1 )->title ); 50 + TEST_ASSERT_EQUAL_STRING( "D", s_q.toastAt( 2 )->title ); 51 + } 52 + 53 + static void test_pushToast_mutable_title_string_is_copied() { 54 + // ARRANGE 55 + char mut[8] = "Hello"; 56 + 57 + // ACT 58 + s_q.pushToast( mut, "det", Severity::INFO, 0, kToastMs ); 59 + mut[0] = 'X'; 60 + 61 + // ASSERT — title was copied at push time 62 + TEST_ASSERT_EQUAL_STRING( "Hello", s_q.toastAt( 0 )->title ); 63 + } 64 + 65 + static void test_toast_at_deadline_expires_on_tick() { 66 + // ARRANGE 67 + s_q.pushToast( "t", nullptr, Severity::INFO, /*now=*/0, /*timeout=*/kToastMs ); 68 + TEST_ASSERT_EQUAL_UINT8( 1, s_q.activeToasts() ); 69 + 70 + // ACT + ASSERT — still alive just before deadline 71 + TEST_ASSERT_EQUAL_UINT8( 0, s_q.tick( 1999 ) ); 72 + TEST_ASSERT_EQUAL_UINT8( 1, s_q.activeToasts() ); 73 + 74 + // ACT + ASSERT — dismissed exactly at deadline 75 + TEST_ASSERT_EQUAL_UINT8( 1, s_q.tick( 2000 ) ); 76 + TEST_ASSERT_EQUAL_UINT8( 0, s_q.activeToasts() ); 77 + TEST_ASSERT_NULL( s_q.toastAt( 0 ) ); 78 + } 79 + 80 + static void test_toast_without_tick_does_not_expire() { 81 + // ARRANGE + ACT 82 + s_q.pushToast( "t", nullptr, Severity::INFO, /*now=*/0, /*timeout=*/kToastMs ); 83 + 84 + // ASSERT — time passes but no tick(): toast stays 85 + TEST_ASSERT_EQUAL_UINT8( 1, s_q.activeToasts() ); 86 + TEST_ASSERT_EQUAL_UINT8( 1, s_q.activeToasts() ); 87 + TEST_ASSERT_EQUAL_UINT8( 1, s_q.activeToasts() ); 88 + } 89 + 90 + static void test_tick_dismisses_multiple_at_once() { 91 + // ARRANGE 92 + s_q.pushToast( "a", nullptr, Severity::INFO, 0, 1000 ); 93 + s_q.pushToast( "b", nullptr, Severity::INFO, 0, 1500 ); 94 + s_q.pushToast( "c", nullptr, Severity::INFO, 0, 3000 ); 95 + 96 + // ACT 97 + const uint8_t dismissed = s_q.tick( 2000 ); 98 + 99 + // ASSERT — a and b expired; c remains 100 + TEST_ASSERT_EQUAL_UINT8( 2, dismissed ); 101 + TEST_ASSERT_EQUAL_UINT8( 1, s_q.activeToasts() ); 102 + TEST_ASSERT_EQUAL_STRING( "c", s_q.toastAt( 0 )->title ); 103 + } 104 + 105 + static void test_clearToasts_resets_all_active_toasts() { 106 + // ARRANGE 107 + s_q.pushToast( "a", nullptr, Severity::INFO, 0, kToastMs ); 108 + s_q.pushToast( "b", nullptr, Severity::INFO, 0, kToastMs ); 109 + 110 + // ACT 111 + s_q.clearToasts(); 112 + 113 + // ASSERT 114 + TEST_ASSERT_EQUAL_UINT8( 0, s_q.activeToasts() ); 115 + } 116 + 117 + static void test_confirm_resolves_with_confirm() { 118 + // ARRANGE 119 + s_q.beginModal( ModalKind::CONFIRM, "Delete?", "cred", "Delete", "Cancel", Severity::WARNING, 0, 120 + 0 ); 121 + TEST_ASSERT_TRUE( s_q.hasModal() ); 122 + TEST_ASSERT_EQUAL( ConfirmResult::PENDING, s_q.modal().result ); 123 + 124 + // ACT 125 + s_q.resolveModal( ConfirmResult::CONFIRM ); 126 + 127 + // ASSERT 128 + TEST_ASSERT_FALSE( s_q.hasModal() ); 129 + TEST_ASSERT_EQUAL( ConfirmResult::CONFIRM, s_q.modal().result ); 130 + TEST_ASSERT_EQUAL( ModalKind::NONE, s_q.modal().kind ); 131 + } 132 + 133 + static void test_confirm_resolves_with_cancel() { 134 + // ARRANGE 135 + s_q.beginModal( ModalKind::CONFIRM, "t", "m", "OK", "Cancel", Severity::WARNING, 0, 0 ); 136 + 137 + // ACT 138 + s_q.resolveModal( ConfirmResult::CANCEL ); 139 + 140 + // ASSERT 141 + TEST_ASSERT_EQUAL( ConfirmResult::CANCEL, s_q.modal().result ); 142 + } 143 + 144 + static void test_modal_at_deadline_auto_resolves_with_timeout() { 145 + // ARRANGE 146 + s_q.beginModal( ModalKind::ALERT, "t", "m", "OK", "", Severity::INFO, /*now=*/0, 147 + /*timeoutMs=*/5000 ); 148 + 149 + // ACT + ASSERT — before deadline 150 + TEST_ASSERT_FALSE( s_q.tickModal( 0 ) ); 151 + TEST_ASSERT_FALSE( s_q.tickModal( 4999 ) ); 152 + TEST_ASSERT_TRUE( s_q.hasModal() ); 153 + 154 + // ACT + ASSERT — exactly at deadline 155 + TEST_ASSERT_TRUE( s_q.tickModal( 5000 ) ); 156 + TEST_ASSERT_FALSE( s_q.hasModal() ); 157 + TEST_ASSERT_EQUAL( ConfirmResult::TIMEOUT, s_q.modal().result ); 158 + } 159 + 160 + static void test_modal_infinite_timeout_never_fires() { 161 + // ARRANGE 162 + s_q.beginModal( ModalKind::CONFIRM, "t", "m", "OK", "Cancel", Severity::WARNING, 0, 163 + /*timeout=*/0 ); 164 + 165 + // ACT + ASSERT 166 + TEST_ASSERT_FALSE( s_q.tickModal( 1000000 ) ); 167 + TEST_ASSERT_TRUE( s_q.hasModal() ); 168 + } 169 + 170 + static void test_modalActionCount_all_kinds_return_correct_count() { 171 + // ARRANGE + ACT + ASSERT 172 + TEST_ASSERT_EQUAL_UINT8( 0, modalActionCount( ModalKind::NOTICE ) ); 173 + TEST_ASSERT_EQUAL_UINT8( 1, modalActionCount( ModalKind::ALERT ) ); 174 + TEST_ASSERT_EQUAL_UINT8( 2, modalActionCount( ModalKind::CONFIRM ) ); 175 + TEST_ASSERT_EQUAL_UINT8( 3, modalActionCount( ModalKind::CHOICE ) ); 176 + } 177 + 178 + static void test_notice_modal_has_no_labels() { 179 + // ARRANGE + ACT 180 + s_q.beginNotice( "Paused", "No action required", Severity::INFO, 0, 0 ); 181 + 182 + // ASSERT 183 + TEST_ASSERT_TRUE( s_q.hasModal() ); 184 + TEST_ASSERT_EQUAL( ModalKind::NOTICE, s_q.modal().kind ); 185 + TEST_ASSERT_EQUAL_STRING( "", s_q.modal().cancelLabel ); 186 + TEST_ASSERT_EQUAL_STRING( "", s_q.modal().confirmLabel ); 187 + TEST_ASSERT_EQUAL_STRING( "", s_q.modal().thirdLabel ); 188 + } 189 + 190 + static void test_choice_modal_copies_three_labels() { 191 + // ARRANGE + ACT 192 + s_q.beginChoice( "Choose", "target", "Back", "User", "Pass", Severity::INFO, 0, 0 ); 193 + 194 + // ASSERT 195 + TEST_ASSERT_TRUE( s_q.hasModal() ); 196 + TEST_ASSERT_EQUAL( ModalKind::CHOICE, s_q.modal().kind ); 197 + TEST_ASSERT_EQUAL_STRING( "Back", s_q.modal().cancelLabel ); 198 + TEST_ASSERT_EQUAL_STRING( "User", s_q.modal().confirmLabel ); 199 + TEST_ASSERT_EQUAL_STRING( "Pass", s_q.modal().thirdLabel ); 200 + } 201 + 202 + static void test_spinner_show_then_hide_updates_active_state() { 203 + // ARRANGE + ACT + ASSERT — initial state 204 + TEST_ASSERT_FALSE( s_q.spinnerActive() ); 205 + 206 + // ACT 207 + s_q.spinnerShow( "Pairing..." ); 208 + 209 + // ASSERT 210 + TEST_ASSERT_TRUE( s_q.spinnerActive() ); 211 + TEST_ASSERT_EQUAL_STRING( "Pairing...", s_q.spinnerLabel() ); 212 + 213 + // ACT 214 + s_q.spinnerHide(); 215 + 216 + // ASSERT 217 + TEST_ASSERT_FALSE( s_q.spinnerActive() ); 218 + } 219 + 220 + static void test_transition_at_deadline_expires_on_tick() { 221 + // ARRANGE + ACT + ASSERT — initial state 222 + TEST_ASSERT_FALSE( s_q.transitionActive() ); 223 + 224 + // ACT 225 + s_q.transitionShow( "Opening vault", "Decrypting index", Severity::INFO, 100, 1200 ); 226 + 227 + // ASSERT 228 + TEST_ASSERT_TRUE( s_q.transitionActive() ); 229 + TEST_ASSERT_EQUAL_STRING( "Opening vault", s_q.transition().title ); 230 + TEST_ASSERT_EQUAL_STRING( "Decrypting index", s_q.transition().detail ); 231 + 232 + // ACT + ASSERT — before deadline 233 + TEST_ASSERT_FALSE( s_q.tickTransition( 1299 ) ); 234 + TEST_ASSERT_TRUE( s_q.transitionActive() ); 235 + 236 + // ACT + ASSERT — at deadline 237 + TEST_ASSERT_TRUE( s_q.tickTransition( 1300 ) ); 238 + TEST_ASSERT_FALSE( s_q.transitionActive() ); 239 + } 240 + 241 + static void test_anyActive_all_popup_types_reflect_active_state() { 242 + // ARRANGE + ACT + ASSERT — starts empty 243 + TEST_ASSERT_FALSE( s_q.anyActive() ); 244 + 245 + s_q.pushToast( "t", nullptr, Severity::INFO, 0, kToastMs ); 246 + TEST_ASSERT_TRUE( s_q.anyActive() ); 247 + s_q.clearToasts(); 248 + TEST_ASSERT_FALSE( s_q.anyActive() ); 249 + 250 + s_q.spinnerShow( "x" ); 251 + TEST_ASSERT_TRUE( s_q.anyActive() ); 252 + s_q.spinnerHide(); 253 + TEST_ASSERT_FALSE( s_q.anyActive() ); 254 + 255 + s_q.transitionShow( "x", nullptr, Severity::INFO, 0, 1000 ); 256 + TEST_ASSERT_TRUE( s_q.anyActive() ); 257 + s_q.clearTransition(); 258 + TEST_ASSERT_FALSE( s_q.anyActive() ); 259 + 260 + s_q.beginModal( ModalKind::ALERT, "t", "m", "OK", "", Severity::INFO, 0, 0 ); 261 + TEST_ASSERT_TRUE( s_q.anyActive() ); 262 + s_q.resolveModal( ConfirmResult::CONFIRM ); 263 + TEST_ASSERT_FALSE( s_q.anyActive() ); 264 + } 265 + 266 + // --- Fake function definitions --------------------------------------------- 267 + // Fake function definitions 268 + 269 + // --- Helper definitions ---------------------------------------------------- 270 + // Helper / arrange definitions 271 + 272 + // (none) 273 + 274 + // --- main() ---------------------------------------------------------------- 275 + // main 276 + 277 + int main( int /*argc*/, char** /*argv*/ ) { 278 + UNITY_BEGIN(); 279 + RUN_TEST( test_push_toast_fifo_drops_oldest ); 280 + RUN_TEST( test_pushToast_mutable_title_string_is_copied ); 281 + RUN_TEST( test_toast_at_deadline_expires_on_tick ); 282 + RUN_TEST( test_toast_without_tick_does_not_expire ); 283 + RUN_TEST( test_tick_dismisses_multiple_at_once ); 284 + RUN_TEST( test_clearToasts_resets_all_active_toasts ); 285 + RUN_TEST( test_confirm_resolves_with_confirm ); 286 + RUN_TEST( test_confirm_resolves_with_cancel ); 287 + RUN_TEST( test_modal_at_deadline_auto_resolves_with_timeout ); 288 + RUN_TEST( test_modal_infinite_timeout_never_fires ); 289 + RUN_TEST( test_modalActionCount_all_kinds_return_correct_count ); 290 + RUN_TEST( test_notice_modal_has_no_labels ); 291 + RUN_TEST( test_choice_modal_copies_three_labels ); 292 + RUN_TEST( test_spinner_show_then_hide_updates_active_state ); 293 + RUN_TEST( test_transition_at_deadline_expires_on_tick ); 294 + RUN_TEST( test_anyActive_all_popup_types_reflect_active_state ); 295 + return UNITY_END(); 296 + }
+267
test/ui/test_status_notifier/test_status_notifier.cpp
··· 1 + // Kleidos — Native test: StatusNotifier (hysteresis and one-shot decision logic) 2 + // Build: pio test -e native -f ui/test_status_notifier 3 + 4 + // --- File header ----------------------------------------------------------- 5 + // Includes 6 + 7 + // Group 1: module under test 8 + #include "ui/notifications/status_notifier.h" 9 + 10 + // Group 3: stdlib + Unity 11 + #include <unity.h> 12 + 13 + using notifier::Event; 14 + 15 + // --- Constants ------------------------------------------------------------- 16 + 17 + static constexpr uint32_t kAdminIdleTotalMs = 300000U; 18 + static constexpr uint32_t kAdminIdleWarnMs = 30000U; 19 + 20 + // --- State variables ------------------------------------------------------- 21 + 22 + // (none — all notifier state objects are local to each test function) 23 + 24 + // --- Function declarations ------------------------------------------------- 25 + 26 + // --- setUp / tearDown ------------------------------------------------------ 27 + // setUp / tearDown 28 + 29 + void setUp() {} 30 + void tearDown() { /* intentionally empty */ } 31 + 32 + // --- Tests ----------------------------------------------------------------- 33 + // Test functions 34 + 35 + static void test_battery_prime_does_not_fire() { 36 + // ARRANGE 37 + notifier::BatteryState st; 38 + 39 + // ACT + ASSERT — first sample always swallowed (prevents boot-time event) 40 + TEST_ASSERT_EQUAL( static_cast<int>( Event::NONE ), 41 + static_cast<int>( notifier::decideBattery( st, 50, false ) ) ); 42 + } 43 + 44 + static void test_battery_low_fires_once_then_holds() { 45 + // ARRANGE 46 + notifier::BatteryState st; 47 + (void)notifier::decideBattery( st, 50, false ); // prime 48 + 49 + // ACT + ASSERT — first crossing fires 50 + TEST_ASSERT_EQUAL( static_cast<int>( Event::BatteryLow ), 51 + static_cast<int>( notifier::decideBattery( st, 20, false ) ) ); 52 + 53 + // ASSERT — second sample below exit threshold: no re-emit 54 + TEST_ASSERT_EQUAL( static_cast<int>( Event::NONE ), 55 + static_cast<int>( notifier::decideBattery( st, 19, false ) ) ); 56 + } 57 + 58 + static void test_battery_low_oscillating_values_no_refire() { 59 + // ARRANGE 60 + notifier::BatteryState st; 61 + (void)notifier::decideBattery( st, 50, false ); 62 + (void)notifier::decideBattery( st, 20, false ); // fires LOW 63 + 64 + // ACT + ASSERT — oscillate 19/21 — below exit threshold of 22: never re-arms 65 + for ( int i = 0; i < 10; ++i ) { 66 + TEST_ASSERT_EQUAL( 67 + static_cast<int>( Event::NONE ), 68 + static_cast<int>( notifier::decideBattery( st, ( i % 2 ) ? 21 : 19, false ) ) ); 69 + } 70 + } 71 + 72 + static void test_battery_low_rearms_after_exit_threshold() { 73 + // ARRANGE 74 + notifier::BatteryState st; 75 + (void)notifier::decideBattery( st, 50, false ); 76 + (void)notifier::decideBattery( st, 20, false ); // fires 77 + (void)notifier::decideBattery( st, 22, false ); // re-arm 78 + 79 + // ACT + ASSERT — crossing again after re-arm fires 80 + TEST_ASSERT_EQUAL( static_cast<int>( Event::BatteryLow ), 81 + static_cast<int>( notifier::decideBattery( st, 20, false ) ) ); 82 + } 83 + 84 + static void test_battery_critical_fires_once_per_boot() { 85 + // ARRANGE 86 + notifier::BatteryState st; 87 + (void)notifier::decideBattery( st, 50, false ); // prime 88 + 89 + // ACT + ASSERT — first critical crossing fires 90 + TEST_ASSERT_EQUAL( static_cast<int>( Event::BatteryCritical ), 91 + static_cast<int>( notifier::decideBattery( st, 5, false ) ) ); 92 + 93 + // ASSERT — further samples must not refire 94 + TEST_ASSERT_EQUAL( static_cast<int>( Event::NONE ), 95 + static_cast<int>( notifier::decideBattery( st, 3, false ) ) ); 96 + TEST_ASSERT_EQUAL( static_cast<int>( Event::NONE ), 97 + static_cast<int>( notifier::decideBattery( st, 2, false ) ) ); 98 + } 99 + 100 + static void test_battery_charge_transition_fires_charging_event() { 101 + // ARRANGE 102 + notifier::BatteryState st; 103 + (void)notifier::decideBattery( st, 50, false ); // prime discharging 104 + 105 + // ACT + ASSERT — transition to charging fires once 106 + TEST_ASSERT_EQUAL( static_cast<int>( Event::BatteryCharging ), 107 + static_cast<int>( notifier::decideBattery( st, 50, true ) ) ); 108 + 109 + // ASSERT — same charging state: no refire 110 + TEST_ASSERT_EQUAL( static_cast<int>( Event::NONE ), 111 + static_cast<int>( notifier::decideBattery( st, 55, true ) ) ); 112 + } 113 + 114 + static void test_battery_full_once_per_session() { 115 + // ARRANGE 116 + notifier::BatteryState st; 117 + (void)notifier::decideBattery( st, 50, false ); 118 + (void)notifier::decideBattery( st, 50, true ); // CHARGING 119 + 120 + // ACT + ASSERT — first full fires 121 + TEST_ASSERT_EQUAL( static_cast<int>( Event::BatteryFull ), 122 + static_cast<int>( notifier::decideBattery( st, 99, true ) ) ); 123 + 124 + // ASSERT — holding at 100 %: no refire 125 + TEST_ASSERT_EQUAL( static_cast<int>( Event::NONE ), 126 + static_cast<int>( notifier::decideBattery( st, 100, true ) ) ); 127 + } 128 + 129 + static void test_bleHost_connect_disconnect_fire_correct_events() { 130 + // ARRANGE 131 + notifier::HostState st; 132 + (void)notifier::decideBleHost( st, false ); // prime 133 + 134 + // ACT + ASSERT — connect fires, hold fires nothing, disconnect fires 135 + TEST_ASSERT_EQUAL( static_cast<int>( Event::BleHostConnected ), 136 + static_cast<int>( notifier::decideBleHost( st, true ) ) ); 137 + TEST_ASSERT_EQUAL( static_cast<int>( Event::NONE ), 138 + static_cast<int>( notifier::decideBleHost( st, true ) ) ); 139 + TEST_ASSERT_EQUAL( static_cast<int>( Event::BleHostDisconnected ), 140 + static_cast<int>( notifier::decideBleHost( st, false ) ) ); 141 + } 142 + 143 + static void test_admin_client_fires_on_increase() { 144 + // ARRANGE 145 + notifier::AdminClientState st; 146 + 147 + // ACT + ASSERT 148 + TEST_ASSERT_EQUAL( static_cast<int>( Event::NONE ), 149 + static_cast<int>( notifier::decideAdminClient( st, 0 ) ) ); 150 + TEST_ASSERT_EQUAL( static_cast<int>( Event::AdminClientConnected ), 151 + static_cast<int>( notifier::decideAdminClient( st, 1 ) ) ); 152 + TEST_ASSERT_EQUAL( static_cast<int>( Event::NONE ), 153 + static_cast<int>( notifier::decideAdminClient( st, 1 ) ) ); 154 + TEST_ASSERT_EQUAL( static_cast<int>( Event::NONE ), 155 + static_cast<int>( notifier::decideAdminClient( st, 0 ) ) ); 156 + TEST_ASSERT_EQUAL( static_cast<int>( Event::AdminClientConnected ), 157 + static_cast<int>( notifier::decideAdminClient( st, 1 ) ) ); 158 + } 159 + 160 + static void test_admin_idle_warn_then_timeout() { 161 + // ARRANGE 162 + notifier::AdminIdleState st; 163 + 164 + // ACT + ASSERT — well before warn boundary: nothing 165 + TEST_ASSERT_EQUAL( static_cast<int>( Event::NONE ), 166 + static_cast<int>( notifier::decideAdminIdle( st, 100000, kAdminIdleTotalMs, 167 + kAdminIdleWarnMs ) ) ); 168 + 169 + // ACT + ASSERT — at warn boundary: warning fires 170 + TEST_ASSERT_EQUAL( 171 + static_cast<int>( Event::AdminIdleWarning ), 172 + static_cast<int>( notifier::decideAdminIdle( st, kAdminIdleTotalMs - kAdminIdleWarnMs, 173 + kAdminIdleTotalMs, kAdminIdleWarnMs ) ) ); 174 + 175 + // ASSERT — in warn window: no refire 176 + TEST_ASSERT_EQUAL( static_cast<int>( Event::NONE ), 177 + static_cast<int>( notifier::decideAdminIdle( 178 + st, kAdminIdleTotalMs - 10000, kAdminIdleTotalMs, kAdminIdleWarnMs ) ) ); 179 + 180 + // ACT + ASSERT — at total: timeout fires 181 + TEST_ASSERT_EQUAL( static_cast<int>( Event::AdminTimeout ), 182 + static_cast<int>( notifier::decideAdminIdle( 183 + st, kAdminIdleTotalMs, kAdminIdleTotalMs, kAdminIdleWarnMs ) ) ); 184 + 185 + // ASSERT — after timeout: no refire 186 + TEST_ASSERT_EQUAL( static_cast<int>( Event::NONE ), 187 + static_cast<int>( notifier::decideAdminIdle( 188 + st, kAdminIdleTotalMs + 5000, kAdminIdleTotalMs, kAdminIdleWarnMs ) ) ); 189 + } 190 + 191 + static void test_admin_idle_after_activity_reset_rearms_warn() { 192 + // ARRANGE 193 + notifier::AdminIdleState st; 194 + (void)notifier::decideAdminIdle( st, kAdminIdleTotalMs - kAdminIdleWarnMs, kAdminIdleTotalMs, 195 + kAdminIdleWarnMs ); // warn fired 196 + (void)notifier::decideAdminIdle( st, 0, kAdminIdleTotalMs, 197 + kAdminIdleWarnMs ); // activity reset 198 + 199 + // ACT + ASSERT — crossing warn boundary again fires fresh warning 200 + TEST_ASSERT_EQUAL( 201 + static_cast<int>( Event::AdminIdleWarning ), 202 + static_cast<int>( notifier::decideAdminIdle( st, kAdminIdleTotalMs - kAdminIdleWarnMs, 203 + kAdminIdleTotalMs, kAdminIdleWarnMs ) ) ); 204 + } 205 + 206 + static void test_auto_lock_fires_once_rearms_when_far() { 207 + // ARRANGE 208 + notifier::AutoLockState st; 209 + 210 + // ACT + ASSERT 211 + TEST_ASSERT_EQUAL( static_cast<int>( Event::NONE ), 212 + static_cast<int>( notifier::decideAutoLock( st, 10000, 5000 ) ) ); 213 + TEST_ASSERT_EQUAL( static_cast<int>( Event::AutoLockCountdown ), 214 + static_cast<int>( notifier::decideAutoLock( st, 5000, 5000 ) ) ); 215 + TEST_ASSERT_EQUAL( static_cast<int>( Event::NONE ), 216 + static_cast<int>( notifier::decideAutoLock( st, 3000, 5000 ) ) ); 217 + 218 + // ACT + ASSERT — activity reset (ms grows past window) 219 + TEST_ASSERT_EQUAL( static_cast<int>( Event::NONE ), 220 + static_cast<int>( notifier::decideAutoLock( st, 60000, 5000 ) ) ); 221 + 222 + // ACT + ASSERT — countdown again fires fresh 223 + TEST_ASSERT_EQUAL( static_cast<int>( Event::AutoLockCountdown ), 224 + static_cast<int>( notifier::decideAutoLock( st, 4000, 5000 ) ) ); 225 + } 226 + 227 + static void test_ota_applied_one_shot() { 228 + // ARRANGE 229 + notifier::OtaAppliedState st; 230 + 231 + // ACT + ASSERT 232 + TEST_ASSERT_EQUAL( static_cast<int>( Event::NONE ), 233 + static_cast<int>( notifier::decideOtaApplied( st, false ) ) ); 234 + TEST_ASSERT_EQUAL( static_cast<int>( Event::OtaApplied ), 235 + static_cast<int>( notifier::decideOtaApplied( st, true ) ) ); 236 + TEST_ASSERT_EQUAL( static_cast<int>( Event::NONE ), 237 + static_cast<int>( notifier::decideOtaApplied( st, true ) ) ); 238 + } 239 + 240 + // --- Fake function definitions --------------------------------------------- 241 + // Fake function definitions 242 + 243 + // --- Helper definitions ---------------------------------------------------- 244 + // Helper / arrange definitions 245 + 246 + // (none) 247 + 248 + // --- main() ---------------------------------------------------------------- 249 + // main 250 + 251 + int main( int /*argc*/, char** /*argv*/ ) { 252 + UNITY_BEGIN(); 253 + RUN_TEST( test_battery_prime_does_not_fire ); 254 + RUN_TEST( test_battery_low_fires_once_then_holds ); 255 + RUN_TEST( test_battery_low_oscillating_values_no_refire ); 256 + RUN_TEST( test_battery_low_rearms_after_exit_threshold ); 257 + RUN_TEST( test_battery_critical_fires_once_per_boot ); 258 + RUN_TEST( test_battery_charge_transition_fires_charging_event ); 259 + RUN_TEST( test_battery_full_once_per_session ); 260 + RUN_TEST( test_bleHost_connect_disconnect_fire_correct_events ); 261 + RUN_TEST( test_admin_client_fires_on_increase ); 262 + RUN_TEST( test_admin_idle_warn_then_timeout ); 263 + RUN_TEST( test_admin_idle_after_activity_reset_rearms_warn ); 264 + RUN_TEST( test_auto_lock_fires_once_rearms_when_far ); 265 + RUN_TEST( test_ota_applied_one_shot ); 266 + return UNITY_END(); 267 + }
+266
test/vault/test_audit_log/test_audit_log.cpp
··· 1 + // Kleidos — Native test: Audit Log (circular buffer logic) 2 + // Build: pio test -e native -f vault/test_audit_log 3 + 4 + // --- File header ----------------------------------------------------------- 5 + 6 + // Group 3: stdlib + Unity 7 + #include <cstdint> 8 + #include <cstring> 9 + 10 + #include <unity.h> 11 + 12 + // --- Constants ------------------------------------------------------------- 13 + 14 + // Replicate AuditLog types — no NVS/RTC hardware available on host. 15 + enum class AuditEvent : uint8_t { 16 + PIN_OK = 0, 17 + PIN_FAIL = 1, 18 + VAULT_WIPE = 2, 19 + BLE_TYPE = 3, 20 + OTA_UPDATE = 4, 21 + ADMIN_LOGIN = 5, 22 + }; 23 + 24 + struct AuditEntry { 25 + uint32_t timestamp; 26 + uint8_t event; 27 + uint8_t attempts; 28 + }; 29 + 30 + static constexpr uint8_t kAuditLogSize = 16; 31 + 32 + // --- State variables ------------------------------------------------------- 33 + 34 + static struct { 35 + AuditEntry entries[kAuditLogSize]; 36 + uint8_t idx; 37 + uint32_t time; 38 + } helper; 39 + 40 + // --- Function declarations ------------------------------------------------- 41 + 42 + // Arrange helpers 43 + static void simClear(); 44 + static void simRecord( AuditEvent event, uint8_t attempts = 0 ); 45 + static uint8_t simGetEntries( AuditEntry* out, uint8_t maxEntries ); 46 + static bool simGetLastEntry( AuditEntry& entry ); 47 + 48 + // --- setUp / tearDown ------------------------------------------------------ 49 + 50 + void setUp() { 51 + helper = {}; 52 + } 53 + 54 + void tearDown() { /* intentionally empty */ } 55 + 56 + // --- Tests ----------------------------------------------------------------- 57 + 58 + static void test_empty_log_returns_zero() { 59 + // ARRANGE 60 + AuditEntry out[kAuditLogSize]; 61 + 62 + // ACT 63 + uint8_t count = simGetEntries( out, kAuditLogSize ); 64 + 65 + // ASSERT 66 + TEST_ASSERT_EQUAL( 0, count ); 67 + } 68 + 69 + static void test_empty_log_no_last_entry() { 70 + // ARRANGE 71 + AuditEntry entry{}; 72 + 73 + // ACT + ASSERT 74 + TEST_ASSERT_FALSE( simGetLastEntry( entry ) ); 75 + } 76 + 77 + static void test_single_record() { 78 + // ARRANGE 79 + helper.time = 100; 80 + simRecord( AuditEvent::PIN_OK, 0 ); 81 + AuditEntry out[kAuditLogSize]; 82 + 83 + // ACT 84 + uint8_t count = simGetEntries( out, kAuditLogSize ); 85 + 86 + // ASSERT 87 + TEST_ASSERT_EQUAL( 1, count ); 88 + TEST_ASSERT_EQUAL( 100, out[0].timestamp ); 89 + TEST_ASSERT_EQUAL( 0, out[0].event ); 90 + } 91 + 92 + static void test_last_entry_returns_most_recent() { 93 + // ARRANGE 94 + helper.time = 100; 95 + simRecord( AuditEvent::PIN_OK ); 96 + helper.time = 200; 97 + simRecord( AuditEvent::PIN_FAIL, 1 ); 98 + helper.time = 300; 99 + simRecord( AuditEvent::BLE_TYPE ); 100 + AuditEntry entry{}; 101 + 102 + // ACT 103 + bool found = simGetLastEntry( entry ); 104 + 105 + // ASSERT 106 + TEST_ASSERT_TRUE( found ); 107 + TEST_ASSERT_EQUAL( 300, entry.timestamp ); 108 + TEST_ASSERT_EQUAL( static_cast<uint8_t>( AuditEvent::BLE_TYPE ), entry.event ); 109 + } 110 + 111 + static void test_entries_oldest_first() { 112 + // ARRANGE 113 + for ( uint8_t i = 0; i < 5; i++ ) { 114 + helper.time = ( i + 1 ) * 100; 115 + simRecord( AuditEvent::PIN_FAIL, i ); 116 + } 117 + AuditEntry out[kAuditLogSize]; 118 + 119 + // ACT 120 + uint8_t count = simGetEntries( out, kAuditLogSize ); 121 + 122 + // ASSERT 123 + TEST_ASSERT_EQUAL( 5, count ); 124 + for ( uint8_t i = 0; i < count - 1; i++ ) { 125 + TEST_ASSERT_TRUE( out[i].timestamp <= out[i + 1].timestamp ); 126 + } 127 + } 128 + 129 + static void test_circular_wrap() { 130 + // ARRANGE — fill buffer completely 131 + for ( uint8_t i = 0; i < kAuditLogSize; i++ ) { 132 + helper.time = ( i + 1 ) * 10; 133 + simRecord( AuditEvent::PIN_FAIL, i ); 134 + } 135 + AuditEntry out[kAuditLogSize]; 136 + uint8_t pre = simGetEntries( out, kAuditLogSize ); 137 + TEST_ASSERT_EQUAL( kAuditLogSize, pre ); // precondition 138 + 139 + // ACT — overflow by one; oldest (timestamp=10) overwritten 140 + helper.time = 999; 141 + simRecord( AuditEvent::VAULT_WIPE, 10 ); 142 + 143 + // ASSERT 144 + uint8_t count = simGetEntries( out, kAuditLogSize ); 145 + TEST_ASSERT_EQUAL( kAuditLogSize, count ); 146 + TEST_ASSERT_EQUAL( 20, out[0].timestamp ); 147 + TEST_ASSERT_EQUAL( 999, out[kAuditLogSize - 1].timestamp ); 148 + TEST_ASSERT_EQUAL( static_cast<uint8_t>( AuditEvent::VAULT_WIPE ), 149 + out[kAuditLogSize - 1].event ); 150 + } 151 + 152 + static void test_max_entries_limit() { 153 + // ARRANGE 154 + for ( uint8_t i = 0; i < 10; i++ ) { 155 + helper.time = ( i + 1 ) * 10; 156 + simRecord( AuditEvent::PIN_OK ); 157 + } 158 + AuditEntry out[3]; 159 + 160 + // ACT 161 + uint8_t count = simGetEntries( out, 3 ); 162 + 163 + // ASSERT 164 + TEST_ASSERT_EQUAL( 3, count ); 165 + } 166 + 167 + static void test_clear_resets_all() { 168 + // ARRANGE 169 + helper.time = 100; 170 + simRecord( AuditEvent::PIN_OK ); 171 + simRecord( AuditEvent::PIN_FAIL ); 172 + 173 + // ACT 174 + simClear(); 175 + 176 + // ASSERT 177 + AuditEntry out[kAuditLogSize]; 178 + uint8_t count = simGetEntries( out, kAuditLogSize ); 179 + TEST_ASSERT_EQUAL( 0, count ); 180 + } 181 + 182 + static void test_event_types_stored_correctly() { 183 + // ARRANGE 184 + AuditEvent events[] = { 185 + AuditEvent::PIN_OK, AuditEvent::PIN_FAIL, AuditEvent::VAULT_WIPE, 186 + AuditEvent::BLE_TYPE, AuditEvent::OTA_UPDATE, AuditEvent::ADMIN_LOGIN, 187 + }; 188 + for ( size_t i = 0; i < 6; i++ ) { 189 + helper.time = ( i + 1 ) * 100; 190 + simRecord( events[i], static_cast<uint8_t>( i ) ); 191 + } 192 + AuditEntry out[kAuditLogSize]; 193 + 194 + // ACT 195 + uint8_t count = simGetEntries( out, kAuditLogSize ); 196 + 197 + // ASSERT 198 + TEST_ASSERT_EQUAL( 6, count ); 199 + for ( size_t i = 0; i < 6; i++ ) { 200 + TEST_ASSERT_EQUAL( static_cast<uint8_t>( events[i] ), out[i].event ); 201 + TEST_ASSERT_EQUAL( i, out[i].attempts ); 202 + } 203 + } 204 + 205 + static void test_attempts_field_max() { 206 + // ARRANGE 207 + helper.time = 1; 208 + simRecord( AuditEvent::PIN_FAIL, 255 ); 209 + AuditEntry entry{}; 210 + 211 + // ACT 212 + bool found = simGetLastEntry( entry ); 213 + 214 + // ASSERT 215 + TEST_ASSERT_TRUE( found ); 216 + TEST_ASSERT_EQUAL( 255, entry.attempts ); 217 + } 218 + 219 + // --- State variables ------------------------------------------------------- 220 + 221 + static void simClear() { 222 + helper = {}; 223 + } 224 + 225 + static void simRecord( AuditEvent event, uint8_t attempts ) { 226 + helper.entries[helper.idx].timestamp = helper.time; 227 + helper.entries[helper.idx].event = static_cast<uint8_t>( event ); 228 + helper.entries[helper.idx].attempts = attempts; 229 + helper.idx = static_cast<uint8_t>( ( helper.idx + 1 ) % kAuditLogSize ); 230 + } 231 + 232 + static uint8_t simGetEntries( AuditEntry* out, uint8_t maxEntries ) { 233 + uint8_t count = 0; 234 + for ( uint8_t i = 0; i < kAuditLogSize && count < maxEntries; i++ ) { 235 + uint8_t pos = static_cast<uint8_t>( ( helper.idx + i ) % kAuditLogSize ); 236 + if ( helper.entries[pos].timestamp == 0 && helper.entries[pos].event == 0 ) 237 + continue; 238 + out[count++] = helper.entries[pos]; 239 + } 240 + return count; 241 + } 242 + 243 + static bool simGetLastEntry( AuditEntry& entry ) { 244 + uint8_t lastIdx = static_cast<uint8_t>( ( helper.idx + kAuditLogSize - 1 ) % kAuditLogSize ); 245 + if ( helper.entries[lastIdx].timestamp == 0 && helper.entries[lastIdx].event == 0 ) 246 + return false; 247 + entry = helper.entries[lastIdx]; 248 + return true; 249 + } 250 + 251 + // --- main() ---------------------------------------------------------------- 252 + 253 + int main( int /*argc*/, char** /*argv*/ ) { 254 + UNITY_BEGIN(); 255 + RUN_TEST( test_empty_log_returns_zero ); 256 + RUN_TEST( test_empty_log_no_last_entry ); 257 + RUN_TEST( test_single_record ); 258 + RUN_TEST( test_last_entry_returns_most_recent ); 259 + RUN_TEST( test_entries_oldest_first ); 260 + RUN_TEST( test_circular_wrap ); 261 + RUN_TEST( test_max_entries_limit ); 262 + RUN_TEST( test_clear_resets_all ); 263 + RUN_TEST( test_event_types_stored_correctly ); 264 + RUN_TEST( test_attempts_field_max ); 265 + return UNITY_END(); 266 + }
+338
test/vault/test_brute_force_guard/test_brute_force_guard.cpp
··· 1 + // Kleidos — Native test: BruteForceGuard 2 + // Tests lockout duration, remaining-lockout calculation, lockout state machine, 3 + // and edge cases. Replicates pure logic — NVS/RTC not available natively. 4 + // Build: pio test -e native -f vault/test_brute_force_guard 5 + 6 + #include <cstdint> 7 + #include <cstring> 8 + 9 + #include <unity.h> 10 + 11 + // Platform stubs 12 + // the clock facade; provide a minimal stub for this test which does not 13 + // exercise time-sensitive button behavior. 14 + static uint32_t fakeMillis = 0; 15 + extern "C" uint32_t millis() { 16 + return fakeMillis; 17 + } 18 + 19 + // --- Constants ------------------------------------------------------------- 20 + // --- Constants ------------------------------------------------------------- 21 + static constexpr uint8_t kWarnThreshold = 3; 22 + static constexpr uint8_t kShortLockStart = 4; 23 + static constexpr uint8_t kLongLockStart = 7; 24 + static constexpr uint8_t kWipeThreshold = 10; 25 + static constexpr uint32_t kShortLockSec = 30; 26 + static constexpr uint32_t kLongLockSec = 300; 27 + 28 + // --- State variables ------------------------------------------------------- 29 + // --- Helper definitions ---------------------------------------------------- 30 + // (none needed — tests are stateless, helpers carry their own state) 31 + 32 + // --- Function declarations ------------------------------------------------- 33 + // --- Helper definitions ---------------------------------------------------- 34 + static uint32_t getLockoutDuration( uint8_t attempts ); 35 + static bool shouldWipe( uint8_t attempts ); 36 + 37 + // --- setUp / tearDown ------------------------------------------------------ 38 + // setUp / tearDown 39 + // --- setUp / tearDown ------------------------------------------------------ 40 + void setUp() { 41 + fakeMillis = 0; 42 + } 43 + 44 + void tearDown() { /* intentionally empty */ } 45 + 46 + // --- Helper definitions ---------------------------------------------------- 47 + // --- State variables ------------------------------------------------------- 48 + struct SimBruteForce { 49 + uint8_t attempts = 0; 50 + uint32_t lockoutUntilSec = 0; 51 + uint32_t currentTimeSec = 0; 52 + 53 + uint32_t registerFailure() { 54 + attempts++; 55 + if ( attempts >= kWipeThreshold ) 56 + return UINT32_MAX; 57 + uint32_t lockSec = getLockoutDuration( attempts ); 58 + if ( lockSec > 0 ) 59 + lockoutUntilSec = currentTimeSec + lockSec; 60 + return lockSec; 61 + } 62 + 63 + uint32_t getRemainingLockout() const { 64 + if ( lockoutUntilSec == 0 ) 65 + return 0; 66 + if ( currentTimeSec >= lockoutUntilSec ) 67 + return 0; 68 + return lockoutUntilSec - currentTimeSec; 69 + } 70 + 71 + void resetAttempts() { 72 + attempts = 0; 73 + lockoutUntilSec = 0; 74 + } 75 + }; 76 + 77 + // --- Tests: lockout duration ----------------------------------------------- 78 + // --- Tests ----------------------------------------------------------------- 79 + static void test_lockout_duration_no_lockout_first_3_attempts() { 80 + TEST_ASSERT_EQUAL( 0, getLockoutDuration( 1 ) ); 81 + TEST_ASSERT_EQUAL( 0, getLockoutDuration( 2 ) ); 82 + TEST_ASSERT_EQUAL( 0, getLockoutDuration( 3 ) ); 83 + } 84 + 85 + static void test_lockout_duration_30s_after_4_attempts() { 86 + TEST_ASSERT_EQUAL( kShortLockSec, getLockoutDuration( 4 ) ); 87 + TEST_ASSERT_EQUAL( kShortLockSec, getLockoutDuration( 5 ) ); 88 + TEST_ASSERT_EQUAL( kShortLockSec, getLockoutDuration( 6 ) ); 89 + } 90 + 91 + static void test_lockout_duration_300s_after_7_attempts() { 92 + TEST_ASSERT_EQUAL( kLongLockSec, getLockoutDuration( 7 ) ); 93 + TEST_ASSERT_EQUAL( kLongLockSec, getLockoutDuration( 8 ) ); 94 + TEST_ASSERT_EQUAL( kLongLockSec, getLockoutDuration( 9 ) ); 95 + } 96 + 97 + static void test_lockout_duration_zero_attempts_no_lockout() { 98 + TEST_ASSERT_EQUAL( 0, getLockoutDuration( 0 ) ); 99 + TEST_ASSERT_FALSE( shouldWipe( 0 ) ); 100 + } 101 + 102 + static void test_lockout_duration_boundary_short_to_long() { 103 + TEST_ASSERT_EQUAL( kShortLockSec, getLockoutDuration( 6 ) ); 104 + TEST_ASSERT_EQUAL( kLongLockSec, getLockoutDuration( 7 ) ); 105 + } 106 + 107 + static void test_lockout_duration_monotonically_increases() { 108 + uint32_t prev = 0; 109 + for ( uint8_t i = 1; i <= 9; i++ ) { 110 + uint32_t current = getLockoutDuration( i ); 111 + TEST_ASSERT_GREATER_OR_EQUAL( prev, current ); 112 + prev = current; 113 + } 114 + } 115 + 116 + // --- Tests: wipe threshold ------------------------------------------------- 117 + // --- Tests ----------------------------------------------------------------- 118 + static void test_wipe_no_wipe_before_10() { 119 + for ( uint8_t i = 0; i < kWipeThreshold; i++ ) { 120 + TEST_ASSERT_FALSE( shouldWipe( i ) ); 121 + } 122 + } 123 + 124 + static void test_wipe_at_10_and_beyond() { 125 + TEST_ASSERT_TRUE( shouldWipe( 10 ) ); 126 + TEST_ASSERT_TRUE( shouldWipe( 11 ) ); 127 + TEST_ASSERT_TRUE( shouldWipe( 255 ) ); 128 + } 129 + 130 + static void test_wipe_boundary_long_to_wipe() { 131 + TEST_ASSERT_EQUAL( kLongLockSec, getLockoutDuration( 9 ) ); 132 + TEST_ASSERT_FALSE( shouldWipe( 9 ) ); 133 + TEST_ASSERT_TRUE( shouldWipe( 10 ) ); 134 + } 135 + 136 + // --- Tests: remaining lockout ---------------------------------------------- 137 + // --- Tests ----------------------------------------------------------------- 138 + static void test_remaining_lockout_no_failures() { 139 + SimBruteForce bf; 140 + bf.currentTimeSec = 1000; 141 + TEST_ASSERT_EQUAL( 0, bf.getRemainingLockout() ); 142 + } 143 + 144 + static void test_remaining_lockout_below_threshold() { 145 + SimBruteForce bf; 146 + bf.currentTimeSec = 1000; 147 + bf.registerFailure(); 148 + bf.registerFailure(); 149 + bf.registerFailure(); 150 + TEST_ASSERT_EQUAL( 0, bf.getRemainingLockout() ); 151 + } 152 + 153 + static void test_remaining_lockout_30s_immediately() { 154 + SimBruteForce bf; 155 + bf.currentTimeSec = 1000; 156 + for ( int i = 0; i < 3; i++ ) 157 + bf.registerFailure(); 158 + uint32_t lockSec = bf.registerFailure(); // 4th 159 + 160 + // ASSERT 161 + TEST_ASSERT_EQUAL( 30, lockSec ); 162 + TEST_ASSERT_EQUAL( 30, bf.getRemainingLockout() ); 163 + } 164 + 165 + static void test_remaining_lockout_decreases_with_time() { 166 + SimBruteForce bf; 167 + bf.currentTimeSec = 1000; 168 + for ( int i = 0; i < 4; i++ ) 169 + bf.registerFailure(); 170 + 171 + bf.currentTimeSec = 1010; 172 + TEST_ASSERT_EQUAL( 20, bf.getRemainingLockout() ); 173 + 174 + bf.currentTimeSec = 1025; 175 + TEST_ASSERT_EQUAL( 5, bf.getRemainingLockout() ); 176 + } 177 + 178 + static void test_remaining_lockout_expires() { 179 + SimBruteForce bf; 180 + bf.currentTimeSec = 1000; 181 + for ( int i = 0; i < 4; i++ ) 182 + bf.registerFailure(); 183 + 184 + bf.currentTimeSec = 1030; 185 + TEST_ASSERT_EQUAL( 0, bf.getRemainingLockout() ); 186 + 187 + bf.currentTimeSec = 1031; 188 + TEST_ASSERT_EQUAL( 0, bf.getRemainingLockout() ); 189 + } 190 + 191 + static void test_remaining_lockout_300s_at_7_attempts() { 192 + SimBruteForce bf; 193 + bf.currentTimeSec = 5000; 194 + for ( int i = 0; i < 7; i++ ) 195 + bf.registerFailure(); 196 + 197 + TEST_ASSERT_EQUAL( 300, bf.getRemainingLockout() ); 198 + 199 + bf.currentTimeSec = 5150; 200 + TEST_ASSERT_EQUAL( 150, bf.getRemainingLockout() ); 201 + } 202 + 203 + static void test_remaining_lockout_one_second_left() { 204 + SimBruteForce bf; 205 + bf.currentTimeSec = 1000; 206 + for ( int i = 0; i < 4; i++ ) 207 + bf.registerFailure(); 208 + 209 + bf.currentTimeSec = 1029; 210 + TEST_ASSERT_EQUAL( 1, bf.getRemainingLockout() ); 211 + } 212 + 213 + static void test_remaining_lockout_extends_on_repeated_failures() { 214 + SimBruteForce bf; 215 + bf.currentTimeSec = 1000; 216 + for ( int i = 0; i < 4; i++ ) 217 + bf.registerFailure(); 218 + TEST_ASSERT_EQUAL( 30, bf.getRemainingLockout() ); 219 + 220 + // 5th failure at 1010 → lockout until 1040 221 + bf.currentTimeSec = 1010; 222 + bf.registerFailure(); 223 + TEST_ASSERT_EQUAL( 30, bf.getRemainingLockout() ); 224 + 225 + bf.currentTimeSec = 1035; 226 + TEST_ASSERT_EQUAL( 5, bf.getRemainingLockout() ); 227 + } 228 + 229 + static void test_remaining_lockout_progressive_sequence() { 230 + SimBruteForce bf; 231 + bf.currentTimeSec = 0; 232 + for ( int i = 0; i < 3; i++ ) 233 + TEST_ASSERT_EQUAL( 0, bf.registerFailure() ); 234 + 235 + uint32_t lock4 = bf.registerFailure(); 236 + TEST_ASSERT_EQUAL( 30, lock4 ); 237 + bf.currentTimeSec = 31; 238 + 239 + TEST_ASSERT_EQUAL( 30, bf.registerFailure() ); 240 + bf.currentTimeSec = 62; 241 + 242 + TEST_ASSERT_EQUAL( 30, bf.registerFailure() ); 243 + bf.currentTimeSec = 93; 244 + 245 + TEST_ASSERT_EQUAL( 300, bf.registerFailure() ); 246 + TEST_ASSERT_EQUAL( 300, bf.getRemainingLockout() ); 247 + } 248 + 249 + static void test_wipe_returns_uint32_max() { 250 + SimBruteForce bf; 251 + bf.currentTimeSec = 100; 252 + for ( int i = 0; i < 9; i++ ) 253 + bf.registerFailure(); 254 + TEST_ASSERT_EQUAL( UINT32_MAX, bf.registerFailure() ); 255 + TEST_ASSERT_TRUE( shouldWipe( bf.attempts ) ); 256 + } 257 + 258 + static void test_reset_clears_lockout() { 259 + SimBruteForce bf; 260 + bf.currentTimeSec = 100; 261 + for ( int i = 0; i < 5; i++ ) 262 + bf.registerFailure(); 263 + TEST_ASSERT_GREATER_THAN( 0, bf.getRemainingLockout() ); 264 + 265 + bf.resetAttempts(); 266 + TEST_ASSERT_EQUAL( 0, bf.getRemainingLockout() ); 267 + TEST_ASSERT_EQUAL( 0, bf.attempts ); 268 + } 269 + 270 + // --- Tests: RTC arithmetic ------------------------------------------------- 271 + // --- Tests ----------------------------------------------------------------- 272 + static void test_rtc_total_seconds_computation() { 273 + // ARRANGE 274 + uint8_t hours = 14, minutes = 30, seconds = 45; 275 + 276 + // ACT 277 + uint32_t totalSec = hours * 3600u + minutes * 60u + seconds; 278 + 279 + // ASSERT 280 + TEST_ASSERT_EQUAL( 52245, totalSec ); 281 + TEST_ASSERT_EQUAL( 0, 0 * 3600u + 0 * 60u + 0 ); 282 + TEST_ASSERT_EQUAL( 86399, 23 * 3600u + 59 * 60u + 59 ); 283 + } 284 + 285 + // --- Fake function definitions --------------------------------------------- 286 + // --- Function declarations ------------------------------------------------- 287 + // (none — all logic is replicated inline above) 288 + 289 + // --- Helper definitions ---------------------------------------------------- 290 + // --- State variables ------------------------------------------------------- 291 + static uint32_t getLockoutDuration( uint8_t attempts ) { 292 + if ( attempts < kShortLockStart ) 293 + return 0; 294 + if ( attempts < kLongLockStart ) 295 + return kShortLockSec; 296 + return kLongLockSec; 297 + } 298 + 299 + static bool shouldWipe( uint8_t attempts ) { 300 + return attempts >= kWipeThreshold; 301 + } 302 + 303 + // --- main() ---------------------------------------------------------------- 304 + // --- main() ---------------------------------------------------------------- 305 + int main( int /*argc*/, char** /*argv*/ ) { 306 + UNITY_BEGIN(); 307 + 308 + // Lockout duration 309 + RUN_TEST( test_lockout_duration_no_lockout_first_3_attempts ); 310 + RUN_TEST( test_lockout_duration_30s_after_4_attempts ); 311 + RUN_TEST( test_lockout_duration_300s_after_7_attempts ); 312 + RUN_TEST( test_lockout_duration_zero_attempts_no_lockout ); 313 + RUN_TEST( test_lockout_duration_boundary_short_to_long ); 314 + RUN_TEST( test_lockout_duration_monotonically_increases ); 315 + 316 + // Wipe threshold 317 + RUN_TEST( test_wipe_no_wipe_before_10 ); 318 + RUN_TEST( test_wipe_at_10_and_beyond ); 319 + RUN_TEST( test_wipe_boundary_long_to_wipe ); 320 + 321 + // Remaining lockout 322 + RUN_TEST( test_remaining_lockout_no_failures ); 323 + RUN_TEST( test_remaining_lockout_below_threshold ); 324 + RUN_TEST( test_remaining_lockout_30s_immediately ); 325 + RUN_TEST( test_remaining_lockout_decreases_with_time ); 326 + RUN_TEST( test_remaining_lockout_expires ); 327 + RUN_TEST( test_remaining_lockout_300s_at_7_attempts ); 328 + RUN_TEST( test_remaining_lockout_one_second_left ); 329 + RUN_TEST( test_remaining_lockout_extends_on_repeated_failures ); 330 + RUN_TEST( test_remaining_lockout_progressive_sequence ); 331 + RUN_TEST( test_wipe_returns_uint32_max ); 332 + RUN_TEST( test_reset_clears_lockout ); 333 + 334 + // RTC arithmetic 335 + RUN_TEST( test_rtc_total_seconds_computation ); 336 + 337 + return UNITY_END(); 338 + }
+291
test/vault/test_vault_recovery_logic/test_vault_recovery_logic.cpp
··· 1 + // Kleidos — Native test: Vault Recovery Logic (rekey-recovery decision logic) 2 + // Build: pio test -e native -f vault/test_vault_recovery_logic 3 + 4 + // --- File header ----------------------------------------------------------- 5 + 6 + // Group 1: module(s) under test 7 + #include "vault/vault_recovery_logic.h" 8 + 9 + // Group 3: stdlib + Unity 10 + #include <unity.h> 11 + 12 + // --- Constants ------------------------------------------------------------- 13 + 14 + using vaultrec::Action; 15 + using vaultrec::decideRecoveryAction; 16 + using vaultrec::InspectionInput; 17 + 18 + // --- Helper definitions ---------------------------------------------------- 19 + 20 + static InspectionInput makeIn( bool hasShadow, bool hasBak, bool hasMeta, uint32_t metaKv, 21 + uint32_t minKv, uint32_t maxKv, uint32_t count, 22 + bool hasOld = false ); 23 + 24 + // --- setUp / tearDown ------------------------------------------------------ 25 + 26 + void setUp() {} 27 + void tearDown() { /* intentionally empty */ } 28 + 29 + // --- Tests ----------------------------------------------------------------- 30 + 31 + // NOTHING 32 + 33 + static void test_clean_fs_requires_no_action() { 34 + // ACT 35 + auto result = decideRecoveryAction( false, false, true ); 36 + 37 + // ASSERT 38 + TEST_ASSERT_EQUAL( static_cast<int>( Action::NOTHING ), static_cast<int>( result ) ); 39 + } 40 + 41 + static void test_empty_fs_requires_no_action() { 42 + // ACT — fresh device, no vault at all yet 43 + auto result = decideRecoveryAction( false, false, false ); 44 + 45 + // ASSERT 46 + TEST_ASSERT_EQUAL( static_cast<int>( Action::NOTHING ), static_cast<int>( result ) ); 47 + } 48 + 49 + // ROLLBACK_SHADOW — pre-commit crash 50 + 51 + static void test_shadow_without_bak_rolls_back_shadow() { 52 + // ACT 53 + auto result = decideRecoveryAction( true, false, true ); 54 + 55 + // ASSERT 56 + TEST_ASSERT_EQUAL( static_cast<int>( Action::RollbackShadow ), static_cast<int>( result ) ); 57 + } 58 + 59 + // RESTORE_META — mid-commit crash 60 + 61 + static void test_bak_without_shadow_restores_meta() { 62 + // Shadow already fully purged but meta.bak still present (commit finished 63 + // swaps but crashed before dropping meta.bak). 64 + 65 + // ACT 66 + auto result = decideRecoveryAction( false, true, true ); 67 + 68 + // ASSERT 69 + TEST_ASSERT_EQUAL( static_cast<int>( Action::RestoreMeta ), static_cast<int>( result ) ); 70 + } 71 + 72 + static void test_bak_with_shadow_restores_meta() { 73 + // Full mid-commit: meta.bak is present alongside shadow files. 74 + 75 + // ACT 76 + auto result = decideRecoveryAction( true, true, true ); 77 + 78 + // ASSERT 79 + TEST_ASSERT_EQUAL( static_cast<int>( Action::RestoreMeta ), static_cast<int>( result ) ); 80 + } 81 + 82 + static void test_bak_without_canonical_meta_still_restores() { 83 + // Worst case: canonical meta was removed and we crashed before the new 84 + // one could be renamed into place. meta.bak is the only survivor. 85 + 86 + // ACT 87 + auto result = decideRecoveryAction( true, true, false ); 88 + 89 + // ASSERT 90 + TEST_ASSERT_EQUAL( static_cast<int>( Action::RestoreMeta ), static_cast<int>( result ) ); 91 + } 92 + 93 + // G4 — keyVersion consistency cases 94 + 95 + static void test_consistent_creds_no_action() { 96 + // All canonical creds match meta.keyVersion → nothing to do. 97 + 98 + // ARRANGE 99 + auto in = makeIn( false, false, true, 100 + /*metaKv=*/3, /*minKv=*/3, /*maxKv=*/3, /*count=*/12 ); 101 + 102 + // ACT 103 + auto result = decideRecoveryAction( in ); 104 + 105 + // ASSERT 106 + TEST_ASSERT_EQUAL( static_cast<int>( Action::NOTHING ), static_cast<int>( result ) ); 107 + } 108 + 109 + static void test_legacy_zero_kv_consistent_no_action() { 110 + // Legacy vault: meta.kv missing (=0) and no .kv sidecars (all =0). 111 + // Should look fully consistent and trigger no recovery. 112 + 113 + // ARRANGE 114 + auto in = makeIn( false, false, true, 115 + /*metaKv=*/0, /*minKv=*/0, /*maxKv=*/0, /*count=*/8 ); 116 + 117 + // ACT 118 + auto result = decideRecoveryAction( in ); 119 + 120 + // ASSERT 121 + TEST_ASSERT_EQUAL( static_cast<int>( Action::NOTHING ), static_cast<int>( result ) ); 122 + } 123 + 124 + static void test_partial_rename_with_bak_rolls_back_partial() { 125 + // meta.kv was bumped to 5; some canonical creds still report 4 126 + // (their rename never executed). meta.bak still present → can roll 127 + // back the meta swap (best effort, data loss is logged separately). 128 + 129 + // ARRANGE 130 + auto in = makeIn( /*hasShadow=*/true, /*hasBak=*/true, /*hasMeta=*/true, 131 + /*metaKv=*/5, /*minKv=*/4, /*maxKv=*/5, /*count=*/10 ); 132 + 133 + // ACT 134 + auto result = decideRecoveryAction( in ); 135 + 136 + // ASSERT 137 + TEST_ASSERT_EQUAL( static_cast<int>( Action::RollbackPartialRename ), 138 + static_cast<int>( result ) ); 139 + } 140 + 141 + static void test_partial_rename_no_bak_requires_manual() { 142 + // Same inconsistency but meta.bak is gone → no automatic recovery 143 + // is safe; the caller must wipe + restore from a user backup. 144 + 145 + // ARRANGE 146 + auto in = makeIn( /*hasShadow=*/false, /*hasBak=*/false, /*hasMeta=*/true, 147 + /*metaKv=*/5, /*minKv=*/4, /*maxKv=*/5, /*count=*/10 ); 148 + 149 + // ACT 150 + auto result = decideRecoveryAction( in ); 151 + 152 + // ASSERT 153 + TEST_ASSERT_EQUAL( static_cast<int>( Action::RequireManualRecovery ), 154 + static_cast<int>( result ) ); 155 + } 156 + 157 + static void test_cred_ahead_of_meta_requires_manual() { 158 + // Pathological: a cred reports kv > meta.kv. Should not happen in 159 + // normal operation (meta is swapped before per-cred renames in the 160 + // current rekey path) — defensive REQUIRE_MANUAL_RECOVERY. 161 + 162 + // ARRANGE 163 + auto in = makeIn( /*hasShadow=*/false, /*hasBak=*/false, /*hasMeta=*/true, 164 + /*metaKv=*/2, /*minKv=*/2, /*maxKv=*/3, /*count=*/5 ); 165 + 166 + // ACT 167 + auto result = decideRecoveryAction( in ); 168 + 169 + // ASSERT 170 + TEST_ASSERT_EQUAL( static_cast<int>( Action::RequireManualRecovery ), 171 + static_cast<int>( result ) ); 172 + } 173 + 174 + static void test_cred_ahead_of_meta_with_bak_rolls_back_partial() { 175 + // Same pathological case but meta.bak present → rollback path. 176 + 177 + // ARRANGE 178 + auto in = makeIn( /*hasShadow=*/false, /*hasBak=*/true, /*hasMeta=*/true, 179 + /*metaKv=*/2, /*minKv=*/2, /*maxKv=*/3, /*count=*/5 ); 180 + 181 + // ACT 182 + auto result = decideRecoveryAction( in ); 183 + 184 + // ASSERT 185 + TEST_ASSERT_EQUAL( static_cast<int>( Action::RollbackPartialRename ), 186 + static_cast<int>( result ) ); 187 + } 188 + 189 + static void test_g4_takes_priority_over_shadow_only() { 190 + // If a partial-rename is detected, it must take priority over the 191 + // legacy ROLLBACK_SHADOW decision (otherwise we'd miss the data 192 + // safety issue). 193 + 194 + // ARRANGE 195 + auto in = makeIn( /*hasShadow=*/true, /*hasBak=*/false, /*hasMeta=*/true, 196 + /*metaKv=*/5, /*minKv=*/4, /*maxKv=*/5, /*count=*/10 ); 197 + 198 + // ACT 199 + auto result = decideRecoveryAction( in ); 200 + 201 + // ASSERT 202 + TEST_ASSERT_EQUAL( static_cast<int>( Action::RequireManualRecovery ), 203 + static_cast<int>( result ) ); 204 + } 205 + 206 + // H2 — /vault/old/ staging tree 207 + 208 + // Mid-rename crash with /vault/old/ + meta.bak: rollback losslessly. 209 + static void test_h2_old_dir_with_bak_and_inconsistent_creds_rolls_back_from_old() { 210 + // ARRANGE 211 + auto in = makeIn( /*hasShadow=*/true, /*hasBak=*/true, /*hasMeta=*/true, 212 + /*metaKv=*/6, /*minKv=*/5, /*maxKv=*/6, /*count=*/8, 213 + /*hasOld=*/true ); 214 + 215 + // ACT 216 + auto result = decideRecoveryAction( in ); 217 + 218 + // ASSERT 219 + TEST_ASSERT_EQUAL( static_cast<int>( Action::RollbackFromOldDir ), static_cast<int>( result ) ); 220 + } 221 + 222 + // Rekey already finished but cleanup didn't drop /vault/old/ yet. 223 + // All creds match meta — nothing to restore. The I/O caller is 224 + // responsible for purging the leftover staging tree. 225 + static void test_h2_old_dir_with_consistent_creds_is_nothing() { 226 + // ARRANGE 227 + auto in = makeIn( /*hasShadow=*/false, /*hasBak=*/true, /*hasMeta=*/true, 228 + /*metaKv=*/6, /*minKv=*/6, /*maxKv=*/6, /*count=*/8, 229 + /*hasOld=*/true ); 230 + 231 + // ACT 232 + auto result = decideRecoveryAction( in ); 233 + 234 + // ASSERT 235 + TEST_ASSERT_EQUAL( static_cast<int>( Action::NOTHING ), static_cast<int>( result ) ); 236 + } 237 + 238 + // Pathological: /vault/old/ present but meta.bak gone and creds 239 + // inconsistent. We have no safe target meta version → escalate. 240 + static void test_h2_old_dir_without_bak_and_inconsistent_creds_requires_manual() { 241 + // ARRANGE 242 + auto in = makeIn( /*hasShadow=*/false, /*hasBak=*/false, /*hasMeta=*/true, 243 + /*metaKv=*/6, /*minKv=*/5, /*maxKv=*/6, /*count=*/8, 244 + /*hasOld=*/true ); 245 + 246 + // ACT 247 + auto result = decideRecoveryAction( in ); 248 + 249 + // ASSERT 250 + TEST_ASSERT_EQUAL( static_cast<int>( Action::RequireManualRecovery ), 251 + static_cast<int>( result ) ); 252 + } 253 + 254 + // --- State variables ------------------------------------------------------- 255 + 256 + static InspectionInput makeIn( bool hasShadow, bool hasBak, bool hasMeta, uint32_t metaKv, 257 + uint32_t minKv, uint32_t maxKv, uint32_t count, bool hasOld ) { 258 + InspectionInput in{}; 259 + in.hasShadowDir = hasShadow; 260 + in.hasOldDir = hasOld; 261 + in.hasMetaBak = hasBak; 262 + in.hasCanonicalMeta = hasMeta; 263 + in.metaKeyVersion = metaKv; 264 + in.minCredKeyVersion = minKv; 265 + in.maxCredKeyVersion = maxKv; 266 + in.credCount = count; 267 + return in; 268 + } 269 + 270 + // --- main() ---------------------------------------------------------------- 271 + 272 + int main( int /*argc*/, char** /*argv*/ ) { 273 + UNITY_BEGIN(); 274 + RUN_TEST( test_clean_fs_requires_no_action ); 275 + RUN_TEST( test_empty_fs_requires_no_action ); 276 + RUN_TEST( test_shadow_without_bak_rolls_back_shadow ); 277 + RUN_TEST( test_bak_without_shadow_restores_meta ); 278 + RUN_TEST( test_bak_with_shadow_restores_meta ); 279 + RUN_TEST( test_bak_without_canonical_meta_still_restores ); 280 + RUN_TEST( test_consistent_creds_no_action ); 281 + RUN_TEST( test_legacy_zero_kv_consistent_no_action ); 282 + RUN_TEST( test_partial_rename_with_bak_rolls_back_partial ); 283 + RUN_TEST( test_partial_rename_no_bak_requires_manual ); 284 + RUN_TEST( test_cred_ahead_of_meta_requires_manual ); 285 + RUN_TEST( test_cred_ahead_of_meta_with_bak_rolls_back_partial ); 286 + RUN_TEST( test_g4_takes_priority_over_shadow_only ); 287 + RUN_TEST( test_h2_old_dir_with_bak_and_inconsistent_creds_rolls_back_from_old ); 288 + RUN_TEST( test_h2_old_dir_with_consistent_creds_is_nothing ); 289 + RUN_TEST( test_h2_old_dir_without_bak_and_inconsistent_creds_requires_manual ); 290 + return UNITY_END(); 291 + }
+1004
test/vault/test_vault_store/test_vault_store.cpp
··· 1 + // Kleidos — Native test: VaultStore 2 + // Tests credential JSON serialization/deserialization, v2 meta structure, 3 + // PIN verifier computation, favorites bitfield, credential ordering, 4 + // HMAC integrity, encrypt/decrypt round-trips, and CSV edge cases. 5 + // Uses VaultCrypto (real mbedTLS) and ArduinoJson. 6 + // Build: pio test -e native -f vault/test_vault_store 7 + 8 + #include "crypto/vault_crypto.h" 9 + 10 + #include <ArduinoJson.h> 11 + 12 + #include <algorithm> 13 + #include <cstdint> 14 + #include <cstring> 15 + 16 + #include <unity.h> 17 + 18 + // Platform stubs 19 + // references millis() directly. 20 + static uint32_t fakeMillis = 0; 21 + extern "C" uint32_t millis() { 22 + return fakeMillis; 23 + } 24 + 25 + // --- Constants ------------------------------------------------------------- 26 + // --- Constants ------------------------------------------------------------- 27 + static constexpr size_t kMaxCredentials = 50; 28 + static constexpr size_t kMaxTotp = 20; 29 + static constexpr size_t kMaxNameLen = 32; 30 + static constexpr size_t kMaxUserLen = 64; 31 + static constexpr size_t kMaxPassLen = 128; 32 + static constexpr size_t kMaxUrlLen = 128; 33 + 34 + static constexpr uint8_t kMetaV2Magic[3] = { 0x53, 0x50, 0x02 }; 35 + static constexpr char kPinVerifyTag[] = "kleidos-pin-v2"; 36 + 37 + // --- State variables ------------------------------------------------------- 38 + // State variables 39 + // --- Function declarations ------------------------------------------------- 40 + // (none — tests are stateless) 41 + 42 + // --- State variables ------------------------------------------------------- 43 + struct VaultMetaV2 { 44 + uint8_t magic[3]; 45 + uint8_t kdfSalt[kSaltSize]; 46 + uint8_t pinVerifier[kPinHashSize]; 47 + uint8_t hmacSalt[kSaltSize]; 48 + }; 49 + 50 + struct VaultMeta { 51 + uint8_t pinSalt[kSaltSize]; 52 + uint8_t pinHash[kPinHashSize]; 53 + uint8_t encSalt[kSaltSize]; 54 + uint8_t hmacSalt[kSaltSize]; 55 + }; 56 + 57 + struct Credential { 58 + char name[kMaxNameLen + 1]; 59 + char user[kMaxUserLen + 1]; 60 + char pass[kMaxPassLen + 1]; 61 + char url[kMaxUrlLen + 1]; 62 + }; 63 + 64 + struct CredentialEntry { 65 + uint8_t id; 66 + char name[kMaxNameLen + 1]; 67 + bool favorite; 68 + uint8_t sortOrder; 69 + }; 70 + 71 + // --- Function declarations ------------------------------------------------- 72 + // --- Helper definitions ---------------------------------------------------- 73 + static bool simulateProvision( const char* pin, size_t pinLen, VaultMetaV2& meta, uint8_t* keyOut ); 74 + static bool simulateUnlock( const char* pin, size_t pinLen, const VaultMetaV2& meta, 75 + uint8_t* keyOut ); 76 + static void setFavoriteBit( uint8_t* favBits, uint8_t id, bool fav ); 77 + static bool getFavoriteBit( const uint8_t* favBits, uint8_t id ); 78 + static void buildOrderArray( const uint8_t* ids, uint8_t count, uint8_t* order ); 79 + static void sortEntries( CredentialEntry* entries, uint8_t n ); 80 + 81 + // --- setUp / tearDown ------------------------------------------------------ 82 + // setUp / tearDown 83 + // --- setUp / tearDown ------------------------------------------------------ 84 + void setUp() { 85 + fakeMillis = 0; 86 + } 87 + 88 + void tearDown() { /* intentionally empty */ } 89 + 90 + // --- Tests: credential JSON serialization ---------------------------------- 91 + // --- Tests ----------------------------------------------------------------- 92 + static void test_credential_json_serialize_roundtrip() { 93 + // ARRANGE 94 + Credential c = {}; 95 + strncpy( c.name, "GitHub", sizeof( c.name ) - 1 ); 96 + strncpy( c.user, "user@test.com", sizeof( c.user ) - 1 ); 97 + strncpy( c.pass, "s3cret!Pass", sizeof( c.pass ) - 1 ); 98 + strncpy( c.url, "https://github.com", sizeof( c.url ) - 1 ); 99 + 100 + // ACT 101 + JsonDocument doc; 102 + doc["name"] = c.name; 103 + doc["user"] = c.user; 104 + doc["pass"] = c.pass; 105 + doc["url"] = c.url; 106 + char buf[512]; 107 + size_t len = serializeJson( doc, buf, sizeof( buf ) ); 108 + 109 + JsonDocument doc2; 110 + DeserializationError err = deserializeJson( doc2, buf, len ); 111 + 112 + // ASSERT 113 + TEST_ASSERT_GREATER_THAN( 0, len ); 114 + TEST_ASSERT_TRUE( err == DeserializationError::Ok ); 115 + TEST_ASSERT_EQUAL_STRING( "GitHub", doc2["name"] ); 116 + TEST_ASSERT_EQUAL_STRING( "user@test.com", doc2["user"] ); 117 + TEST_ASSERT_EQUAL_STRING( "s3cret!Pass", doc2["pass"] ); 118 + TEST_ASSERT_EQUAL_STRING( "https://github.com", doc2["url"] ); 119 + } 120 + 121 + static void test_credential_json_max_name_length_preserved() { 122 + // ARRANGE 123 + char longName[kMaxNameLen + 1]; 124 + memset( longName, 'A', kMaxNameLen ); 125 + longName[kMaxNameLen] = '\0'; 126 + 127 + // ACT 128 + JsonDocument doc; 129 + doc["name"] = longName; 130 + doc["user"] = ""; 131 + doc["pass"] = ""; 132 + doc["url"] = ""; 133 + char buf[512]; 134 + size_t len = serializeJson( doc, buf, sizeof( buf ) ); 135 + JsonDocument doc2; 136 + deserializeJson( doc2, buf, len ); 137 + 138 + // ASSERT 139 + TEST_ASSERT_EQUAL_STRING( longName, doc2["name"] ); 140 + } 141 + 142 + static void test_credential_json_empty_fields_roundtrip() { 143 + // ARRANGE + ACT 144 + JsonDocument doc; 145 + doc["name"] = ""; 146 + doc["user"] = ""; 147 + doc["pass"] = ""; 148 + doc["url"] = ""; 149 + char buf[256]; 150 + size_t len = serializeJson( doc, buf, sizeof( buf ) ); 151 + JsonDocument doc2; 152 + DeserializationError err = deserializeJson( doc2, buf, len ); 153 + 154 + // ASSERT 155 + TEST_ASSERT_TRUE( err == DeserializationError::Ok ); 156 + TEST_ASSERT_EQUAL_STRING( "", doc2["name"] ); 157 + TEST_ASSERT_EQUAL_STRING( "", doc2["user"] ); 158 + TEST_ASSERT_EQUAL_STRING( "", doc2["pass"] ); 159 + TEST_ASSERT_EQUAL_STRING( "", doc2["url"] ); 160 + } 161 + 162 + static void test_credential_json_special_characters_roundtrip() { 163 + // ARRANGE 164 + JsonDocument doc; 165 + doc["name"] = "Test \"Quotes\" & <Tags>"; 166 + doc["user"] = "user+tag@example.com"; 167 + doc["pass"] = "p@$$w0rd!#%^&*(){}[]"; 168 + doc["url"] = "https://example.com/path?q=1&b=2"; 169 + 170 + // ACT 171 + char buf[512]; 172 + size_t len = serializeJson( doc, buf, sizeof( buf ) ); 173 + JsonDocument doc2; 174 + DeserializationError err = deserializeJson( doc2, buf, len ); 175 + 176 + // ASSERT 177 + TEST_ASSERT_TRUE( err == DeserializationError::Ok ); 178 + TEST_ASSERT_EQUAL_STRING( "Test \"Quotes\" & <Tags>", doc2["name"] ); 179 + TEST_ASSERT_EQUAL_STRING( "p@$$w0rd!#%^&*(){}[]", doc2["pass"] ); 180 + } 181 + 182 + static void test_credential_json_unicode_utf8_roundtrip() { 183 + // ARRANGE 184 + JsonDocument doc; 185 + doc["name"] = "Contraseña"; 186 + doc["user"] = "用户@例子.com"; 187 + doc["pass"] = "🔑secret"; 188 + doc["url"] = ""; 189 + 190 + // ACT 191 + char buf[512]; 192 + size_t len = serializeJson( doc, buf, sizeof( buf ) ); 193 + JsonDocument doc2; 194 + DeserializationError err = deserializeJson( doc2, buf, len ); 195 + 196 + // ASSERT 197 + TEST_ASSERT_TRUE( err == DeserializationError::Ok ); 198 + TEST_ASSERT_EQUAL_STRING( "Contraseña", doc2["name"] ); 199 + } 200 + 201 + static void test_credential_json_missing_optional_fields_default_empty() { 202 + // ARRANGE 203 + const char* json = "{\"name\":\"OnlyName\"}"; 204 + JsonDocument doc; 205 + deserializeJson( doc, json ); 206 + 207 + // ACT 208 + Credential c = {}; 209 + strncpy( c.name, doc["name"] | "", sizeof( c.name ) - 1 ); 210 + strncpy( c.user, doc["user"] | "", sizeof( c.user ) - 1 ); 211 + strncpy( c.pass, doc["pass"] | "", sizeof( c.pass ) - 1 ); 212 + strncpy( c.url, doc["url"] | "", sizeof( c.url ) - 1 ); 213 + 214 + // ASSERT 215 + TEST_ASSERT_EQUAL_STRING( "OnlyName", c.name ); 216 + TEST_ASSERT_EQUAL_STRING( "", c.user ); 217 + TEST_ASSERT_EQUAL_STRING( "", c.pass ); 218 + TEST_ASSERT_EQUAL_STRING( "", c.url ); 219 + } 220 + 221 + static void test_credential_json_list_serializes_all_entries() { 222 + // ARRANGE + ACT 223 + JsonDocument doc; 224 + JsonArray arr = doc.to<JsonArray>(); 225 + for ( int i = 0; i < 5; i++ ) { 226 + JsonObject obj = arr.add<JsonObject>(); 227 + obj["id"] = i; 228 + char name[16]; 229 + snprintf( name, sizeof( name ), "Cred_%d", i ); 230 + obj["name"] = name; 231 + } 232 + char buf[512]; 233 + size_t len = serializeJson( doc, buf, sizeof( buf ) ); 234 + JsonDocument doc2; 235 + deserializeJson( doc2, buf, len ); 236 + JsonArray arr2 = doc2.as<JsonArray>(); 237 + 238 + // ASSERT 239 + TEST_ASSERT_EQUAL( 5, arr2.size() ); 240 + TEST_ASSERT_EQUAL( 0, arr2[0]["id"].as<int>() ); 241 + TEST_ASSERT_EQUAL_STRING( "Cred_0", arr2[0]["name"] ); 242 + TEST_ASSERT_EQUAL( 4, arr2[4]["id"].as<int>() ); 243 + } 244 + 245 + static void test_credential_json_invalid_input_returns_error() { 246 + // ARRANGE 247 + const char* badJson = "{name: missing quotes}"; 248 + 249 + // ACT 250 + JsonDocument doc; 251 + DeserializationError err = deserializeJson( doc, badJson ); 252 + 253 + // ASSERT 254 + TEST_ASSERT_FALSE( err == DeserializationError::Ok ); 255 + } 256 + 257 + static void test_credential_json_max_credentials_array() { 258 + // ARRANGE + ACT 259 + JsonDocument doc; 260 + JsonArray arr = doc.to<JsonArray>(); 261 + for ( int i = 0; i < (int)kMaxCredentials; i++ ) { 262 + JsonObject obj = arr.add<JsonObject>(); 263 + char name[16]; 264 + snprintf( name, sizeof( name ), "C%d", i ); 265 + obj["name"] = name; 266 + obj["id"] = i; 267 + } 268 + char buf[8192]; 269 + size_t len = serializeJson( doc, buf, sizeof( buf ) ); 270 + JsonDocument doc2; 271 + DeserializationError err = deserializeJson( doc2, buf, len ); 272 + 273 + // ASSERT 274 + TEST_ASSERT_GREATER_THAN( 0, len ); 275 + TEST_ASSERT_TRUE( err == DeserializationError::Ok ); 276 + TEST_ASSERT_EQUAL( (int)kMaxCredentials, (int)doc2.as<JsonArray>().size() ); 277 + } 278 + 279 + // --- Tests: TOTP JSON ------------------------------------------------------ 280 + // --- Tests ----------------------------------------------------------------- 281 + static void test_totp_json_6_digit_roundtrip() { 282 + // ARRANGE 283 + JsonDocument doc; 284 + doc["name"] = "GitHub TOTP"; 285 + doc["secret"] = "JBSWY3DPEHPK3PXP"; 286 + doc["digits"] = 6; 287 + doc["period"] = 30; 288 + 289 + // ACT 290 + char buf[256]; 291 + size_t len = serializeJson( doc, buf, sizeof( buf ) ); 292 + JsonDocument doc2; 293 + DeserializationError err = deserializeJson( doc2, buf, len ); 294 + 295 + // ASSERT 296 + TEST_ASSERT_GREATER_THAN( 0, len ); 297 + TEST_ASSERT_TRUE( err == DeserializationError::Ok ); 298 + TEST_ASSERT_EQUAL_STRING( "GitHub TOTP", doc2["name"] ); 299 + TEST_ASSERT_EQUAL_STRING( "JBSWY3DPEHPK3PXP", doc2["secret"] ); 300 + TEST_ASSERT_EQUAL( 6, doc2["digits"].as<int>() ); 301 + TEST_ASSERT_EQUAL( 30, doc2["period"].as<int>() ); 302 + } 303 + 304 + static void test_totp_json_8_digit_period_60() { 305 + // ARRANGE 306 + JsonDocument doc; 307 + doc["name"] = "8-digit TOTP"; 308 + doc["secret"] = "MFRGG"; 309 + doc["digits"] = 8; 310 + doc["period"] = 60; 311 + 312 + // ACT 313 + char buf[256]; 314 + size_t len = serializeJson( doc, buf, sizeof( buf ) ); 315 + JsonDocument doc2; 316 + deserializeJson( doc2, buf, len ); 317 + 318 + // ASSERT 319 + TEST_ASSERT_EQUAL( 8, doc2["digits"].as<int>() ); 320 + TEST_ASSERT_EQUAL( 60, doc2["period"].as<int>() ); 321 + } 322 + 323 + static void test_totp_hex_secret_encode_decode_roundtrip() { 324 + // ARRANGE 325 + uint8_t secret[] = { 0xDE, 0xAD, 0xBE, 0xEF, 0x42 }; 326 + uint8_t secretLen = 5; 327 + 328 + // ACT — encode 329 + char hexSecret[129]; 330 + for ( uint8_t i = 0; i < secretLen; i++ ) 331 + snprintf( hexSecret + i * 2, 3, "%02x", secret[i] ); 332 + hexSecret[secretLen * 2] = '\0'; 333 + 334 + // ACT — decode 335 + uint8_t decoded[64]; 336 + size_t hexLen = strlen( hexSecret ); 337 + for ( size_t i = 0; i < hexLen / 2 && i < 64; i++ ) { 338 + char hex[3] = { hexSecret[i * 2], hexSecret[i * 2 + 1], '\0' }; 339 + decoded[i] = static_cast<uint8_t>( strtoul( hex, nullptr, 16 ) ); 340 + } 341 + 342 + // ASSERT 343 + TEST_ASSERT_EQUAL_STRING( "deadbeef42", hexSecret ); 344 + TEST_ASSERT_EQUAL_MEMORY( secret, decoded, secretLen ); 345 + } 346 + 347 + // --- Tests: CSV parsing ---------------------------------------------------- 348 + // --- Tests ----------------------------------------------------------------- 349 + static void test_csv_simple_line_parsed_correctly() { 350 + // ARRANGE 351 + const char* csvLine = "GitHub,john@test.com,MyP@ss!,https://github.com"; 352 + char line[256]; 353 + strncpy( line, csvLine, sizeof( line ) - 1 ); 354 + line[sizeof( line ) - 1] = '\0'; 355 + 356 + // ACT 357 + char* fields[4] = {}; 358 + int idx = 0; 359 + char* tok = strtok( line, "," ); 360 + while ( tok && idx < 4 ) { 361 + fields[idx++] = tok; 362 + tok = strtok( nullptr, "," ); 363 + } 364 + 365 + // ASSERT 366 + TEST_ASSERT_EQUAL( 4, idx ); 367 + TEST_ASSERT_EQUAL_STRING( "GitHub", fields[0] ); 368 + TEST_ASSERT_EQUAL_STRING( "john@test.com", fields[1] ); 369 + TEST_ASSERT_EQUAL_STRING( "MyP@ss!", fields[2] ); 370 + TEST_ASSERT_EQUAL_STRING( "https://github.com", fields[3] ); 371 + } 372 + 373 + static void test_csv_url_with_multiple_colons_preserved() { 374 + // ARRANGE — split only on first 3 commas; rest is URL 375 + const char* csvLine = "MyServer,admin,secret,https://host:8080/path"; 376 + char line[256]; 377 + strncpy( line, csvLine, sizeof( line ) - 1 ); 378 + line[sizeof( line ) - 1] = '\0'; 379 + 380 + // ACT 381 + char* fields[4] = {}; 382 + int idx = 0; 383 + char* p = line; 384 + for ( int i = 0; i < 3 && p; i++ ) { 385 + fields[idx++] = p; 386 + char* comma = strchr( p, ',' ); 387 + if ( comma ) { 388 + *comma = '\0'; 389 + p = comma + 1; 390 + } else { 391 + p = nullptr; 392 + } 393 + } 394 + if ( p ) 395 + fields[idx++] = p; 396 + 397 + // ASSERT 398 + TEST_ASSERT_EQUAL( 4, idx ); 399 + TEST_ASSERT_EQUAL_STRING( "MyServer", fields[0] ); 400 + TEST_ASSERT_EQUAL_STRING( "admin", fields[1] ); 401 + TEST_ASSERT_EQUAL_STRING( "secret", fields[2] ); 402 + TEST_ASSERT_EQUAL_STRING( "https://host:8080/path", fields[3] ); 403 + } 404 + 405 + // --- Tests: backup / export JSON ------------------------------------------- 406 + // --- Tests ----------------------------------------------------------------- 407 + static void test_backup_json_format_roundtrip() { 408 + // ARRANGE + ACT 409 + JsonDocument doc; 410 + JsonArray arr = doc.to<JsonArray>(); 411 + JsonObject f1 = arr.add<JsonObject>(); 412 + f1["file"] = "cred_00.bin"; 413 + f1["data"] = "AQIDBA=="; 414 + JsonObject f2 = arr.add<JsonObject>(); 415 + f2["file"] = "cred_01.bin"; 416 + f2["data"] = "BQYHCA=="; 417 + char buf[512]; 418 + size_t len = serializeJson( doc, buf, sizeof( buf ) ); 419 + JsonDocument doc2; 420 + deserializeJson( doc2, buf, len ); 421 + JsonArray arr2 = doc2.as<JsonArray>(); 422 + 423 + // ASSERT 424 + TEST_ASSERT_EQUAL( 2, arr2.size() ); 425 + TEST_ASSERT_EQUAL_STRING( "cred_00.bin", arr2[0]["file"] ); 426 + } 427 + 428 + static void test_export_json_structure_roundtrip() { 429 + // ARRANGE + ACT 430 + JsonDocument doc; 431 + doc["version"] = 1; 432 + doc["meta"] = "U1AB"; 433 + JsonArray creds = doc["credentials"].to<JsonArray>(); 434 + JsonObject c0 = creds.add<JsonObject>(); 435 + c0["id"] = 0; 436 + c0["data"] = "AQIDBA=="; 437 + char buf[512]; 438 + size_t len = serializeJson( doc, buf, sizeof( buf ) ); 439 + JsonDocument doc2; 440 + DeserializationError err = deserializeJson( doc2, buf, len ); 441 + 442 + // ASSERT 443 + TEST_ASSERT_TRUE( err == DeserializationError::Ok ); 444 + TEST_ASSERT_EQUAL( 1, doc2["version"].as<int>() ); 445 + TEST_ASSERT_EQUAL_STRING( "U1AB", doc2["meta"] ); 446 + TEST_ASSERT_EQUAL( 1, doc2["credentials"].as<JsonArray>().size() ); 447 + } 448 + 449 + static void test_import_rejects_wrong_version() { 450 + // ARRANGE 451 + const char* json = "{\"version\":99,\"meta\":\"AA==\",\"credentials\":[]}"; 452 + JsonDocument doc; 453 + deserializeJson( doc, json ); 454 + 455 + // ACT 456 + int version = doc["version"] | 0; 457 + 458 + // ASSERT 459 + TEST_ASSERT_NOT_EQUAL( 1, version ); 460 + } 461 + 462 + static void test_import_rejects_missing_version() { 463 + // ARRANGE 464 + const char* json = "{\"meta\":\"AA==\",\"credentials\":[]}"; 465 + JsonDocument doc; 466 + deserializeJson( doc, json ); 467 + 468 + // ASSERT 469 + TEST_ASSERT_EQUAL( 0, doc["version"] | 0 ); 470 + } 471 + 472 + static void test_import_skips_ids_at_or_above_max() { 473 + // ARRANGE 474 + JsonDocument doc; 475 + doc["version"] = 1; 476 + JsonArray creds = doc["credentials"].to<JsonArray>(); 477 + JsonObject good = creds.add<JsonObject>(); 478 + good["id"] = 0; 479 + good["data"] = "AQID"; 480 + JsonObject bad1 = creds.add<JsonObject>(); 481 + bad1["id"] = 0xFF; 482 + bad1["data"] = "BQYH"; 483 + JsonObject bad2 = creds.add<JsonObject>(); 484 + bad2["id"] = kMaxCredentials; 485 + bad2["data"] = "CAQE"; 486 + 487 + // ACT + ASSERT — only id=0 passes the guard 488 + for ( JsonObject obj : creds ) { 489 + uint8_t id = obj["id"] | 0xFF; 490 + if ( id < kMaxCredentials ) 491 + TEST_ASSERT_EQUAL( 0, id ); 492 + } 493 + } 494 + 495 + static void test_import_empty_credentials_array_accepted() { 496 + // ARRANGE 497 + const char* json = "{\"version\":1,\"meta\":\"AAAA\",\"credentials\":[]}"; 498 + JsonDocument doc; 499 + DeserializationError err = deserializeJson( doc, json ); 500 + 501 + // ASSERT 502 + TEST_ASSERT_TRUE( err == DeserializationError::Ok ); 503 + TEST_ASSERT_EQUAL( 0, doc["credentials"].as<JsonArray>().size() ); 504 + } 505 + 506 + // --- Tests: v2 meta structure ---------------------------------------------- 507 + // --- Tests ----------------------------------------------------------------- 508 + static void test_v2_meta_magic_bytes_correct() { 509 + TEST_ASSERT_EQUAL_HEX8( 0x53, kMetaV2Magic[0] ); 510 + TEST_ASSERT_EQUAL_HEX8( 0x50, kMetaV2Magic[1] ); 511 + TEST_ASSERT_EQUAL_HEX8( 0x02, kMetaV2Magic[2] ); 512 + } 513 + 514 + static void test_v2_meta_size_matches_spec() { 515 + // magic(3) + kdfSalt(16) + pinVerifier(32) + hmacSalt(16) = 67 516 + TEST_ASSERT_EQUAL( 67, sizeof( VaultMetaV2 ) ); 517 + } 518 + 519 + static void test_v1_meta_size_matches_spec() { 520 + // pinSalt(16) + pinHash(32) + encSalt(16) + hmacSalt(16) = 80 521 + TEST_ASSERT_EQUAL( 80, sizeof( VaultMeta ) ); 522 + } 523 + 524 + static void test_v2_size_is_distinguishable_from_v1() { 525 + TEST_ASSERT_NOT_EQUAL( sizeof( VaultMetaV2 ), sizeof( VaultMeta ) ); 526 + } 527 + 528 + // --- Tests: v2 PIN provision / unlock -------------------------------------- 529 + // --- Tests ----------------------------------------------------------------- 530 + static void test_v2_provision_and_unlock_correct_pin_succeeds() { 531 + // ARRANGE 532 + VaultMetaV2 meta; 533 + uint8_t provKey[kAesKeySize]; 534 + TEST_ASSERT_TRUE( simulateProvision( "1234", 4, meta, provKey ) ); 535 + 536 + // ACT 537 + uint8_t unlockKey[kAesKeySize]; 538 + bool ok = simulateUnlock( "1234", 4, meta, unlockKey ); 539 + 540 + // ASSERT 541 + TEST_ASSERT_TRUE( ok ); 542 + TEST_ASSERT_EQUAL_MEMORY( provKey, unlockKey, kAesKeySize ); 543 + } 544 + 545 + static void test_v2_unlock_wrong_pin_fails() { 546 + // ARRANGE 547 + VaultMetaV2 meta; 548 + uint8_t provKey[kAesKeySize]; 549 + TEST_ASSERT_TRUE( simulateProvision( "1234", 4, meta, provKey ) ); 550 + 551 + // ACT + ASSERT 552 + uint8_t unlockKey[kAesKeySize]; 553 + TEST_ASSERT_FALSE( simulateUnlock( "9999", 4, meta, unlockKey ) ); 554 + } 555 + 556 + static void test_v2_unlock_empty_pin_fails() { 557 + // ARRANGE 558 + VaultMetaV2 meta; 559 + uint8_t provKey[kAesKeySize]; 560 + TEST_ASSERT_TRUE( simulateProvision( "5678", 4, meta, provKey ) ); 561 + 562 + // ACT + ASSERT 563 + uint8_t unlockKey[kAesKeySize]; 564 + TEST_ASSERT_FALSE( simulateUnlock( "", 0, meta, unlockKey ) ); 565 + } 566 + 567 + static void test_v2_different_pins_produce_different_keys() { 568 + // ARRANGE 569 + VaultMetaV2 meta1, meta2; 570 + uint8_t key1[kAesKeySize], key2[kAesKeySize]; 571 + 572 + // ACT 573 + TEST_ASSERT_TRUE( simulateProvision( "1111", 4, meta1, key1 ) ); 574 + TEST_ASSERT_TRUE( simulateProvision( "2222", 4, meta2, key2 ) ); 575 + 576 + // ASSERT 577 + TEST_ASSERT_FALSE( VaultCrypto::constantTimeEqual( key1, key2, kAesKeySize ) ); 578 + } 579 + 580 + static void test_v2_same_pin_same_salt_deterministic_key() { 581 + // ARRANGE — fixed salt → deterministic key derivation 582 + VaultMetaV2 meta; 583 + memcpy( meta.magic, kMetaV2Magic, sizeof( kMetaV2Magic ) ); 584 + memset( meta.kdfSalt, 0xAA, kSaltSize ); 585 + 586 + // ACT 587 + uint8_t key1[kAesKeySize], key2[kAesKeySize]; 588 + VaultCrypto::deriveKey( "test", 4, meta.kdfSalt, kSaltSize, key1, kAesKeySize ); 589 + VaultCrypto::deriveKey( "test", 4, meta.kdfSalt, kSaltSize, key2, kAesKeySize ); 590 + 591 + // ASSERT 592 + TEST_ASSERT_EQUAL_MEMORY( key1, key2, kAesKeySize ); 593 + } 594 + 595 + static void test_v2_pin_verifier_is_deterministic() { 596 + // ARRANGE 597 + VaultMetaV2 meta; 598 + memcpy( meta.magic, kMetaV2Magic, sizeof( kMetaV2Magic ) ); 599 + memset( meta.kdfSalt, 0xCC, kSaltSize ); 600 + uint8_t key[kAesKeySize]; 601 + VaultCrypto::deriveKey( "1234", 4, meta.kdfSalt, kSaltSize, key, kAesKeySize ); 602 + 603 + // ACT 604 + uint8_t verifier1[kPinHashSize], verifier2[kPinHashSize]; 605 + VaultCrypto::hmacSha256( key, kAesKeySize, reinterpret_cast<const uint8_t*>( kPinVerifyTag ), 606 + sizeof( kPinVerifyTag ) - 1, verifier1 ); 607 + VaultCrypto::hmacSha256( key, kAesKeySize, reinterpret_cast<const uint8_t*>( kPinVerifyTag ), 608 + sizeof( kPinVerifyTag ) - 1, verifier2 ); 609 + 610 + // ASSERT 611 + TEST_ASSERT_EQUAL_MEMORY( verifier1, verifier2, kPinHashSize ); 612 + } 613 + 614 + // --- Tests: v1 → v2 migration ---------------------------------------------- 615 + // --- Tests ----------------------------------------------------------------- 616 + static void test_v1_to_v2_migration_preserves_key() { 617 + // ARRANGE — v1 encSalt becomes v2 kdfSalt 618 + uint8_t encSalt[kSaltSize]; 619 + memset( encSalt, 0xDD, kSaltSize ); 620 + uint8_t v1Key[kAesKeySize]; 621 + VaultCrypto::deriveKey( "4321", 4, encSalt, kSaltSize, v1Key, kAesKeySize ); 622 + 623 + VaultMetaV2 v2; 624 + memcpy( v2.magic, kMetaV2Magic, sizeof( kMetaV2Magic ) ); 625 + memcpy( v2.kdfSalt, encSalt, kSaltSize ); 626 + VaultCrypto::hmacSha256( v1Key, kAesKeySize, reinterpret_cast<const uint8_t*>( kPinVerifyTag ), 627 + sizeof( kPinVerifyTag ) - 1, v2.pinVerifier ); 628 + 629 + // ACT 630 + uint8_t v2Key[kAesKeySize]; 631 + bool ok = simulateUnlock( "4321", 4, v2, v2Key ); 632 + 633 + // ASSERT 634 + TEST_ASSERT_TRUE( ok ); 635 + TEST_ASSERT_EQUAL_MEMORY( v1Key, v2Key, kAesKeySize ); 636 + } 637 + 638 + // --- Tests: favorites bitfield --------------------------------------------- 639 + // --- Tests ----------------------------------------------------------------- 640 + static void test_favorites_all_off_initially() { 641 + uint8_t favBits[( kMaxCredentials + 7 ) / 8] = {}; 642 + for ( uint8_t i = 0; i < kMaxCredentials; i++ ) 643 + TEST_ASSERT_FALSE( getFavoriteBit( favBits, i ) ); 644 + } 645 + 646 + static void test_favorites_set_and_get() { 647 + uint8_t favBits[( kMaxCredentials + 7 ) / 8] = {}; 648 + setFavoriteBit( favBits, 0, true ); 649 + setFavoriteBit( favBits, 7, true ); 650 + setFavoriteBit( favBits, 49, true ); 651 + 652 + TEST_ASSERT_TRUE( getFavoriteBit( favBits, 0 ) ); 653 + TEST_ASSERT_TRUE( getFavoriteBit( favBits, 7 ) ); 654 + TEST_ASSERT_TRUE( getFavoriteBit( favBits, 49 ) ); 655 + TEST_ASSERT_FALSE( getFavoriteBit( favBits, 1 ) ); 656 + } 657 + 658 + static void test_favorites_clear_single_bit() { 659 + uint8_t favBits[( kMaxCredentials + 7 ) / 8] = {}; 660 + setFavoriteBit( favBits, 5, true ); 661 + setFavoriteBit( favBits, 5, false ); 662 + TEST_ASSERT_FALSE( getFavoriteBit( favBits, 5 ) ); 663 + } 664 + 665 + static void test_favorites_adjacent_bits_independent() { 666 + uint8_t favBits[( kMaxCredentials + 7 ) / 8] = {}; 667 + setFavoriteBit( favBits, 8, true ); 668 + TEST_ASSERT_TRUE( getFavoriteBit( favBits, 8 ) ); 669 + TEST_ASSERT_FALSE( getFavoriteBit( favBits, 7 ) ); 670 + TEST_ASSERT_FALSE( getFavoriteBit( favBits, 9 ) ); 671 + } 672 + 673 + // --- Tests: credential ordering -------------------------------------------- 674 + // --- Tests ----------------------------------------------------------------- 675 + static void test_order_unset_value_is_0xFF() { 676 + uint8_t order[kMaxCredentials]; 677 + memset( order, 0xFF, sizeof( order ) ); 678 + for ( uint8_t i = 0; i < kMaxCredentials; i++ ) 679 + TEST_ASSERT_EQUAL_HEX8( 0xFF, order[i] ); 680 + } 681 + 682 + static void test_order_set_custom_positions() { 683 + uint8_t ids[] = { 5, 2, 0, 10 }; 684 + uint8_t order[kMaxCredentials]; 685 + buildOrderArray( ids, 4, order ); 686 + 687 + TEST_ASSERT_EQUAL( 2, order[0] ); 688 + TEST_ASSERT_EQUAL( 1, order[2] ); 689 + TEST_ASSERT_EQUAL( 0, order[5] ); 690 + TEST_ASSERT_EQUAL( 3, order[10] ); 691 + TEST_ASSERT_EQUAL_HEX8( 0xFF, order[1] ); 692 + } 693 + 694 + static void test_order_empty_list_all_0xFF() { 695 + uint8_t order[kMaxCredentials]; 696 + buildOrderArray( nullptr, 0, order ); 697 + for ( uint8_t i = 0; i < kMaxCredentials; i++ ) 698 + TEST_ASSERT_EQUAL_HEX8( 0xFF, order[i] ); 699 + } 700 + 701 + // --- Tests: credential sorting --------------------------------------------- 702 + // --- Tests ----------------------------------------------------------------- 703 + static void test_sort_favorites_placed_before_non_favorites() { 704 + CredentialEntry entries[3]; 705 + memset( entries, 0, sizeof( entries ) ); 706 + entries[0] = { 0, "Bravo", false, 0xFF }; 707 + entries[1] = { 1, "Alpha", true, 0xFF }; 708 + entries[2] = { 2, "Charlie", false, 0xFF }; 709 + 710 + sortEntries( entries, 3 ); 711 + 712 + TEST_ASSERT_EQUAL( 1, entries[0].id ); 713 + } 714 + 715 + static void test_sort_by_sort_order_within_same_fav_group() { 716 + CredentialEntry entries[3]; 717 + memset( entries, 0, sizeof( entries ) ); 718 + entries[0] = { 0, "C", false, 2 }; 719 + entries[1] = { 1, "A", false, 0 }; 720 + entries[2] = { 2, "B", false, 1 }; 721 + 722 + sortEntries( entries, 3 ); 723 + 724 + TEST_ASSERT_EQUAL( 1, entries[0].id ); 725 + TEST_ASSERT_EQUAL( 2, entries[1].id ); 726 + TEST_ASSERT_EQUAL( 0, entries[2].id ); 727 + } 728 + 729 + static void test_sort_ordered_entries_before_unordered() { 730 + CredentialEntry entries[3]; 731 + memset( entries, 0, sizeof( entries ) ); 732 + entries[0] = { 0, "X", false, 0xFF }; 733 + entries[1] = { 1, "Y", false, 0 }; 734 + entries[2] = { 2, "Z", false, 0xFF }; 735 + 736 + sortEntries( entries, 3 ); 737 + 738 + TEST_ASSERT_EQUAL( 1, entries[0].id ); 739 + } 740 + 741 + static void test_sort_favorites_with_order_before_non_favorites_with_order() { 742 + CredentialEntry entries[4]; 743 + memset( entries, 0, sizeof( entries ) ); 744 + entries[0] = { 3, "D", false, 0xFF }; 745 + entries[1] = { 1, "B", true, 1 }; 746 + entries[2] = { 0, "A", true, 0 }; 747 + entries[3] = { 2, "C", false, 0 }; 748 + 749 + sortEntries( entries, 4 ); 750 + 751 + TEST_ASSERT_EQUAL( 0, entries[0].id ); 752 + TEST_ASSERT_EQUAL( 1, entries[1].id ); 753 + TEST_ASSERT_EQUAL( 2, entries[2].id ); 754 + TEST_ASSERT_EQUAL( 3, entries[3].id ); 755 + } 756 + 757 + // --- Tests: credential wipe / encrypt-decrypt ------------------------------ 758 + // --- Tests ----------------------------------------------------------------- 759 + static void test_credential_wipe_zeros_all_bytes() { 760 + // ARRANGE 761 + Credential c; 762 + strcpy( c.name, "Secret" ); 763 + strcpy( c.pass, "p@ssw0rd!" ); 764 + 765 + // ACT 766 + VaultCrypto::secureWipe( &c, sizeof( c ) ); 767 + 768 + // ASSERT 769 + const uint8_t* bytes = reinterpret_cast<const uint8_t*>( &c ); 770 + bool allZero = true; 771 + for ( size_t i = 0; i < sizeof( c ); i++ ) 772 + if ( bytes[i] != 0 ) { 773 + allZero = false; 774 + break; 775 + } 776 + TEST_ASSERT_TRUE( allZero ); 777 + } 778 + 779 + static void test_encrypt_decrypt_credential_json_roundtrip() { 780 + // ARRANGE 781 + Credential cred; 782 + strcpy( cred.name, "TestSite" ); 783 + strcpy( cred.user, "user@test.com" ); 784 + strcpy( cred.pass, "MyP@ss123!" ); 785 + strcpy( cred.url, "https://test.com" ); 786 + JsonDocument doc; 787 + doc["name"] = cred.name; 788 + doc["user"] = cred.user; 789 + doc["pass"] = cred.pass; 790 + doc["url"] = cred.url; 791 + uint8_t jsonBuf[512]; 792 + size_t jsonLen = serializeJson( doc, reinterpret_cast<char*>( jsonBuf ), sizeof( jsonBuf ) ); 793 + 794 + uint8_t salt[kSaltSize] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16 }; 795 + uint8_t key[kAesKeySize]; 796 + TEST_ASSERT_TRUE( VaultCrypto::deriveKey( "1234", 4, salt, kSaltSize, key, kAesKeySize ) ); 797 + 798 + // ACT 799 + uint8_t iv[kIvSize]; 800 + uint8_t cipherBuf[512 + kAesBlockSize]; 801 + size_t cipherLen = 0; 802 + TEST_ASSERT_TRUE( VaultCrypto::encrypt( key, jsonBuf, jsonLen, iv, cipherBuf, &cipherLen ) ); 803 + 804 + uint8_t plainBuf[512]; 805 + size_t plainLen = 0; 806 + TEST_ASSERT_TRUE( VaultCrypto::decrypt( key, iv, cipherBuf, cipherLen, plainBuf, &plainLen ) ); 807 + 808 + JsonDocument doc2; 809 + DeserializationError err = deserializeJson( doc2, plainBuf, plainLen ); 810 + 811 + // ASSERT 812 + TEST_ASSERT_TRUE( err == DeserializationError::Ok ); 813 + TEST_ASSERT_EQUAL_STRING( "TestSite", doc2["name"] ); 814 + TEST_ASSERT_EQUAL_STRING( "MyP@ss123!", doc2["pass"] ); 815 + } 816 + 817 + // --- Tests: HMAC integrity ------------------------------------------------- 818 + // --- Tests ----------------------------------------------------------------- 819 + static void test_hmac_detects_tampered_data() { 820 + // ARRANGE 821 + uint8_t key[kAesKeySize]; 822 + memset( key, 0x42, kAesKeySize ); 823 + uint8_t data[] = { 1, 2, 3, 4, 5, 6, 7, 8 }; 824 + 825 + // ACT 826 + uint8_t hmac1[kHmacSize]; 827 + TEST_ASSERT_TRUE( VaultCrypto::hmacSha256( key, kAesKeySize, data, sizeof( data ), hmac1 ) ); 828 + data[3] = 99; 829 + uint8_t hmac2[kHmacSize]; 830 + TEST_ASSERT_TRUE( VaultCrypto::hmacSha256( key, kAesKeySize, data, sizeof( data ), hmac2 ) ); 831 + 832 + // ASSERT 833 + TEST_ASSERT_FALSE( VaultCrypto::constantTimeEqual( hmac1, hmac2, kHmacSize ) ); 834 + } 835 + 836 + static void test_hmac_different_keys_produce_different_macs() { 837 + // ARRANGE 838 + uint8_t key1[kAesKeySize], key2[kAesKeySize]; 839 + memset( key1, 0x11, kAesKeySize ); 840 + memset( key2, 0x22, kAesKeySize ); 841 + uint8_t data[] = { 10, 20, 30 }; 842 + 843 + // ACT 844 + uint8_t hmac1[kHmacSize], hmac2[kHmacSize]; 845 + VaultCrypto::hmacSha256( key1, kAesKeySize, data, sizeof( data ), hmac1 ); 846 + VaultCrypto::hmacSha256( key2, kAesKeySize, data, sizeof( data ), hmac2 ); 847 + 848 + // ASSERT 849 + TEST_ASSERT_FALSE( VaultCrypto::constantTimeEqual( hmac1, hmac2, kHmacSize ) ); 850 + } 851 + 852 + // --- Fake function definitions --------------------------------------------- 853 + // --- Function declarations ------------------------------------------------- 854 + // (none) 855 + 856 + // --- Helper definitions ---------------------------------------------------- 857 + // --- State variables ------------------------------------------------------- 858 + static bool simulateProvision( const char* pin, size_t pinLen, VaultMetaV2& meta, 859 + uint8_t* keyOut ) { 860 + memcpy( meta.magic, kMetaV2Magic, sizeof( kMetaV2Magic ) ); 861 + VaultCrypto::generateRandom( meta.kdfSalt, kSaltSize ); 862 + VaultCrypto::generateRandom( meta.hmacSalt, kSaltSize ); 863 + if ( !VaultCrypto::deriveKey( pin, pinLen, meta.kdfSalt, kSaltSize, keyOut, kAesKeySize ) ) 864 + return false; 865 + return VaultCrypto::hmacSha256( keyOut, kAesKeySize, 866 + reinterpret_cast<const uint8_t*>( kPinVerifyTag ), 867 + sizeof( kPinVerifyTag ) - 1, meta.pinVerifier ); 868 + } 869 + 870 + static bool simulateUnlock( const char* pin, size_t pinLen, const VaultMetaV2& meta, 871 + uint8_t* keyOut ) { 872 + if ( !VaultCrypto::deriveKey( pin, pinLen, meta.kdfSalt, kSaltSize, keyOut, kAesKeySize ) ) 873 + return false; 874 + uint8_t computed[kPinHashSize]; 875 + if ( !VaultCrypto::hmacSha256( keyOut, kAesKeySize, 876 + reinterpret_cast<const uint8_t*>( kPinVerifyTag ), 877 + sizeof( kPinVerifyTag ) - 1, computed ) ) 878 + return false; 879 + bool ok = VaultCrypto::constantTimeEqual( computed, meta.pinVerifier, kPinHashSize ); 880 + VaultCrypto::secureWipe( computed, sizeof( computed ) ); 881 + if ( !ok ) 882 + VaultCrypto::secureWipe( keyOut, kAesKeySize ); 883 + return ok; 884 + } 885 + 886 + static void setFavoriteBit( uint8_t* favBits, uint8_t id, bool fav ) { 887 + if ( fav ) 888 + favBits[id / 8] |= static_cast<uint8_t>( 1 << ( id % 8 ) ); 889 + else 890 + favBits[id / 8] &= static_cast<uint8_t>( ~( 1 << ( id % 8 ) ) ); 891 + } 892 + 893 + static bool getFavoriteBit( const uint8_t* favBits, uint8_t id ) { 894 + return ( favBits[id / 8] >> ( id % 8 ) ) & 1; 895 + } 896 + 897 + static void buildOrderArray( const uint8_t* ids, uint8_t count, uint8_t* order ) { 898 + memset( order, 0xFF, kMaxCredentials ); 899 + for ( uint8_t i = 0; i < count && i < kMaxCredentials; i++ ) 900 + if ( ids[i] < kMaxCredentials ) 901 + order[ids[i]] = i; 902 + } 903 + 904 + static void sortEntries( CredentialEntry* entries, uint8_t n ) { 905 + for ( uint8_t i = 1; i < n; i++ ) { 906 + for ( uint8_t j = i; j > 0; j-- ) { 907 + bool swap = false; 908 + if ( entries[j].favorite && !entries[j - 1].favorite ) { 909 + swap = true; 910 + } else if ( entries[j].favorite == entries[j - 1].favorite ) { 911 + if ( entries[j].sortOrder != 0xFF && entries[j - 1].sortOrder == 0xFF ) 912 + swap = true; 913 + else if ( entries[j].sortOrder != 0xFF && entries[j - 1].sortOrder != 0xFF 914 + && entries[j].sortOrder < entries[j - 1].sortOrder ) 915 + swap = true; 916 + } 917 + if ( swap ) { 918 + CredentialEntry tmp = entries[j]; 919 + entries[j] = entries[j - 1]; 920 + entries[j - 1] = tmp; 921 + } else { 922 + break; 923 + } 924 + } 925 + } 926 + } 927 + 928 + // --- main() ---------------------------------------------------------------- 929 + // --- main() ---------------------------------------------------------------- 930 + int main( int /*argc*/, char** /*argv*/ ) { 931 + UNITY_BEGIN(); 932 + 933 + // Credential JSON 934 + RUN_TEST( test_credential_json_serialize_roundtrip ); 935 + RUN_TEST( test_credential_json_max_name_length_preserved ); 936 + RUN_TEST( test_credential_json_empty_fields_roundtrip ); 937 + RUN_TEST( test_credential_json_special_characters_roundtrip ); 938 + RUN_TEST( test_credential_json_unicode_utf8_roundtrip ); 939 + RUN_TEST( test_credential_json_missing_optional_fields_default_empty ); 940 + RUN_TEST( test_credential_json_list_serializes_all_entries ); 941 + RUN_TEST( test_credential_json_invalid_input_returns_error ); 942 + RUN_TEST( test_credential_json_max_credentials_array ); 943 + 944 + // TOTP JSON 945 + RUN_TEST( test_totp_json_6_digit_roundtrip ); 946 + RUN_TEST( test_totp_json_8_digit_period_60 ); 947 + RUN_TEST( test_totp_hex_secret_encode_decode_roundtrip ); 948 + 949 + // CSV parsing 950 + RUN_TEST( test_csv_simple_line_parsed_correctly ); 951 + RUN_TEST( test_csv_url_with_multiple_colons_preserved ); 952 + 953 + // Backup / export JSON 954 + RUN_TEST( test_backup_json_format_roundtrip ); 955 + RUN_TEST( test_export_json_structure_roundtrip ); 956 + RUN_TEST( test_import_rejects_wrong_version ); 957 + RUN_TEST( test_import_rejects_missing_version ); 958 + RUN_TEST( test_import_skips_ids_at_or_above_max ); 959 + RUN_TEST( test_import_empty_credentials_array_accepted ); 960 + 961 + // V2 meta structure 962 + RUN_TEST( test_v2_meta_magic_bytes_correct ); 963 + RUN_TEST( test_v2_meta_size_matches_spec ); 964 + RUN_TEST( test_v1_meta_size_matches_spec ); 965 + RUN_TEST( test_v2_size_is_distinguishable_from_v1 ); 966 + 967 + // V2 provision / unlock 968 + RUN_TEST( test_v2_provision_and_unlock_correct_pin_succeeds ); 969 + RUN_TEST( test_v2_unlock_wrong_pin_fails ); 970 + RUN_TEST( test_v2_unlock_empty_pin_fails ); 971 + RUN_TEST( test_v2_different_pins_produce_different_keys ); 972 + RUN_TEST( test_v2_same_pin_same_salt_deterministic_key ); 973 + RUN_TEST( test_v2_pin_verifier_is_deterministic ); 974 + 975 + // V1 → V2 migration 976 + RUN_TEST( test_v1_to_v2_migration_preserves_key ); 977 + 978 + // Favorites 979 + RUN_TEST( test_favorites_all_off_initially ); 980 + RUN_TEST( test_favorites_set_and_get ); 981 + RUN_TEST( test_favorites_clear_single_bit ); 982 + RUN_TEST( test_favorites_adjacent_bits_independent ); 983 + 984 + // Ordering 985 + RUN_TEST( test_order_unset_value_is_0xFF ); 986 + RUN_TEST( test_order_set_custom_positions ); 987 + RUN_TEST( test_order_empty_list_all_0xFF ); 988 + 989 + // Sorting 990 + RUN_TEST( test_sort_favorites_placed_before_non_favorites ); 991 + RUN_TEST( test_sort_by_sort_order_within_same_fav_group ); 992 + RUN_TEST( test_sort_ordered_entries_before_unordered ); 993 + RUN_TEST( test_sort_favorites_with_order_before_non_favorites_with_order ); 994 + 995 + // Credential wipe / encrypt-decrypt 996 + RUN_TEST( test_credential_wipe_zeros_all_bytes ); 997 + RUN_TEST( test_encrypt_decrypt_credential_json_roundtrip ); 998 + 999 + // HMAC integrity 1000 + RUN_TEST( test_hmac_detects_tampered_data ); 1001 + RUN_TEST( test_hmac_different_keys_produce_different_macs ); 1002 + 1003 + return UNITY_END(); 1004 + }
+137
test/web/test_admin_idle_logic/test_admin_idle_logic.cpp
··· 1 + // Kleidos — Native test: Admin Idle Logic (idle timeout decision logic) 2 + // Build: pio test -e native -f web/test_admin_idle_logic 3 + 4 + // --- File header ----------------------------------------------------------- 5 + 6 + // Group 1: module(s) under test 7 + #include "web/admin_idle_logic.h" 8 + 9 + // Group 3: stdlib + Unity 10 + #include <unity.h> 11 + 12 + // --- Constants ------------------------------------------------------------- 13 + 14 + using web::decideIdleResponse; 15 + using web::IdleResponse; 16 + 17 + static constexpr uint32_t kTimeoutMs = 300000u; // 5 min 18 + static constexpr uint32_t kWarnMs = 30000u; // 30 s 19 + 20 + // --- setUp / tearDown ------------------------------------------------------ 21 + 22 + void setUp() {} 23 + void tearDown() { /* intentionally empty */ } 24 + 25 + // --- Tests ----------------------------------------------------------------- 26 + 27 + // elapsed=0 → full window remains, banner hidden. 28 + static void test_fresh_session_no_banner() { 29 + // ACT 30 + auto r = decideIdleResponse( 0u, kTimeoutMs, kWarnMs ); 31 + 32 + // ASSERT 33 + TEST_ASSERT_EQUAL_UINT32( 300u, r.remainingS ); 34 + TEST_ASSERT_FALSE( r.showBanner ); 35 + } 36 + 37 + // At the warning threshold, banner arms. 38 + static void test_at_warning_threshold_banner_on() { 39 + // ACT 40 + auto r = decideIdleResponse( 270000u, kTimeoutMs, kWarnMs ); 41 + 42 + // ASSERT 43 + TEST_ASSERT_EQUAL_UINT32( 30u, r.remainingS ); 44 + TEST_ASSERT_TRUE( r.showBanner ); 45 + } 46 + 47 + // Inside the warning window. 48 + static void test_inside_warning_window_banner_on() { 49 + // ACT 50 + auto r = decideIdleResponse( 290000u, kTimeoutMs, kWarnMs ); 51 + 52 + // ASSERT 53 + TEST_ASSERT_EQUAL_UINT32( 10u, r.remainingS ); 54 + TEST_ASSERT_TRUE( r.showBanner ); 55 + } 56 + 57 + // Past the timeout: clamp to zero, banner still on (caller will tear down). 58 + static void test_past_timeout_clamped_to_zero() { 59 + // ACT 60 + auto r = decideIdleResponse( 305000u, kTimeoutMs, kWarnMs ); 61 + 62 + // ASSERT 63 + TEST_ASSERT_EQUAL_UINT32( 0u, r.remainingS ); 64 + TEST_ASSERT_TRUE( r.showBanner ); 65 + } 66 + 67 + // Just before the warning window — banner stays hidden. 68 + static void test_just_before_warning_no_banner() { 69 + // ACT 70 + auto r = decideIdleResponse( 269000u, kTimeoutMs, kWarnMs ); 71 + 72 + // ASSERT 73 + TEST_ASSERT_EQUAL_UINT32( 31u, r.remainingS ); 74 + TEST_ASSERT_FALSE( r.showBanner ); 75 + } 76 + 77 + // Degenerate: warn lead larger than timeout — banner is permanently on. 78 + static void test_warn_larger_than_timeout_always_on() { 79 + // ACT 80 + auto r = decideIdleResponse( 0u, 10000u, 60000u ); 81 + 82 + // ASSERT 83 + TEST_ASSERT_EQUAL_UINT32( 10u, r.remainingS ); 84 + TEST_ASSERT_TRUE( r.showBanner ); 85 + } 86 + 87 + // H1: "user keeps the page open but never interacts". 88 + // 89 + // Models the post-H1 reality where a connected station no longer pumps 90 + // `lastClientActivity_` — only authenticated /api/* requests do (and 91 + // /api/idle is read-only). With the JS poll firing every ~10 s on a 92 + // timer, the elapsed counter must walk up to the warning threshold and 93 + // then to the timeout without being reset. 94 + // 95 + // Sequence: 0s, 100s, 200s, 269s, 270s, 290s, 300s, 305s. 96 + static void test_idle_walk_with_no_user_input_emits_banner_and_times_out() { 97 + // ARRANGE 98 + constexpr uint32_t kStep[] = { 0, 100000, 200000, 269000, 270000, 290000, 300000, 305000 }; 99 + constexpr uint32_t kRem[] = { 300, 200, 100, 31, 30, 10, 0, 0 }; 100 + constexpr bool kBanner[] = { false, false, false, false, true, true, true, true }; 101 + 102 + // ACT + ASSERT 103 + for ( size_t i = 0; i < sizeof( kStep ) / sizeof( kStep[0] ); ++i ) { 104 + auto r = decideIdleResponse( kStep[i], kTimeoutMs, kWarnMs ); 105 + TEST_ASSERT_EQUAL_UINT32( kRem[i], r.remainingS ); 106 + if ( kBanner[i] ) { 107 + TEST_ASSERT_TRUE( r.showBanner ); 108 + } else { 109 + TEST_ASSERT_FALSE( r.showBanner ); 110 + } 111 + } 112 + } 113 + 114 + // H1: explicit 270 s threshold ⇒ exactly 30 s remaining + banner armed. 115 + static void test_banner_arms_at_270s_remaining_30() { 116 + // ACT 117 + auto r = decideIdleResponse( 270000u, kTimeoutMs, kWarnMs ); 118 + 119 + // ASSERT 120 + TEST_ASSERT_EQUAL_UINT32( 30u, r.remainingS ); 121 + TEST_ASSERT_TRUE( r.showBanner ); 122 + } 123 + 124 + // --- main() ---------------------------------------------------------------- 125 + 126 + int main( int /*argc*/, char** /*argv*/ ) { 127 + UNITY_BEGIN(); 128 + RUN_TEST( test_fresh_session_no_banner ); 129 + RUN_TEST( test_at_warning_threshold_banner_on ); 130 + RUN_TEST( test_inside_warning_window_banner_on ); 131 + RUN_TEST( test_past_timeout_clamped_to_zero ); 132 + RUN_TEST( test_just_before_warning_no_banner ); 133 + RUN_TEST( test_warn_larger_than_timeout_always_on ); 134 + RUN_TEST( test_idle_walk_with_no_user_input_emits_banner_and_times_out ); 135 + RUN_TEST( test_banner_arms_at_270s_remaining_30 ); 136 + return UNITY_END(); 137 + }