Add support for audit display and conversion of ExportGeneration entities

- Introduced `ExportGenerationSubjectConverter` for converting `ExportGeneration` entities into audit subjects.
- Added `ExportGenerationSubjectDisplayer` for translating and displaying `ExportGeneration` audit subjects.
- Included new translation keys for `ExportGeneration` display messages.
- Implemented comprehensive unit tests for both the converter and displayer to ensure reliability.
This commit is contained in:
2026-03-17 12:06:51 +01:00
parent 7b8ee96938
commit eba4527093
5 changed files with 306 additions and 0 deletions

View File

@@ -0,0 +1,37 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\MainBundle\Audit\Converter;
use Chill\MainBundle\Audit\Subject;
use Chill\MainBundle\Audit\SubjectBag;
use Chill\MainBundle\Audit\SubjectConverterInterface;
use Chill\MainBundle\Entity\ExportGeneration;
class ExportGenerationSubjectConverter implements SubjectConverterInterface
{
public function convert(mixed $subject, bool $includeAssociated = false): SubjectBag
{
\assert($subject instanceof ExportGeneration);
return new SubjectBag(new Subject('export_generation', ['id' => $subject->getId(), 'alias' => $subject->getExportAlias()]));
}
public function supportsConvert(mixed $subject): bool
{
return $subject instanceof ExportGeneration;
}
public static function getDefaultPriority(): int
{
return 0;
}
}

View File

@@ -0,0 +1,49 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\MainBundle\Audit\Displayer;
use Chill\MainBundle\Audit\Subject;
use Chill\MainBundle\Audit\SubjectDisplayerInterface;
use Chill\MainBundle\Repository\ExportGenerationRepository;
use Symfony\Contracts\Translation\TranslatorInterface;
final readonly class ExportGenerationSubjectDisplayer implements SubjectDisplayerInterface
{
public function __construct(
private ExportGenerationRepository $exportGenerationRepository,
private TranslatorInterface $translator,
) {}
public function supportsDisplay(Subject $subject, array $options = []): bool
{
return 'export_generation' === $subject->type;
}
public function display(Subject $subject, string $format = 'html', array $options = []): string
{
$export = $this->exportGenerationRepository->find($subject->identifiers['id']);
$msg = match (null === $export || '' === $export->getStoredObject()->getTitle()) {
true => $this->translator->trans('audit.export_generation.display_without_entity', ['{{ id }}' => $subject->identifiers['id']]),
false => $this->translator->trans('audit.export_generation.display_with_entity', [
'{{ id }}' => $subject->identifiers['id'],
'{{ title }}' => 'html' === $format ? htmlspecialchars($export->getStoredObject()->getTitle()) : $export->getStoredObject()->getTitle(),
]),
};
if ('html' === $format) {
return '<span>'.$msg.'</span>';
}
return $msg;
}
}

View File

@@ -0,0 +1,73 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\MainBundle\Tests\Audit\Converter;
use Chill\MainBundle\Audit\Converter\ExportGenerationSubjectConverter;
use Chill\MainBundle\Entity\ExportGeneration;
use PHPUnit\Framework\TestCase;
/**
* @internal
*
* @coversNothing
*/
class ExportGenerationSubjectConverterTest extends TestCase
{
private ExportGenerationSubjectConverter $converter;
protected function setUp(): void
{
$this->converter = new ExportGenerationSubjectConverter();
}
public function testSupportsConvert(): void
{
$exportGeneration = $this->createMock(ExportGeneration::class);
$this->assertTrue($this->converter->supportsConvert($exportGeneration));
$this->assertFalse($this->converter->supportsConvert(new \stdClass()));
}
public function testConvert(): void
{
$exportAlias = 'my_export_alias';
$exportGeneration = new ExportGeneration($exportAlias);
$id = $exportGeneration->getId();
$subjectBag = $this->converter->convert($exportGeneration);
$subject = $subjectBag->subject;
$this->assertSame('export_generation', $subject->type);
$this->assertSame($id, $subject->identifiers['id']);
$this->assertSame($exportAlias, $subject->identifiers['alias']);
}
public function testConvertWithIncludeAssociated(): void
{
$exportAlias = 'my_export_alias';
$exportGeneration = new ExportGeneration($exportAlias);
$id = $exportGeneration->getId();
$subjectBag = $this->converter->convert($exportGeneration, true);
$subject = $subjectBag->subject;
$this->assertSame('export_generation', $subject->type);
$this->assertSame($id, $subject->identifiers['id']);
$this->assertSame($exportAlias, $subject->identifiers['alias']);
$this->assertEmpty($subjectBag->associatedSubjects);
}
public function testGetDefaultPriority(): void
{
$this->assertSame(0, ExportGenerationSubjectConverter::getDefaultPriority());
}
}

View File

@@ -0,0 +1,142 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\MainBundle\Tests\Audit\Displayer;
use Chill\MainBundle\Audit\Displayer\ExportGenerationSubjectDisplayer;
use Chill\MainBundle\Audit\Subject;
use Chill\MainBundle\Entity\ExportGeneration;
use Chill\MainBundle\Repository\ExportGenerationRepository;
use PHPUnit\Framework\TestCase;
use Prophecy\PhpUnit\ProphecyTrait;
use Symfony\Contracts\Translation\TranslatorInterface;
/**
* @internal
*
* @coversNothing
*/
class ExportGenerationSubjectDisplayerTest extends TestCase
{
use ProphecyTrait;
private $exportGenerationRepository;
private $translator;
private ExportGenerationSubjectDisplayer $displayer;
protected function setUp(): void
{
$this->exportGenerationRepository = $this->prophesize(ExportGenerationRepository::class);
$this->translator = $this->prophesize(TranslatorInterface::class);
$this->displayer = new ExportGenerationSubjectDisplayer(
$this->exportGenerationRepository->reveal(),
$this->translator->reveal()
);
}
public function testSupportsDisplay(): void
{
$subject = new Subject('export_generation', ['id' => 'foo']);
$this->assertTrue($this->displayer->supportsDisplay($subject));
$otherSubject = new Subject('other', ['id' => 'foo']);
$this->assertFalse($this->displayer->supportsDisplay($otherSubject));
}
public function testDisplayWithEntityNotFound(): void
{
$id = '550e8400-e29b-41d4-a716-446655440000';
$subject = new Subject('export_generation', ['id' => $id]);
$this->exportGenerationRepository->find($id)->willReturn(null);
$this->translator->trans('audit.export_generation.display_without_entity', ['{{ id }}' => $id])
->willReturn('Export not found '.$id);
$result = $this->displayer->display($subject, 'text');
$this->assertSame('Export not found '.$id, $result);
}
public function testDisplayWithEntityFound(): void
{
$id = '550e8400-e29b-41d4-a716-446655440000';
$subject = new Subject('export_generation', ['id' => $id]);
$exportGeneration = new ExportGeneration('my_alias');
$exportGeneration->getStoredObject()->setTitle('My Export Title');
$this->exportGenerationRepository->find($id)->willReturn($exportGeneration);
$this->translator->trans('audit.export_generation.display_with_entity', [
'{{ id }}' => $id,
'{{ title }}' => 'My Export Title',
])->willReturn('Export '.$id.' with title My Export Title');
$result = $this->displayer->display($subject, 'text');
$this->assertSame('Export '.$id.' with title My Export Title', $result);
}
public function testDisplayWithEmptyTitle(): void
{
$id = '550e8400-e29b-41d4-a716-446655440000';
$subject = new Subject('export_generation', ['id' => $id]);
$exportGeneration = new ExportGeneration('my_alias');
$exportGeneration->getStoredObject()->setTitle('');
$this->exportGenerationRepository->find($id)->willReturn($exportGeneration);
$this->translator->trans('audit.export_generation.display_without_entity', ['{{ id }}' => $id])
->willReturn('Export not found '.$id);
$result = $this->displayer->display($subject, 'text');
$this->assertSame('Export not found '.$id, $result);
}
public function testDisplayHtmlFormatWrapsInSpan(): void
{
$id = '550e8400-e29b-41d4-a716-446655440000';
$subject = new Subject('export_generation', ['id' => $id]);
$this->exportGenerationRepository->find($id)->willReturn(null);
$this->translator->trans('audit.export_generation.display_without_entity', ['{{ id }}' => $id])
->willReturn('Translated Msg');
$result = $this->displayer->display($subject, 'html');
$this->assertSame('<span>Translated Msg</span>', $result);
}
public function testDisplayHtmlFormatEscapesTitle(): void
{
$id = '550e8400-e29b-41d4-a716-446655440000';
$subject = new Subject('export_generation', ['id' => $id]);
$exportGeneration = new ExportGeneration('my_alias');
$exportGeneration->getStoredObject()->setTitle('Title with <script>alert("xss")</script>');
$this->exportGenerationRepository->find($id)->willReturn($exportGeneration);
$escapedTitle = 'Title with &lt;script&gt;alert(&quot;xss&quot;)&lt;/script&gt;';
$this->translator->trans('audit.export_generation.display_with_entity', [
'{{ id }}' => $id,
'{{ title }}' => $escapedTitle,
])->willReturn('Export with title '.$escapedTitle);
$result = $this->displayer->display($subject, 'html');
$this->assertSame('<span>Export with title '.$escapedTitle.'</span>', $result);
}
}

View File

@@ -1046,3 +1046,8 @@ audit_trail:
waiting: En cours de préparation
failure: Erreur lors de la préparation du document
stopped: La préparation a nécessité une attente trop longue, veuillez recharger la page
audit:
export_generation:
display_without_entity: "Génération d'export n°{{ id }}"
display_with_entity: "Génération d'export n°{{ id }}: {{ title }}"