em = self::getContainer()->get(EntityManagerInterface::class); // Clean slate for deterministic assertions: these tests are not wrapped // in a rolled-back transaction. $this->em->getConnection()->executeStatement("DELETE FROM time_entry WHERE title = 'ts-test-entry'"); $this->em->getConnection()->executeStatement("DELETE FROM \"user\" WHERE username = 'ts_test_user'"); } protected function tearDown(): void { $this->em->getConnection()->executeStatement("DELETE FROM time_entry WHERE title = 'ts-test-entry'"); $this->em->getConnection()->executeStatement("DELETE FROM \"user\" WHERE username = 'ts_test_user'"); parent::tearDown(); unset($this->em); } public function testCreatedAtIsSetOnPersist(): void { $entry = $this->makeEntry(); $this->em->persist($entry); $this->em->flush(); self::assertInstanceOf(DateTimeImmutable::class, $entry->getCreatedAt(), 'createdAt must be set after flush'); self::assertInstanceOf(DateTimeImmutable::class, $entry->getUpdatedAt(), 'updatedAt must be set after flush'); // Confirm it was actually persisted to the DB, not just the in-memory object. $this->em->clear(); $reloaded = $this->em->getRepository(TimeEntry::class)->find($entry->getId()); self::assertNotNull($reloaded); self::assertNotNull($reloaded->getCreatedAt(), 'created_at must be persisted in DB'); } public function testUpdatedAtIsRefreshedOnUpdate(): void { $entry = $this->makeEntry(); $this->em->persist($entry); $this->em->flush(); $entryId = $entry->getId(); $initialUpdatedAt = $entry->getUpdatedAt(); self::assertNotNull($initialUpdatedAt); // Force a distinguishable timestamp by backdating the persisted value, // so the preUpdate refresh is observable even within the same second. $this->em->getConnection()->executeStatement( "UPDATE time_entry SET updated_at = '2000-01-01 00:00:00' WHERE id = :id", ['id' => $entryId], ); $this->em->clear(); /** @var TimeEntry $managed */ $managed = $this->em->getRepository(TimeEntry::class)->find($entryId); self::assertSame('2000-01-01 00:00:00', $managed->getUpdatedAt()?->format('Y-m-d H:i:s')); // Mutate a tracked field and flush -> preUpdate must refresh updated_at. $managed->setTitle('ts-test-entry'); $managed->setDescription('changed description'); $this->em->flush(); $this->em->clear(); /** @var TimeEntry $reloaded */ $reloaded = $this->em->getRepository(TimeEntry::class)->find($entryId); self::assertNotNull($reloaded->getUpdatedAt()); self::assertNotSame( '2000-01-01 00:00:00', $reloaded->getUpdatedAt()->format('Y-m-d H:i:s'), 'updated_at must be refreshed and persisted on UPDATE', ); } private function makeEntry(): TimeEntry { $user = new User(); $user->setUsername('ts_test_user'); $user->setPassword('hashed-secret'); $user->setRoles(['ROLE_USER']); $this->em->persist($user); $entry = new TimeEntry(); $entry->setUser($user); $entry->setTitle('ts-test-entry'); $entry->setStartedAt(new DateTimeImmutable()); return $entry; } }