feat(devices): X-Just-Provisioned gate so reset devices can't leak prior owner's photos
CI / test (push) Has been cancelled
CI / test (push) Has been cancelled
Pairs with the firmware change of the same scope. Server-side contract:
- X-Just-Provisioned: 1 + binding older than 5 min → 204 (the device
paints its setup QR) regardless of any approved images on the
seller's account. No image content leaks.
- X-Just-Provisioned: 1 + binding fresher than 5 min → normal
response (200/304/204), with X-Claimed: 1 stamped so the firmware
clears its NVS flag and returns to standard operation.
- No header → no special behavior. Long-stable devices unaffected.
This is what makes the BOOT-5s reset actually safe to use as the
"factory reset" gesture: now it forces a real re-claim cycle, instead
of silently inheriting the prior owner's content because the MAC is
still bound on the server.
3 functional tests: stale-binding 204, fresh-binding 200 + X-Claimed,
and absence-of-header preserves legacy behavior.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -478,6 +478,63 @@ class DeviceImageControllerTest extends AppWebTestCase
|
||||
$this->assertResponseStatusCodeSame(304);
|
||||
}
|
||||
|
||||
// ── X-Just-Provisioned gate ──────────────────────────────────────────
|
||||
//
|
||||
// The device sets a "just provisioned" flag in NVS at WiFi-setup time
|
||||
// and sends X-Just-Provisioned: 1 on every poll until it sees X-Claimed
|
||||
// back. The server's contract:
|
||||
//
|
||||
// - Header set + binding is stale → 204 (force the device to its
|
||||
// setup-QR screen instead of leaking the prior owner's photo).
|
||||
// - Header set + binding is fresh → normal response + X-Claimed: 1
|
||||
// so the firmware clears the flag and resumes regular operation.
|
||||
// - Header absent → no special behavior (legacy + steady-state polls).
|
||||
|
||||
public function test_just_provisioned_with_stale_binding_returns_204(): void
|
||||
{
|
||||
$setup = $this->createTestSetup();
|
||||
$device = $setup['device'];
|
||||
|
||||
// Backdate linkedAt to look stale (older than the FRESH_CLAIM window).
|
||||
$ref = new \ReflectionProperty(\App\Entity\Device::class, 'linkedAt');
|
||||
$ref->setAccessible(true);
|
||||
$ref->setValue($device, new \DateTimeImmutable('-1 hour'));
|
||||
$this->em()->flush();
|
||||
|
||||
$this->client->request('GET', '/api/device/' . self::MAC . '/image', [], [], [
|
||||
'HTTP_X_Just_Provisioned' => '1',
|
||||
]);
|
||||
|
||||
$this->assertResponseStatusCodeSame(204, 'stale binding + just-provisioned must NOT serve the prior owner\'s image');
|
||||
$this->assertFalse(
|
||||
$this->client->getResponse()->headers->has('X-Claimed'),
|
||||
'X-Claimed must not be sent until the binding is fresh',
|
||||
);
|
||||
}
|
||||
|
||||
public function test_just_provisioned_with_fresh_binding_serves_with_X_Claimed(): void
|
||||
{
|
||||
$setup = $this->createTestSetup();
|
||||
// createTestSetup() doesn't backdate linkedAt — it's now-ish, well
|
||||
// within the FRESH_CLAIM window — so this exercises the post-claim
|
||||
// transition branch.
|
||||
|
||||
$this->client->request('GET', '/api/device/' . self::MAC . '/image', [], [], [
|
||||
'HTTP_X_Just_Provisioned' => '1',
|
||||
]);
|
||||
|
||||
$this->assertResponseIsSuccessful();
|
||||
$this->assertSame('1', $this->client->getResponse()->headers->get('X-Claimed'));
|
||||
}
|
||||
|
||||
public function test_no_just_provisioned_header_means_no_X_Claimed_response_header(): void
|
||||
{
|
||||
$this->createTestSetup();
|
||||
$this->client->request('GET', '/api/device/' . self::MAC . '/image');
|
||||
$this->assertResponseIsSuccessful();
|
||||
$this->assertFalse($this->client->getResponse()->headers->has('X-Claimed'));
|
||||
}
|
||||
|
||||
// Returns 204 when RenderedAsset has Ready status but filePath is null (device.poll.no_asset path)
|
||||
public function test_returns_204_when_ready_asset_has_null_file_path(): void
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user