#include #include #include #include #include // Include mocks first — they shadow system/Arduino headers. // -iquote test/mocks ensures quoted includes from test_main find mocks first. // operation.h uses #ifdef UNIT_TEST to pick epd_mock.h and esp_sleep.h. #include "Arduino.h" #include "WiFi.h" #include "WiFiClientSecure.h" #include "Preferences.h" #include "LittleFS.h" #include "epd_mock.h" #include "esp_sleep.h" #include "HTTPClient.h" #include "SPI.h" #include "WebServer.h" #include "DNSServer.h" #include "qrcode.h" #include "config.h" // Define globals referenced as extern in the mock headers int g_http_get_code; std::map g_http_response_headers; std::map g_http_request_headers; bool g_http_end_called; std::string g_http_body; int g_epd_init_count, g_epd_sleep_count, g_epd_draw_image_count; int g_epd_fill_count, g_epd_fill_last_color, g_epd_draw_setup_count; int g_epd_draw_border_count, g_epd_draw_border_last_color, g_epd_draw_border_last_thickness; uint64_t g_sleep_us; bool g_deep_sleep_started; esp_sleep_wakeup_cause_t g_wakeup_cause; int g_ext0_wakeup_pin; int g_ext0_wakeup_level; // Globals for new mocks int g_show_setup_qr_count; uint32_t g_millis_value; int g_digital_read_value; int g_wifi_status; // Ordering / sequencing globals (shared with Preferences.h and epd_mock.h) int g_call_seq = 0; int g_prefs_putint_seq = -1; int g_epd_draw_seq = -1; // Include the template under test AFTER all mocks are defined. // operation.h with UNIT_TEST defined will include "epd_mock.h" and "esp_sleep.h" // via -iquote test/mocks path (real src/epd.h is never pulled in). #include "../../src/operation.h" // Test fixtures static Preferences prefs; static MockHTTPClient http; void reset_state() { g_http_get_code = 200; g_http_response_headers.clear(); g_http_request_headers.clear(); g_http_end_called = false; g_http_body = "TESTDATA"; g_epd_init_count = g_epd_sleep_count = g_epd_draw_image_count = 0; g_epd_fill_count = g_epd_fill_last_color = g_epd_draw_setup_count = 0; g_epd_draw_border_count = g_epd_draw_border_last_color = g_epd_draw_border_last_thickness = 0; g_sleep_us = 0; g_deep_sleep_started = false; g_wakeup_cause = ESP_SLEEP_WAKEUP_TIMER; // default to timer wake unless a test sets cold g_ext0_wakeup_pin = -1; g_ext0_wakeup_level = -1; g_show_setup_qr_count = 0; g_millis_value = 0; g_digital_read_value = HIGH; // button not pressed by default g_wifi_status = WL_CONNECTED; // connected by default prefs.clear(); LittleFS.files.clear(); http._ended = false; g_call_seq = 0; g_prefs_putint_seq = -1; g_epd_draw_seq = -1; } void setUp() { reset_state(); } void tearDown() {} // FW-01: 200 response — file written, epd_draw called, NVS saved, deep sleep started void test_fw01_200_response_happy_path() { // 30 s — at SLEEP_CLAMP_MIN_MS, well under MAX, so honored as-is g_http_response_headers["X-Image-Id"] = "42"; g_http_response_headers["X-Interval-Ms"] = "30000"; g_http_body = "BINDATA"; normal_operation_impl(String("1C:C3:AB:D1:91:F8"), http, String("https://test/api/device/mac/image"), prefs); TEST_ASSERT_EQUAL(1, g_epd_draw_image_count); TEST_ASSERT_EQUAL(42, prefs.getInt("img_id", -1)); TEST_ASSERT_EQUAL_UINT64(30000ULL * 1000ULL, g_sleep_us); TEST_ASSERT_TRUE(g_deep_sleep_started); TEST_ASSERT_FALSE(LittleFS.files[IMAGE_PATH].empty()); } // FW-02: REGRESSION — headers must be read BEFORE http.end(), otherwise newId is empty void test_fw02_headers_read_before_end_regression() { g_http_response_headers["X-Image-Id"] = "99"; normal_operation_impl(String("mac"), http, String("url"), prefs); // If newId was read after end(), NVS img_id would remain -1 TEST_ASSERT_EQUAL(99, prefs.getInt("img_id", -1)); } // FW-03: 304 — no epd draw, no init, deep sleep started void test_fw03_304_no_redraw() { g_http_get_code = 304; // 30 s — at SLEEP_CLAMP_MIN_MS, well under MAX, so honored as-is g_http_response_headers["X-Interval-Ms"] = "30000"; // 304 only ever happens when the device already holds the image, so // pre-set img_id to match what the server would have served. Without // this the FIRST_IMAGE_POLL bootstrap override would (correctly) // shorten sleep to 15 s — and that's not what this test exercises. prefs.ints[NVS_KEY_IMG_ID] = 1; normal_operation_impl(String("mac"), http, String("url"), prefs); TEST_ASSERT_EQUAL(0, g_epd_init_count); TEST_ASSERT_EQUAL(0, g_epd_draw_image_count); TEST_ASSERT_TRUE(g_deep_sleep_started); TEST_ASSERT_EQUAL_UINT64(30000ULL * 1000ULL, g_sleep_us); } // FW-04: 204 — show_setup_qr called exactly once void test_fw04_204_shows_setup_qr() { g_http_get_code = 204; normal_operation_impl(String("mac"), http, String("url"), prefs); TEST_ASSERT_EQUAL(1, g_show_setup_qr_count); TEST_ASSERT_EQUAL(0, g_epd_draw_image_count); } // FW-05: 404 — show_setup_qr called exactly once void test_fw05_404_shows_setup_qr() { g_http_get_code = 404; normal_operation_impl(String("mac"), http, String("url"), prefs); TEST_ASSERT_EQUAL(1, g_show_setup_qr_count); TEST_ASSERT_EQUAL(0, g_epd_draw_image_count); } // FW-06a: 5xx error WITH a cached image → preserve last image and overlay a // yellow BORDER (per FR38). MUST NOT fill the screen with yellow — that would // destroy the last good image. Sets the err_border NVS flag so the next // healthy response repaints clean. void test_fw06a_error_with_cache_draws_border_not_fill() { g_http_get_code = 500; LittleFS.files[IMAGE_PATH] = "IMGDATA"; normal_operation_impl(String("mac"), http, String("url"), prefs); TEST_ASSERT_EQUAL_MESSAGE(0, g_epd_fill_count, "epd_fill must NOT be called when a cached image exists — it would obliterate the photo"); TEST_ASSERT_EQUAL(1, g_epd_draw_border_count); TEST_ASSERT_EQUAL(COLOR_YELLOW, g_epd_draw_border_last_color); TEST_ASSERT_EQUAL(BORDER_THICKNESS_PX, g_epd_draw_border_last_thickness); TEST_ASSERT_EQUAL(1, prefs.getInt(NVS_KEY_ERR_BORDER, -1)); } // FW-06b: 5xx error with NO cached image → fall back to full yellow fill so // the user still sees a sync-fail signal on a fresh device. void test_fw06b_error_without_cache_falls_back_to_fill() { g_http_get_code = 500; // LittleFS has no IMAGE_PATH entry normal_operation_impl(String("mac"), http, String("url"), prefs); TEST_ASSERT_EQUAL(0, g_epd_draw_border_count); TEST_ASSERT_EQUAL(1, g_epd_fill_count); TEST_ASSERT_EQUAL(COLOR_YELLOW, g_epd_fill_last_color); TEST_ASSERT_EQUAL(1, prefs.getInt(NVS_KEY_ERR_BORDER, -1)); } // FW-06c: 304 with err_border flag set (sync recovered after a previous // failure) → repaint the cached image clean and clear the flag. void test_fw06c_304_after_error_repaints_clean() { g_http_get_code = 304; prefs.ints[NVS_KEY_ERR_BORDER] = 1; prefs.ints[NVS_KEY_IMG_ID] = 7; LittleFS.files[IMAGE_PATH] = "IMGDATA"; normal_operation_impl(String("mac"), http, String("url"), prefs); TEST_ASSERT_EQUAL(1, g_epd_init_count); TEST_ASSERT_EQUAL(1, g_epd_draw_image_count); TEST_ASSERT_EQUAL_MESSAGE(0, g_epd_draw_border_count, "304 with err flag must redraw clean — no border on the recovery frame"); TEST_ASSERT_EQUAL(0, prefs.getInt(NVS_KEY_ERR_BORDER, -1)); } // FW-06d: 304 (same image) with NO error or pending state must NOT touch the // display. Specifically must not invoke any yellow path. Locks down the // regression the user reported: 304 was suspected of triggering yellow fill. void test_fw06d_304_steady_state_does_not_fill_yellow() { g_http_get_code = 304; prefs.ints[NVS_KEY_IMG_ID] = 7; prefs.ints[NVS_KEY_SCHEMA_V] = NVS_SCHEMA_VERSION; // post-migration steady state LittleFS.files[IMAGE_PATH] = "IMGDATA"; // err_border = 0, draw_needed = 0 (defaults) normal_operation_impl(String("mac"), http, String("url"), prefs); TEST_ASSERT_EQUAL(0, g_epd_fill_count); TEST_ASSERT_EQUAL(0, g_epd_draw_border_count); TEST_ASSERT_EQUAL(0, g_epd_draw_image_count); TEST_ASSERT_EQUAL(0, g_epd_init_count); } // FW-06e: 200 response after a previous error border → fresh image fully // overwrites the framebuffer, err_border flag cleared. void test_fw06e_200_after_error_clears_flag() { g_http_response_headers["X-Image-Id"] = "8"; g_http_body = "BINDATA"; prefs.ints[NVS_KEY_ERR_BORDER] = 1; normal_operation_impl(String("mac"), http, String("url"), prefs); TEST_ASSERT_EQUAL(1, g_epd_draw_image_count); TEST_ASSERT_EQUAL(0, prefs.getInt(NVS_KEY_ERR_BORDER, -1)); } // FW-06f: First boot of err-border-aware firmware (NVS schema_v missing) on a // device that may be displaying a stale full-screen yellow from the previous // buggy build → migration forces a clean redraw on the next 304 and bumps // schema_v so it doesn't fire again. Locks in the upgrade-path recovery. void test_fw06f_schema_migration_forces_redraw_on_first_boot() { g_http_get_code = 304; prefs.ints[NVS_KEY_IMG_ID] = 3; LittleFS.files[IMAGE_PATH] = "IMGDATA"; // No NVS_KEY_SCHEMA_V set → defaults to 0, triggers migration normal_operation_impl(String("mac"), http, String("url"), prefs); TEST_ASSERT_EQUAL(1, g_epd_init_count); TEST_ASSERT_EQUAL(1, g_epd_draw_image_count); TEST_ASSERT_EQUAL(NVS_SCHEMA_VERSION, prefs.getInt(NVS_KEY_SCHEMA_V, -1)); TEST_ASSERT_EQUAL(0, prefs.getInt(NVS_KEY_ERR_BORDER, -1)); } // FW-06g: Second boot under same firmware → migration does NOT fire again, // 304 is a no-op as in the steady-state case. void test_fw06g_schema_migration_does_not_fire_again() { g_http_get_code = 304; prefs.ints[NVS_KEY_IMG_ID] = 3; prefs.ints[NVS_KEY_SCHEMA_V] = NVS_SCHEMA_VERSION; LittleFS.files[IMAGE_PATH] = "IMGDATA"; normal_operation_impl(String("mac"), http, String("url"), prefs); TEST_ASSERT_EQUAL(0, g_epd_init_count); TEST_ASSERT_EQUAL(0, g_epd_draw_image_count); TEST_ASSERT_EQUAL(0, g_epd_fill_count); } // FW-07: NVS has saved img_id → X-Current-Image-Id header sent void test_fw07_current_image_id_sent_when_saved() { prefs.ints["img_id"] = 99; g_http_response_headers["X-Image-Id"] = "99"; normal_operation_impl(String("mac"), http, String("url"), prefs); TEST_ASSERT_EQUAL_STRING("99", g_http_request_headers["X-Current-Image-Id"].c_str()); } // FW-08: NVS img_id = -1 (default) → X-Current-Image-Id NOT sent void test_fw08_no_current_image_id_when_default() { // prefs has no img_id — getInt returns -1 normal_operation_impl(String("mac"), http, String("url"), prefs); TEST_ASSERT_TRUE(g_http_request_headers.find("X-Current-Image-Id") == g_http_request_headers.end()); } // FW-09: server interval within clamp range → exact server value used. // 5 min sits well between SLEEP_CLAMP_MIN_MS and SLEEP_CLAMP_MAX_MS. void test_fw09_server_interval_honored() { g_http_response_headers["X-Interval-Ms"] = "300000"; // 5 min g_http_response_headers["X-Image-Id"] = "1"; normal_operation_impl(String("mac"), http, String("url"), prefs); TEST_ASSERT_EQUAL_UINT64(300000ULL * 1000ULL, g_sleep_us); } // FW-10: server interval > SLEEP_CLAMP_MAX_MS → clamped at MAX. // Protects against a misconfigured "every 999 days" stranding the device. void test_fw10_server_interval_clamped_to_max() { g_http_response_headers["X-Interval-Ms"] = "999999999"; g_http_response_headers["X-Image-Id"] = "1"; normal_operation_impl(String("mac"), http, String("url"), prefs); TEST_ASSERT_EQUAL_UINT64(SLEEP_CLAMP_MAX_MS * 1000ULL, g_sleep_us); } // FW-10b: server interval < SLEEP_CLAMP_MIN_MS → clamped UP to MIN. // Protects the battery against a runaway poll if the server sends 1000 ms // (or anything malformed-but-positive that survives strtoull). void test_fw10b_server_interval_clamped_to_min() { g_http_response_headers["X-Interval-Ms"] = "1000"; g_http_response_headers["X-Image-Id"] = "1"; normal_operation_impl(String("mac"), http, String("url"), prefs); TEST_ASSERT_EQUAL_UINT64(SLEEP_CLAMP_MIN_MS * 1000ULL, g_sleep_us); } // FW-FORCE-RESYNC: a wakeup cause of UNDEFINED (cold boot, hard reset, // power-cycle) MUST surface to the server as X-Boot-Reason: cold so that // the server can force a rotation regardless of the wakeTimes schedule. // This is what makes "unplug → replug" a manual refresh feature for users. void test_fw_cold_boot_sends_X_Boot_Reason_cold() { g_wakeup_cause = ESP_SLEEP_WAKEUP_UNDEFINED; g_http_response_headers["X-Image-Id"] = "1"; normal_operation_impl(String("mac"), http, String("url"), prefs); TEST_ASSERT_EQUAL_STRING("cold", g_http_request_headers["X-Boot-Reason"].c_str()); } // Inverse of FW-FORCE-RESYNC: scheduled timer wakeups must not pretend to be // power-cycles, or every poll would force a rotation and the schedule gating // would be useless. void test_fw_timer_wake_sends_X_Boot_Reason_timer() { g_wakeup_cause = ESP_SLEEP_WAKEUP_TIMER; g_http_response_headers["X-Image-Id"] = "1"; normal_operation_impl(String("mac"), http, String("url"), prefs); TEST_ASSERT_EQUAL_STRING("timer", g_http_request_headers["X-Boot-Reason"].c_str()); } // FW-PROV-A: just-provisioned NVS flag set → header sent on the poll. Without // this header the server can't tell a freshly-reset device from a cached // one, and would happily serve the prior owner's photo to a buyer. void test_fw_just_provisioned_flag_sets_header() { prefs.ints[NVS_KEY_JUST_PROVISIONED] = 1; g_http_response_headers["X-Image-Id"] = "1"; normal_operation_impl(String("mac"), http, String("url"), prefs); TEST_ASSERT_EQUAL_STRING("1", g_http_request_headers["X-Just-Provisioned"].c_str()); } // FW-PROV-B: no flag → no header. Steady-state polls must not look like // fresh provisioning, otherwise every reboot would force the awaiting- // claim gate. void test_fw_no_flag_means_no_header() { // prefs.ints[NVS_KEY_JUST_PROVISIONED] is unset, default 0. g_http_response_headers["X-Image-Id"] = "1"; normal_operation_impl(String("mac"), http, String("url"), prefs); TEST_ASSERT_TRUE( g_http_request_headers.find("X-Just-Provisioned") == g_http_request_headers.end() ); } // FW-PROV-C: server returns X-Claimed: 1 → flag clears in NVS. From then // on the firmware polls without the just-provisioned signal, just like a // long-stable device. void test_fw_X_Claimed_response_clears_flag() { prefs.ints[NVS_KEY_JUST_PROVISIONED] = 1; g_http_response_headers["X-Image-Id"] = "1"; g_http_response_headers["X-Claimed"] = "1"; normal_operation_impl(String("mac"), http, String("url"), prefs); TEST_ASSERT_EQUAL(0, prefs.getInt(NVS_KEY_JUST_PROVISIONED, -1)); } // FW-RESET-WAKE: every deep-sleep cycle must arm EXT0 wakeup on the BOOT // button, otherwise holding it during sleep does nothing and the user- // facing 5-second-hold reset only works during the brief awake window. // Reported live by Matt 2026-05-09: held BOOT, device didn't reset. void test_fw_deep_sleep_arms_ext0_button_wakeup() { g_http_response_headers["X-Image-Id"] = "1"; normal_operation_impl(String("mac"), http, String("url"), prefs); TEST_ASSERT_EQUAL(PIN_BTN_RESET, g_ext0_wakeup_pin); TEST_ASSERT_EQUAL(0, g_ext0_wakeup_level); // active-low; BOOT is pulled-up } // FW-FIRST-IMG-A: device has never received an image (img_id = -1) AND the // poll didn't deliver one (e.g. 204 because no images approved yet). Sleep // must be the 15s bootstrap interval, NOT whatever the server's schedule // says. Without this, a fresh device on a noon-daily schedule sits dark // for up to 24 h before the first photo lands. void test_fw_first_image_bootstrap_polls_at_15s_when_no_image_yet() { g_http_get_code = 204; // Server tries to set a 6-hour interval for the user's noon-daily schedule. g_http_response_headers["X-Interval-Ms"] = String((unsigned long)(6ULL * 60 * 60 * 1000)).c_str(); // No img_id in NVS → device has never seen an image. normal_operation_impl(String("mac"), http, String("url"), prefs); TEST_ASSERT_EQUAL_UINT64(FIRST_IMAGE_POLL_INTERVAL_MS * 1000ULL, g_sleep_us); } // FW-FIRST-IMG-B: once we've persisted an image (200 path wrote img_id), the // server's schedule wins again. Without this assertion the override would // trap the device in 15s polling forever and burn the battery. void test_fw_first_image_bootstrap_clears_after_200() { // Pre-set NVS as if we'd received an image previously. prefs.ints[NVS_KEY_IMG_ID] = 42; g_http_get_code = 200; g_http_response_headers["X-Image-Id"] = "42"; g_http_response_headers["X-Interval-Ms"] = "300000"; // 5 min (well above 15s) g_http_body = "BINDATA"; normal_operation_impl(String("mac"), http, String("url"), prefs); TEST_ASSERT_EQUAL_UINT64(300000ULL * 1000ULL, g_sleep_us); } // FW-FIRST-IMG-C: receiving the FIRST image (200 with previously -1 img_id) // must let the server's schedule take over starting from this very poll — // no extra "one more 15s sleep" cycle. void test_fw_first_image_just_arrived_uses_server_interval() { // img_id starts at -1 (default, no prior image). g_http_get_code = 200; g_http_response_headers["X-Image-Id"] = "1"; g_http_response_headers["X-Interval-Ms"] = "300000"; g_http_body = "BINDATA"; normal_operation_impl(String("mac"), http, String("url"), prefs); // The 200 path persists img_id=1 BEFORE sleep is computed. TEST_ASSERT_EQUAL_UINT64(300000ULL * 1000ULL, g_sleep_us); } // FW-PROV-D: server omits X-Claimed (e.g. stale-binding 204) → flag stays // set so the device keeps signaling "I'm freshly provisioned" until a // later poll lands on a fresh-binding response. void test_fw_no_X_Claimed_response_keeps_flag() { prefs.ints[NVS_KEY_JUST_PROVISIONED] = 1; g_http_get_code = 204; // No X-Claimed header set. normal_operation_impl(String("mac"), http, String("url"), prefs); TEST_ASSERT_EQUAL(1, prefs.getInt(NVS_KEY_JUST_PROVISIONED, -1)); } // FW-11: no X-Interval-Ms → fallback used. Should not happen in normal // operation but guards against rolling deploys / hand-crafted responses. void test_fw11_fallback_used_when_header_absent() { g_http_response_headers["X-Image-Id"] = "1"; // no X-Interval-Ms set normal_operation_impl(String("mac"), http, String("url"), prefs); TEST_ASSERT_EQUAL_UINT64(FETCH_INTERVAL_MS_FALLBACK * 1000ULL, g_sleep_us); } // FW-14: 304 — epd_sleep NOT called (display already in hardware deep sleep) void test_fw14_304_skips_epd_sleep() { g_http_get_code = 304; normal_operation_impl(String("mac"), http, String("url"), prefs); TEST_ASSERT_EQUAL(0, g_epd_sleep_count); TEST_ASSERT_EQUAL(0, g_epd_init_count); } // FW-15: 200 — NVS img_id saved BEFORE epd_draw_image_from_file; draw_needed cleared after void test_fw15_nvs_saved_before_epd_draw_and_flag_cleared() { g_http_response_headers["X-Image-Id"] = "42"; g_http_body = "BINDATA"; normal_operation_impl(String("mac"), http, String("url"), prefs); TEST_ASSERT_TRUE_MESSAGE(g_prefs_putint_seq < g_epd_draw_seq, "NVS putInt must be called before epd_draw_image_from_file"); TEST_ASSERT_EQUAL(42, prefs.getInt("img_id", -1)); TEST_ASSERT_EQUAL(1, g_epd_draw_image_count); TEST_ASSERT_EQUAL(0, prefs.getInt("draw", -1)); } // FW-16: 304 with draw_needed=1 (interrupted draw) — re-draws from LittleFS and clears flag void test_fw16_304_with_draw_needed_redraws() { prefs.ints["img_id"] = 42; prefs.ints["draw"] = 1; g_http_get_code = 304; LittleFS.files[IMAGE_PATH] = "IMGDATA"; normal_operation_impl(String("mac"), http, String("url"), prefs); TEST_ASSERT_EQUAL(1, g_epd_init_count); TEST_ASSERT_EQUAL(1, g_epd_draw_image_count); TEST_ASSERT_EQUAL(1, g_epd_sleep_count); TEST_ASSERT_EQUAL(0, prefs.getInt("draw", -1)); } // FW-17a: 200 response with mismatched X-Image-Sha256 — the bytes were // corrupted in transit. Don't paint the panel; don't update NVS_KEY_IMG_ID // (so the next poll re-fetches); raise err_border so the user sees a sync // issue instead of garbage on the panel. void test_fw17a_sha256_mismatch_skips_draw_and_keeps_old_img_id() { g_http_response_headers["X-Image-Id"] = "42"; g_http_response_headers["X-Image-Sha256"] = "deadbeefcafebabedeadbeefcafebabedeadbeefcafebabedeadbeefcafebabe"; g_http_body = "BINDATA"; prefs.ints[NVS_KEY_IMG_ID] = 7; // pre-existing image we want to keep normal_operation_impl(String("mac"), http, String("url"), prefs); // Panel must not be touched — the corrupt bytes never reach hardware. TEST_ASSERT_EQUAL(0, g_epd_init_count); TEST_ASSERT_EQUAL(0, g_epd_draw_image_count); // NVS image-id stays at the prior value — next poll mismatches the // server's currentImage, server sends 200 again, device retries. TEST_ASSERT_EQUAL(7, prefs.getInt(NVS_KEY_IMG_ID, -1)); // err_border raised so the next healthy 304 will repaint clean. TEST_ASSERT_EQUAL(1, prefs.getInt(NVS_KEY_ERR_BORDER, -1)); } // FW-17b: 200 response without an X-Image-Sha256 header (e.g., older server) // — verification is skipped, the success path runs as it always did. Backward- // compat guarantee: the firmware still works against pre-checksum servers. void test_fw17b_missing_sha256_header_skips_verification() { g_http_response_headers["X-Image-Id"] = "42"; // No X-Image-Sha256 set g_http_body = "BINDATA"; normal_operation_impl(String("mac"), http, String("url"), prefs); TEST_ASSERT_EQUAL(1, g_epd_draw_image_count); TEST_ASSERT_EQUAL(42, prefs.getInt(NVS_KEY_IMG_ID, -1)); TEST_ASSERT_EQUAL(0, prefs.getInt(NVS_KEY_ERR_BORDER, -1)); } // FW-12/13: AP SSID derivation via ap_ssid_from_mac() void test_fw12_ap_ssid_from_mac_aabbcc() { String ssid = ap_ssid_from_mac(String("AA:BB:CC:DD:EE:FF")); TEST_ASSERT_EQUAL_STRING("PictureFrame-EEFF", ssid.c_str()); } void test_fw13_ap_ssid_from_real_mac() { String ssid = ap_ssid_from_mac(String("1C:C3:AB:D1:91:F8")); TEST_ASSERT_EQUAL_STRING("PictureFrame-91F8", ssid.c_str()); } int main(int argc, char** argv) { UNITY_BEGIN(); RUN_TEST(test_fw01_200_response_happy_path); RUN_TEST(test_fw02_headers_read_before_end_regression); RUN_TEST(test_fw03_304_no_redraw); RUN_TEST(test_fw04_204_shows_setup_qr); RUN_TEST(test_fw05_404_shows_setup_qr); RUN_TEST(test_fw06a_error_with_cache_draws_border_not_fill); RUN_TEST(test_fw06b_error_without_cache_falls_back_to_fill); RUN_TEST(test_fw06c_304_after_error_repaints_clean); RUN_TEST(test_fw06d_304_steady_state_does_not_fill_yellow); RUN_TEST(test_fw06e_200_after_error_clears_flag); RUN_TEST(test_fw06f_schema_migration_forces_redraw_on_first_boot); RUN_TEST(test_fw06g_schema_migration_does_not_fire_again); RUN_TEST(test_fw07_current_image_id_sent_when_saved); RUN_TEST(test_fw08_no_current_image_id_when_default); RUN_TEST(test_fw09_server_interval_honored); RUN_TEST(test_fw10_server_interval_clamped_to_max); RUN_TEST(test_fw10b_server_interval_clamped_to_min); RUN_TEST(test_fw11_fallback_used_when_header_absent); RUN_TEST(test_fw_cold_boot_sends_X_Boot_Reason_cold); RUN_TEST(test_fw_timer_wake_sends_X_Boot_Reason_timer); RUN_TEST(test_fw_just_provisioned_flag_sets_header); RUN_TEST(test_fw_no_flag_means_no_header); RUN_TEST(test_fw_X_Claimed_response_clears_flag); RUN_TEST(test_fw_no_X_Claimed_response_keeps_flag); RUN_TEST(test_fw_first_image_bootstrap_polls_at_15s_when_no_image_yet); RUN_TEST(test_fw_first_image_bootstrap_clears_after_200); RUN_TEST(test_fw_first_image_just_arrived_uses_server_interval); RUN_TEST(test_fw_deep_sleep_arms_ext0_button_wakeup); RUN_TEST(test_fw12_ap_ssid_from_mac_aabbcc); RUN_TEST(test_fw13_ap_ssid_from_real_mac); RUN_TEST(test_fw14_304_skips_epd_sleep); RUN_TEST(test_fw15_nvs_saved_before_epd_draw_and_flag_cleared); RUN_TEST(test_fw16_304_with_draw_needed_redraws); RUN_TEST(test_fw17a_sha256_mismatch_skips_draw_and_keeps_old_img_id); RUN_TEST(test_fw17b_missing_sha256_header_skips_verification); return UNITY_END(); }