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'); $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([], $device->getWakeTimes(), 'wakeTimes reset'); $this->assertNull($device->getCurrentImage(), 'currentImage reset'); $this->assertNull($device->getNextPollExpectedAt(), 'next-poll reset'); } }