serializer = new DeviceSerializer(); } public function test_includes_all_expected_fields(): void { $device = $this->makeDevice(); $payload = $this->serializer->serialize($device); $this->assertEqualsCanonicalizing( ['id', 'mac', 'name', 'model', 'orientation', 'rotationIntervalMinutes', 'wakeTimes', 'timezone', 'uniquenessWindow', 'rotationMode', 'prioritizeNeverShown', 'linkedAt', 'lastSeenAt', 'nextPollExpectedAt', 'lockedImageId', 'currentImageId'], array_keys($payload), ); } public function test_serializes_model_field(): void { $device = $this->makeDevice(); $device->setModel(\App\Enum\DeviceModel::V2); $payload = $this->serializer->serialize($device); $this->assertSame('v2', $payload['model']); } public function test_serializes_scalars_in_expected_shapes(): void { $device = $this->makeDevice(); $device->setName('Living Room'); $device->setOrientation(Orientation::Portrait); $device->setRotationIntervalMinutes(15); $device->setWakeTimes([6 * 60, 18 * 60]); $device->setTimezone('America/Chicago'); $device->setUniquenessWindow(7); $device->setRotationMode(RotationMode::Random); $device->setPrioritizeNeverShown(true); $payload = $this->serializer->serialize($device); $this->assertSame('Living Room', $payload['name']); $this->assertSame('portrait', $payload['orientation']); $this->assertSame(15, $payload['rotationIntervalMinutes']); $this->assertSame([360, 1080], $payload['wakeTimes']); $this->assertSame('America/Chicago', $payload['timezone']); $this->assertSame(7, $payload['uniquenessWindow']); $this->assertSame('random', $payload['rotationMode']); $this->assertTrue($payload['prioritizeNeverShown']); } public function test_serializes_nullable_timestamps_as_null_when_unset(): void { $device = $this->makeDevice(); $payload = $this->serializer->serialize($device); $this->assertNull($payload['lastSeenAt']); $this->assertNull($payload['nextPollExpectedAt']); $this->assertNull($payload['lockedImageId']); $this->assertNull($payload['currentImageId']); } public function test_serializes_timestamps_as_iso_8601(): void { $device = $this->makeDevice(); $device->markSeen(); $device->setNextPollExpectedAt(new \DateTimeImmutable('2026-05-07T12:34:56+00:00')); $payload = $this->serializer->serialize($device); $this->assertMatchesRegularExpression( '/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}[+-]\d{2}:\d{2}$/', $payload['lastSeenAt'], ); $this->assertSame('2026-05-07T12:34:56+00:00', $payload['nextPollExpectedAt']); } public function test_lockedImageId_and_currentImageId_reflect_assignments(): void { $device = $this->makeDevice(); $image = $this->makeImage(42); $device->setLockedImage($image); $device->setCurrentImage($image); $payload = $this->serializer->serialize($device); $this->assertSame(42, $payload['lockedImageId']); $this->assertSame(42, $payload['currentImageId']); } private function makeDevice(): Device { $device = new Device(); $device->setMac('AA:BB:CC:DD:EE:FF'); $device->setName('Test'); $device->setModel(DeviceModel::V1); $device->setOrientation(Orientation::Landscape); // Persisted-id is null in unit-tests; serializer must tolerate it. return $device; } private function makeImage(int $id): Image { $image = new Image(); // Image::$id is set by Doctrine — bypass via reflection in unit context. $ref = new \ReflectionProperty(Image::class, 'id'); $ref->setAccessible(true); $ref->setValue($image, $id); return $image; } }