feat(operation): X-Just-Provisioned + X-Claimed handshake

Closes the sell-to-friend gap where a buyer's freshly-reset device
would briefly display the seller's photos before the buyer reached
/setup/{mac} to claim. The firmware had no way to tell the server
"I just got reset" — now it does.

Flow:
  - WiFi-setup completion (handle_connect in main.cpp) writes
    NVS_KEY_JUST_PROVISIONED=1 alongside the SSID/PASS save.
  - Every poll while the flag is set sends X-Just-Provisioned: 1.
  - Server (DeviceImageController, paired commit on the webApp side)
    responds with 204 + X-Interval-Ms when the binding is stale,
    forcing the device to its setup-QR fallback. Once the user
    re-claims via /setup/{mac}, the binding is fresh, and the server
    answers with X-Claimed: 1 alongside whatever response code applies.
  - Firmware clears the NVS flag on seeing X-Claimed: 1 — once
    cleared, the device is back to normal long-stable polling.

Tests:
  - PROV-A: flag set in NVS → header on the request
  - PROV-B: no flag → no header (steady state)
  - PROV-C: response with X-Claimed: 1 → flag cleared
  - PROV-D: response without X-Claimed → flag stays (so the next
    poll keeps signaling "not yet acknowledged")

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-08 16:03:22 -04:00
parent bbd5e84db0
commit a0dc4e0115
5 changed files with 87 additions and 3 deletions
+48
View File
@@ -314,6 +314,50 @@ void test_fw_timer_wake_sends_X_Boot_Reason_timer() {
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-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() {
@@ -428,6 +472,10 @@ int main(int argc, char** argv) {
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_fw12_ap_ssid_from_mac_aabbcc);
RUN_TEST(test_fw13_ap_ssid_from_real_mac);
RUN_TEST(test_fw14_304_skips_epd_sleep);