Admins get /admin/history: one line per booking created, rescheduled or released, per series created or ended, and per resource deleted with its bookings. Sortable by time, kind, resource, booked day and both people; filterable by period, kinds, who acted and resource type. Events are snapshots written inside the same transaction as the change, so a rolled-back booking leaves none and a released booking stays readable. app:booking-events:prune keeps twelve months and needs a cron entry on the server. Co-Authored-By: Claude Opus 5.5 <[email protected]>
84 lines
2.6 KiB
PHP
84 lines
2.6 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Tests\Command;
|
|
|
|
use App\Entity\BookingEvent;
|
|
use App\Enum\BookingEventType;
|
|
use App\Enum\ResourceType;
|
|
use Doctrine\ORM\EntityManagerInterface;
|
|
use Symfony\Bundle\FrameworkBundle\Console\Application;
|
|
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
|
|
use Symfony\Component\Console\Command\Command;
|
|
use Symfony\Component\Console\Tester\CommandTester;
|
|
|
|
/**
|
|
* The retention period of the event history, enforced from cron.
|
|
*/
|
|
class PruneBookingEventsCommandTest extends KernelTestCase
|
|
{
|
|
private EntityManagerInterface $em;
|
|
private CommandTester $command;
|
|
|
|
protected function setUp(): void
|
|
{
|
|
self::bootKernel();
|
|
$this->em = self::getContainer()->get(EntityManagerInterface::class);
|
|
$this->em->getConnection()->beginTransaction();
|
|
$this->command = new CommandTester((new Application(self::$kernel))->find('app:booking-events:prune'));
|
|
}
|
|
|
|
protected function tearDown(): void
|
|
{
|
|
$this->em->getConnection()->rollBack();
|
|
parent::tearDown();
|
|
}
|
|
|
|
public function testEventsOlderThanAYearAreDeleted(): void
|
|
{
|
|
$old = $this->event('-13 months');
|
|
$recent = $this->event('-11 months');
|
|
|
|
$this->command->execute([]);
|
|
|
|
$this->command->assertCommandIsSuccessful();
|
|
self::assertStringContainsString('Deleted 1 booking events', $this->command->getDisplay());
|
|
self::assertNull($this->em->find(BookingEvent::class, $old));
|
|
self::assertNotNull($this->em->find(BookingEvent::class, $recent));
|
|
}
|
|
|
|
public function testTheRetentionCanBeShortened(): void
|
|
{
|
|
$event = $this->event('-2 months');
|
|
|
|
$this->command->execute(['--months' => '1']);
|
|
|
|
$this->command->assertCommandIsSuccessful();
|
|
self::assertNull($this->em->find(BookingEvent::class, $event));
|
|
}
|
|
|
|
public function testANonsensicalRetentionIsRefused(): void
|
|
{
|
|
$event = $this->event('-2 months');
|
|
|
|
self::assertSame(Command::INVALID, $this->command->execute(['--months' => '0']));
|
|
self::assertNotNull($this->em->find(BookingEvent::class, $event));
|
|
}
|
|
|
|
private function event(string $when): int
|
|
{
|
|
$event = new BookingEvent(BookingEventType::BOOKING_CREATED, null, 'System', null, 'Tisch 1', ResourceType::DESK);
|
|
$this->em->persist($event);
|
|
$this->em->flush();
|
|
|
|
$this->em->getConnection()->executeStatement(
|
|
'UPDATE booking_event SET occurred_at = ? WHERE id = ?',
|
|
[(new \DateTimeImmutable($when))->format('Y-m-d H:i:s'), $event->getId()],
|
|
);
|
|
$this->em->clear();
|
|
|
|
return (int) $event->getId();
|
|
}
|
|
}
|