e4f811581a
CI / test (push) Has been cancelled
Three coordinated UX changes touching defaults and the settings sheet.
1. Server defaults: DeviceService::linkToUser now sets timezone =
user.timezone and wakeTimes = [12*60] (noon-daily) when creating a
new Device row OR transferring ownership on takeover. Replaces the
prior "1440-min interval anchored to last-seen-time" default that
could land a recipient's first photo at 3 am.
2. PWA propagation note: now mentions "briefly disconnect and reconnect
the frame's power" as the immediate-refresh gesture. Pairs with the
existing X-Boot-Reason: cold force-resync — the firmware already
honors a power-cycle as a deliberate refresh request, but users had
no way to discover that.
3. Remove-this-frame: replaced the native window.confirm() with an
in-sheet confirmation panel showing the explanatory text. Inline
keeps the gesture inside the existing sheet flow and gives the
destructive button a fixed location, instead of a floating native
dialog that varies per browser. The confirm body explicitly says
"this can't be undone" to match the irreversibility.
Tests:
- DeviceServiceTest: new-device default, takeover-resets-with-default,
UTC fallback when user has empty timezone.
- SetupControllerTest: claim-takes-over-defaults updated to assert
[12*60] wakeTimes.
- HomeView.test: 4 cases covering open-confirm, yes-confirm, cancel,
propagation-note text.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
169 lines
6.3 KiB
PHP
169 lines
6.3 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Tests\Unit\Service;
|
|
|
|
use App\Entity\Device;
|
|
use App\Entity\DeviceImageHistory;
|
|
use App\Entity\Image;
|
|
use App\Entity\RenderedAsset;
|
|
use App\Enum\DeviceModel;
|
|
use App\Enum\Orientation;
|
|
use App\Enum\RenderStatus;
|
|
use App\Service\DeviceService;
|
|
use App\Tests\AppKernelTestCase;
|
|
|
|
class DeviceServiceTest extends AppKernelTestCase
|
|
{
|
|
private DeviceService $service;
|
|
|
|
protected function setUp(): void
|
|
{
|
|
parent::setUp();
|
|
$this->service = static::getContainer()->get(DeviceService::class);
|
|
}
|
|
|
|
public function test_link_creates_new_device_with_correct_mac_and_user(): void
|
|
{
|
|
$user = $this->createUser('link1@example.com');
|
|
$device = $this->service->linkToUser('aa:bb:cc:dd:ee:01', $user);
|
|
|
|
$this->assertNotNull($device->getId());
|
|
$this->assertSame('AA:BB:CC:DD:EE:01', $device->getMac());
|
|
$this->assertSame($user->getId(), $device->getUser()->getId());
|
|
}
|
|
|
|
public function test_link_is_idempotent_for_same_user(): void
|
|
{
|
|
$user = $this->createUser('link2@example.com');
|
|
|
|
$d1 = $this->service->linkToUser('aa:bb:cc:dd:ee:02', $user);
|
|
$d2 = $this->service->linkToUser('aa:bb:cc:dd:ee:02', $user);
|
|
|
|
$this->assertSame($d1->getId(), $d2->getId());
|
|
$this->assertSame($user->getId(), $d2->getUser()->getId());
|
|
}
|
|
|
|
public function test_link_purges_history_on_ownership_transfer(): void
|
|
{
|
|
$user1 = $this->createUser('owner1@example.com');
|
|
$user2 = $this->createUser('owner2@example.com');
|
|
|
|
// Create device owned by user1 with an image and history
|
|
$device = $this->service->linkToUser('aa:bb:cc:dd:ee:03', $user1);
|
|
|
|
$image = (new Image())->setUser($user1)->setOriginalFilename('x.jpg')->setStoragePath('x');
|
|
$image->approveForDevice($device);
|
|
$this->em()->persist($image);
|
|
|
|
$history = new DeviceImageHistory($device, $image);
|
|
$this->em()->persist($history);
|
|
$this->em()->flush();
|
|
|
|
// Transfer to user2 — must explicitly opt in via allowClaim now,
|
|
// otherwise the service throws DeviceClaimRequiredException.
|
|
$this->service->linkToUser('aa:bb:cc:dd:ee:03', $user2, allowClaim: true);
|
|
|
|
// History should be gone
|
|
$count = $this->em()->createQueryBuilder()
|
|
->select('COUNT(h.id)')
|
|
->from(DeviceImageHistory::class, 'h')
|
|
->where('h.device = :device')
|
|
->setParameter('device', $device)
|
|
->getQuery()
|
|
->getSingleScalarResult();
|
|
|
|
$this->assertSame(0, (int) $count);
|
|
|
|
// Approval should be revoked
|
|
$this->em()->refresh($image);
|
|
$this->assertFalse($image->isApprovedForDevice($device));
|
|
}
|
|
|
|
public function test_link_throws_DeviceClaimRequiredException_without_consent(): void
|
|
{
|
|
$user1 = $this->createUser('claimreq1@example.com');
|
|
$user2 = $this->createUser('claimreq2@example.com');
|
|
|
|
$device = new Device();
|
|
$device->setMac('AA:BB:CC:DD:EE:04');
|
|
$device->setName('Old');
|
|
$device->setUser($user1);
|
|
$this->em()->persist($device);
|
|
$this->em()->flush();
|
|
|
|
$this->expectException(\App\Service\DeviceClaimRequiredException::class);
|
|
$this->service->linkToUser('AA:BB:CC:DD:EE:04', $user2);
|
|
}
|
|
|
|
public function test_isClaimedByAnotherUser_returns_true_only_when_owned_by_someone_else(): void
|
|
{
|
|
$owner = $this->createUser('claim-owner@example.com');
|
|
$other = $this->createUser('claim-other@example.com');
|
|
|
|
$device = new Device();
|
|
$device->setMac('AA:BB:CC:DD:EE:05');
|
|
$device->setName('Frame');
|
|
$device->setUser($owner);
|
|
$this->em()->persist($device);
|
|
$this->em()->flush();
|
|
|
|
$this->assertFalse($this->service->isClaimedByAnotherUser('AA:BB:CC:DD:EE:05', $owner));
|
|
$this->assertTrue($this->service->isClaimedByAnotherUser('AA:BB:CC:DD:EE:05', $other));
|
|
// Unknown MAC: not claimed by anyone, so not "claimed by another."
|
|
$this->assertFalse($this->service->isClaimedByAnotherUser('FF:FF:FF:FF:FF:FF', $other));
|
|
}
|
|
|
|
public function test_link_resets_device_specific_state_on_takeover(): void
|
|
{
|
|
$oldOwner = $this->createUser('takeover-old@example.com');
|
|
$newOwner = $this->createUser('takeover-new@example.com');
|
|
$newOwner->setTimezone('America/Chicago');
|
|
$this->em()->flush();
|
|
|
|
$device = new Device();
|
|
$device->setMac('AA:BB:CC:DD:EE:06');
|
|
$device->setName('Living Room');
|
|
$device->setUser($oldOwner);
|
|
$device->setWakeTimes([6 * 60, 22 * 60]);
|
|
$this->em()->persist($device);
|
|
$this->em()->flush();
|
|
|
|
$this->service->linkToUser('AA:BB:CC:DD:EE:06', $newOwner, allowClaim: true);
|
|
|
|
$this->em()->refresh($device);
|
|
$this->assertSame('', $device->getName(), 'name reset');
|
|
$this->assertSame([12 * 60], $device->getWakeTimes(), 'wakeTimes default to noon-daily');
|
|
$this->assertSame('America/Chicago', $device->getTimezone(), 'timezone matches new owner');
|
|
$this->assertNull($device->getCurrentImage(), 'currentImage reset');
|
|
$this->assertNull($device->getNextPollExpectedAt(), 'next-poll reset');
|
|
}
|
|
|
|
public function test_link_creates_new_device_with_noon_daily_default(): void
|
|
{
|
|
$user = $this->createUser('new-device@example.com');
|
|
$user->setTimezone('Europe/Stockholm');
|
|
$this->em()->flush();
|
|
|
|
$device = $this->service->linkToUser('AA:BB:CC:DD:EE:09', $user);
|
|
|
|
$this->assertSame([12 * 60], $device->getWakeTimes(), 'noon-daily default');
|
|
$this->assertSame('Europe/Stockholm', $device->getTimezone(), 'inherits user tz');
|
|
}
|
|
|
|
public function test_link_falls_back_to_UTC_when_user_has_no_timezone(): void
|
|
{
|
|
$user = $this->createUser('no-tz@example.com');
|
|
// Force the user's timezone to empty to exercise the fallback —
|
|
// some legacy User rows may have NULL/blank timezone.
|
|
$ref = new \ReflectionProperty(\App\Entity\User::class, 'timezone');
|
|
$ref->setAccessible(true);
|
|
$ref->setValue($user, '');
|
|
$this->em()->flush();
|
|
|
|
$device = $this->service->linkToUser('AA:BB:CC:DD:EE:0A', $user);
|
|
$this->assertSame('UTC', $device->getTimezone());
|
|
}
|
|
}
|