Partage d'export enregistré et génération asynchrone des exports

This commit is contained in:
2025-07-08 13:53:25 +00:00
parent c4cc0baa8e
commit 8bc16dadb0
447 changed files with 14134 additions and 3854 deletions

View File

@@ -0,0 +1,81 @@
<?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\Authorization;
use Chill\MainBundle\Controller\ExportGenerationCreateFromSavedExportController;
use Chill\MainBundle\Entity\ExportGeneration;
use Chill\MainBundle\Entity\SavedExport;
use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Export\Messenger\ExportRequestGenerationMessage;
use Chill\MainBundle\Security\Authorization\SavedExportVoter;
use Doctrine\ORM\EntityManagerInterface;
use PHPUnit\Framework\TestCase;
use Prophecy\Argument;
use Prophecy\PhpUnit\ProphecyTrait;
use Symfony\Component\Clock\MockClock;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\Messenger\Envelope;
use Symfony\Component\Messenger\MessageBusInterface;
use Symfony\Component\Security\Core\Security;
use Symfony\Component\Serializer\SerializerInterface;
/**
* @internal
*
* @coversNothing
*/
class ExportGenerationCreateFromSavedExportControllerTest extends TestCase
{
use ProphecyTrait;
public function testInvoke(): void
{
$savedExport = new SavedExport();
$savedExport->setOptions($exportOptions = ['test' => 'content'])->setExportAlias('dummy_export_alias');
$user = new User();
$reflection = new \ReflectionClass($user);
$id = $reflection->getProperty('id');
$id->setValue($user, 1);
$security = $this->prophesize(Security::class);
$security->isGranted(SavedExportVoter::GENERATE, $savedExport)->shouldBeCalled()->willReturn(true);
$security->getUser()->willReturn($user);
$entityManager = $this->prophesize(EntityManagerInterface::class);
$entityManager->persist(Argument::that(
static fn ($arg) => $arg instanceof ExportGeneration && $arg->getOptions() === $exportOptions && $arg->getSavedExport() === $savedExport,
))->shouldBeCalled();
$entityManager->flush()->shouldBeCalled();
$messenger = $this->prophesize(MessageBusInterface::class);
$messenger->dispatch(Argument::type(ExportRequestGenerationMessage::class))->shouldBeCalled()->will(
static fn (array $args): Envelope => new Envelope($args[0]),
);
$serializer = $this->prophesize(SerializerInterface::class);
$serializer->serialize(Argument::type(ExportGeneration::class), 'json', ['groups' => ['read']])->shouldBeCalled()->willReturn('{"test": "export-generation"}');
$controller = new ExportGenerationCreateFromSavedExportController(
$security->reveal(),
$entityManager->reveal(),
$messenger->reveal(),
new MockClock(),
$serializer->reveal()
);
$response = $controller($savedExport);
self::assertInstanceOf(JsonResponse::class, $response);
self::assertEquals('{"test": "export-generation"}', $response->getContent());
}
}

View File

@@ -0,0 +1,56 @@
<?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\Controller;
use Chill\DocStoreBundle\Entity\StoredObject;
use Chill\MainBundle\Controller\ExportGenerationController;
use Chill\MainBundle\Entity\ExportGeneration;
use Chill\MainBundle\Export\ExportManager;
use PHPUnit\Framework\TestCase;
use Prophecy\Argument;
use Prophecy\PhpUnit\ProphecyTrait;
use Symfony\Component\Security\Core\Security;
use Symfony\Component\Serializer\SerializerInterface;
use Twig\Environment;
use function PHPUnit\Framework\assertEquals;
/**
* @internal
*
* @coversNothing
*/
class ExportGenerationControllerTest extends TestCase
{
use ProphecyTrait;
public function testObjectStatus(): void
{
$security = $this->prophesize(Security::class);
$security->isGranted('ROLE_USER')->willReturn(true);
$environment = $this->prophesize(Environment::class);
$serializer = $this->prophesize(SerializerInterface::class);
$serializer->serialize(Argument::any(), 'json', ['groups' => ['read']])->willReturn('{}');
$exportManager = $this->prophesize(ExportManager::class);
$pending = new ExportGeneration('dummy', []);
$controller = new ExportGenerationController($security->reveal(), $environment->reveal(), $serializer->reveal(), $exportManager->reveal());
$actual = $controller->objectStatus($pending);
self::assertEquals('{}', $actual->getContent());
$generated = new ExportGeneration('dummy', []);
$generated->getStoredObject()->setStatus(StoredObject::STATUS_READY);
self:assertEquals('{}', $controller->objectStatus($generated)->getContent());
}
}

View File

@@ -21,13 +21,13 @@ use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
*
* @coversNothing
*/
final class ExportControllerTest extends WebTestCase
final class ExportIndexControllerTest extends WebTestCase
{
use PrepareClientTrait;
public function testIndex()
{
$client = $this->getClientAuthenticatedAsAdmin();
$client = $this->getClientAuthenticated();
$client->request('GET', '/fr/exports/');

View File

@@ -0,0 +1,52 @@
<?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\Entity\Workflow;
use Chill\MainBundle\Entity\SavedExport;
use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Entity\UserGroup;
use PHPUnit\Framework\TestCase;
/**
* @internal
*
* @coversNothing
*/
class SavedExportTest extends TestCase
{
public function testIsSharedWithUser(): void
{
// Create test users
$user1 = new User();
$user2 = new User();
$user3 = new User();
// Create a test group and add user2 to the group
$group = new UserGroup();
$group->addUser($user2);
// Create a SavedExport entity
$savedExport = new SavedExport();
// Share the saved export with user1
$savedExport->addShare($user1);
// Share the saved export with the group
$savedExport->addShare($group);
// Assertions
$this->assertTrue($savedExport->isSharedWithUser($user1), 'User1 should have access to the saved export.');
$this->assertTrue($savedExport->isSharedWithUser($user2), 'User2 (via group) should have access to the saved export.');
$this->assertFalse($savedExport->isSharedWithUser($user3), 'User3 should not have access to the saved export.');
}
}

View File

@@ -0,0 +1,117 @@
<?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\Export\Cronjob;
use Chill\MainBundle\Entity\CronJobExecution;
use Chill\MainBundle\Entity\ExportGeneration;
use Chill\MainBundle\Export\Cronjob\RemoveExpiredExportGenerationCronJob;
use Chill\MainBundle\Export\Messenger\RemoveExportGenerationMessage;
use PHPUnit\Framework\TestCase;
use Prophecy\PhpUnit\ProphecyTrait;
use Prophecy\Argument;
use Symfony\Component\Clock\MockClock;
use Symfony\Component\Messenger\Envelope;
use Symfony\Component\Messenger\MessageBusInterface;
use Chill\MainBundle\Repository\ExportGenerationRepository;
/**
* @internal
*
* @coversNothing
*/
class RemoveExpiredExportGenerationCronJobTest extends TestCase
{
use ProphecyTrait;
public function testCanRunReturnsTrueWhenLastExecutionIsNull()
{
$clock = new MockClock(new \DateTimeImmutable('2024-06-25 10:00:00'));
$repo = $this->prophesize(ExportGenerationRepository::class);
$bus = $this->prophesize(MessageBusInterface::class);
$cronJob = new RemoveExpiredExportGenerationCronJob(
$clock,
$repo->reveal(),
$bus->reveal()
);
$this->assertTrue($cronJob->canRun(null));
}
public function testCanRunReturnsTrueWhenLastStartIsOlderThan24Hours()
{
$clock = new MockClock(new \DateTimeImmutable('2024-06-25 10:00:00'));
$repo = $this->prophesize(ExportGenerationRepository::class);
$bus = $this->prophesize(MessageBusInterface::class);
$cronJob = new RemoveExpiredExportGenerationCronJob(
$clock,
$repo->reveal(),
$bus->reveal()
);
$execution = new CronJobExecution('remove-expired-export-generation');
$execution->setLastStart(new \DateTimeImmutable('2024-06-24 09:59:59'));
$this->assertTrue($cronJob->canRun($execution));
}
public function testCanRunReturnsFalseWhenLastStartIsWithin24Hours()
{
$clock = new MockClock(new \DateTimeImmutable('2024-06-25 10:00:00'));
$repo = $this->prophesize(ExportGenerationRepository::class);
$bus = $this->prophesize(MessageBusInterface::class);
$cronJob = new RemoveExpiredExportGenerationCronJob(
$clock,
$repo->reveal(),
$bus->reveal()
);
$execution = new CronJobExecution('remove-expired-export-generation');
$execution->setLastStart(new \DateTimeImmutable('2024-06-24 10:01:00'));
$this->assertFalse($cronJob->canRun($execution));
}
public function testRunDispatchesMessagesForExpiredExportsAndReturnsLastDeletion()
{
$clock = new MockClock(new \DateTimeImmutable('2024-06-25 11:21:00'));
$repo = $this->prophesize(ExportGenerationRepository::class);
$bus = $this->prophesize(MessageBusInterface::class);
$expiredExports = [
new ExportGeneration('dummy', []),
];
$repo->findExpiredExportGeneration(Argument::that(fn ($dateTime) =>
// Ensure the repository is called with the current clock time
$dateTime instanceof \DateTimeImmutable
&& $dateTime->getTimestamp() === $clock->now()->getTimestamp()))->willReturn($expiredExports);
// Expect one RemoveExportGenerationMessage for each expired export
$bus->dispatch(Argument::that(fn (Envelope $envelope) => $envelope->getMessage() instanceof RemoveExportGenerationMessage))
->shouldBeCalledTimes(1)
->will(fn ($args) => $args[0]);
$cronJob = new RemoveExpiredExportGenerationCronJob(
$clock,
$repo->reveal(),
$bus->reveal()
);
$result = $cronJob->run([]);
$this->assertIsArray($result);
$this->assertEquals(['last-deletion' => $clock->now()->getTimestamp()], $result);
}
}

View File

@@ -0,0 +1,333 @@
<?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\Export;
use Chill\MainBundle\Entity\Center;
use Chill\MainBundle\Export\AggregatorInterface;
use Chill\MainBundle\Export\ExportConfigNormalizer;
use Chill\MainBundle\Export\ExportInterface;
use Chill\MainBundle\Export\ExportManager;
use Chill\MainBundle\Export\FilterInterface;
use Chill\MainBundle\Export\FormatterInterface;
use Chill\MainBundle\Repository\CenterRepositoryInterface;
use Chill\MainBundle\Repository\RegroupmentRepositoryInterface;
use PHPUnit\Framework\TestCase;
use Prophecy\Argument;
use Prophecy\PhpUnit\ProphecyTrait;
/**
* @internal
*
* @coversNothing
*/
class ExportConfigNormalizerTest extends TestCase
{
use ProphecyTrait;
public function testNormalizeConfig(): void
{
$filterEnabled = $this->prophesize(FilterInterface::class);
$filterEnabled->normalizeFormData(['test' => '0'])->shouldBeCalled()->willReturn(['test' => '0']);
$filterEnabled->getNormalizationVersion()->willReturn(1);
$filterDisabled = $this->prophesize(FilterInterface::class);
$filterDisabled->normalizeFormData(['default' => '0'])->shouldNotBeCalled();
$aggregatorEnabled = $this->prophesize(AggregatorInterface::class);
$aggregatorEnabled->normalizeFormData(['test' => '0'])->shouldBeCalled()->willReturn(['test' => '0']);
$aggregatorEnabled->getNormalizationVersion()->willReturn(1);
$aggregatorDisabled = $this->prophesize(AggregatorInterface::class);
$aggregatorDisabled->normalizeFormData(['default' => '0'])->shouldNotBeCalled();
$export = $this->prophesize(ExportInterface::class);
$export->normalizeFormData(['test' => '0'])->shouldBeCalled()->willReturn(['test' => '0']);
$export->getNormalizationVersion()->willReturn(1);
$formatter = $this->prophesize(FormatterInterface::class);
$formatter->normalizeFormData(['test' => '0'])->shouldBeCalled()->willReturn(['test' => '0']);
$formatter->getNormalizationVersion()->willReturn(1);
$exportManager = $this->prophesize(ExportManager::class);
$exportManager->getFormatter('xlsx')->shouldBeCalled()->willReturn($formatter->reveal());
$exportManager->getFilter('filterEnabled')->willReturn($filterEnabled->reveal());
$exportManager->getFilter('filterDisabled')->willReturn($filterDisabled->reveal());
$exportManager->getAggregator('aggregatorEnabled')->willReturn($aggregatorEnabled->reveal());
$exportManager->getAggregator('aggregatorDisabled')->willReturn($aggregatorDisabled->reveal());
$exportManager->getExport('export')->willReturn($export->reveal());
$regroupmentRepository = $this->prophesize(RegroupmentRepositoryInterface::class);
$center = $this->prophesize(Center::class);
$center->getId()->willReturn(10);
$formData = [
'centers' => ['centers' => [$center->reveal()]],
'export' => ['test' => '0'],
'filters' => [
'filterEnabled' => ['enabled' => true, 'form' => ['test' => '0']],
'filterDisabled' => ['enabled' => false, 'form' => ['default' => '0']],
],
'aggregators' => [
'aggregatorEnabled' => ['enabled' => true, 'form' => ['test' => '0']],
'aggregatorDisabled' => ['enabled' => false, 'form' => ['default' => '0']],
],
'pick_formatter' => 'xlsx',
'formatter' => ['test' => '0'],
];
$expected = [
'export' => ['form' => ['test' => '0'], 'version' => 1],
'centers' => ['centers' => [10], 'regroupments' => []],
'filters' => [
'filtersEnabled' => ['enabled' => true, 'form' => ['test' => '0'], 'version' => 1],
'filterDisabled' => ['enabled' => false],
],
'aggregators' => [
'aggregatorEnabled' => ['enabled' => true, 'form' => ['test' => '0'], 'version' => 1],
'aggregatorDisabled' => ['enabled' => false],
],
'pick_formatter' => 'xlsx',
'formatter' => [
'form' => ['test' => '0'],
'version' => 1,
],
];
$exportConfigNormalizer = new ExportConfigNormalizer(
$exportManager->reveal(),
$this->prophesize(CenterRepositoryInterface::class)->reveal(),
$regroupmentRepository->reveal()
);
$actual = $exportConfigNormalizer->normalizeConfig('export', $formData);
self::assertEqualsCanonicalizing($expected, $actual);
}
public function testDenormalizeConfig(): void
{
$filterEnabled = $this->prophesize(FilterInterface::class);
$filterEnabled->denormalizeFormData(['test' => '0'], 1)->shouldBeCalled()->willReturn(['test' => '0']);
$filterDisabled = $this->prophesize(FilterInterface::class);
$filterDisabled->denormalizeFormData(Argument::any(), Argument::type('int'))->shouldNotBeCalled();
$filterDisabled->getFormDefaultData()->willReturn(['default' => '0']);
$aggregatorEnabled = $this->prophesize(AggregatorInterface::class);
$aggregatorEnabled->denormalizeFormData(['test' => '0'], 1)->shouldBeCalled()->willReturn(['test' => '0']);
$aggregatorDisabled = $this->prophesize(AggregatorInterface::class);
$aggregatorDisabled->denormalizeFormData(Argument::any(), Argument::type('int'))->shouldNotBeCalled();
$aggregatorDisabled->getFormDefaultData()->willReturn(['default' => '0']);
$export = $this->prophesize(ExportInterface::class);
$export->denormalizeFormData(['test' => '0'], 1)->shouldBeCalled()->willReturn(['test' => '0']);
$formatter = $this->prophesize(FormatterInterface::class);
$formatter->denormalizeFormData(['test' => '0'], 1)->shouldBeCalled()->willReturn(['test' => '0']);
$exportManager = $this->prophesize(ExportManager::class);
$exportManager->getFormatter('xlsx')->shouldBeCalled()->willReturn($formatter->reveal());
$exportManager->getFilter('filterEnabled')->willReturn($filterEnabled->reveal());
$exportManager->getFilter('filterDisabled')->willReturn($filterDisabled->reveal());
$exportManager->getAggregator('aggregatorEnabled')->willReturn($aggregatorEnabled->reveal());
$exportManager->getAggregator('aggregatorDisabled')->willReturn($aggregatorDisabled->reveal());
$exportManager->getExport('export')->willReturn($export->reveal());
$centerRepository = $this->prophesize(CenterRepositoryInterface::class);
$centerRepository->find(10)->willReturn($center = new Center());
$regroupmentRepository = $this->prophesize(RegroupmentRepositoryInterface::class);
$serialized = [
'centers' => ['regroupments' => [], 'centers' => [10]],
'export' => ['form' => ['test' => '0'], 'version' => 1],
'filters' => [
'filterEnabled' => ['enabled' => true, 'form' => ['test' => '0'], 'version' => 1],
'filterDisabled' => ['enabled' => false],
],
'aggregators' => [
'aggregatorEnabled' => ['enabled' => true, 'form' => ['test' => '0'], 'version' => 1],
'aggregatorDisabled' => ['enabled' => false],
],
'pick_formatter' => 'xlsx',
'formatter' => [
'form' => ['test' => '0'],
'version' => 1,
],
];
$expected = [
'export' => ['test' => '0'],
'filters' => [
'filterEnabled' => ['enabled' => true, 'form' => ['test' => '0']],
'filterDisabled' => ['enabled' => false, 'form' => ['default' => '0']],
],
'aggregators' => [
'aggregatorEnabled' => ['enabled' => true, 'form' => ['test' => '0']],
'aggregatorDisabled' => ['enabled' => false, 'form' => ['default' => '0']],
],
'pick_formatter' => 'xlsx',
'formatter' => ['test' => '0'],
'centers' => ['centers' => [$center], 'regroupments' => []],
];
$exportConfigNormalizer = new ExportConfigNormalizer($exportManager->reveal(), $centerRepository->reveal(), $regroupmentRepository->reveal());
$actual = $exportConfigNormalizer->denormalizeConfig('export', $serialized, true);
self::assertEquals($expected, $actual);
}
public function testNormalizeConfigEmptyData(): void
{
$filterEnabled = $this->prophesize(FilterInterface::class);
$filterEnabled->normalizeFormData([])->shouldBeCalled()->willReturn([]);
$filterEnabled->getNormalizationVersion()->willReturn(1);
$filterDisabled = $this->prophesize(FilterInterface::class);
$filterDisabled->normalizeFormData([])->shouldNotBeCalled();
$aggregatorEnabled = $this->prophesize(AggregatorInterface::class);
$aggregatorEnabled->normalizeFormData([])->shouldBeCalled()->willReturn([]);
$aggregatorEnabled->getNormalizationVersion()->willReturn(1);
$aggregatorDisabled = $this->prophesize(AggregatorInterface::class);
$aggregatorDisabled->normalizeFormData([])->shouldNotBeCalled();
$export = $this->prophesize(ExportInterface::class);
$export->normalizeFormData([])->shouldBeCalled()->willReturn([]);
$export->getNormalizationVersion()->willReturn(1);
$formatter = $this->prophesize(FormatterInterface::class);
$formatter->normalizeFormData([])->shouldBeCalled()->willReturn([]);
$formatter->getNormalizationVersion()->willReturn(1);
$exportManager = $this->prophesize(ExportManager::class);
$exportManager->getFormatter('xlsx')->shouldBeCalled()->willReturn($formatter->reveal());
$exportManager->getFilter('filterEnabled')->willReturn($filterEnabled->reveal());
$exportManager->getFilter('filterDisabled')->willReturn($filterDisabled->reveal());
$exportManager->getAggregator('aggregatorEnabled')->willReturn($aggregatorEnabled->reveal());
$exportManager->getAggregator('aggregatorDisabled')->willReturn($aggregatorDisabled->reveal());
$exportManager->getExport('export')->willReturn($export->reveal());
$center = $this->prophesize(Center::class);
$center->getId()->willReturn(10);
$regroupmentRepository = $this->prophesize(RegroupmentRepositoryInterface::class);
$formData = [
'centers' => ['centers' => [$center->reveal()]],
'export' => [],
'filters' => [
'filterEnabled' => ['enabled' => true, 'form' => []],
'filterDisabled' => ['enabled' => false, 'form' => []],
],
'aggregators' => [
'aggregatorEnabled' => ['enabled' => true, 'form' => []],
'aggregatorDisabled' => ['enabled' => false, 'form' => []],
],
'pick_formatter' => 'xlsx',
'formatter' => [],
];
$expected = [
'export' => ['form' => [], 'version' => 1],
'centers' => ['centers' => [10], 'regroupments' => []],
'filters' => [
'filtersEnabled' => ['enabled' => true, 'form' => [], 'version' => 1],
'filterDisabled' => ['enabled' => false],
],
'aggregators' => [
'aggregatorEnabled' => ['enabled' => true, 'form' => [], 'version' => 1],
'aggregatorDisabled' => ['enabled' => false],
],
'pick_formatter' => 'xlsx',
'formatter' => [
'form' => [],
'version' => 1,
],
];
$exportConfigNormalizer = new ExportConfigNormalizer($exportManager->reveal(), $this->prophesize(CenterRepositoryInterface::class)->reveal(), $regroupmentRepository->reveal());
$actual = $exportConfigNormalizer->normalizeConfig('export', $formData);
self::assertEqualsCanonicalizing($expected, $actual);
}
public function testDenormalizeConfigWithEmptyData(): void
{
$filterEnabled = $this->prophesize(FilterInterface::class);
$filterEnabled->denormalizeFormData([], 1)->shouldBeCalled()->willReturn([]);
$filterDisabled = $this->prophesize(FilterInterface::class);
$filterDisabled->denormalizeFormData(Argument::any(), Argument::type('int'))->shouldNotBeCalled();
$filterDisabled->getFormDefaultData()->willReturn([]);
$aggregatorEnabled = $this->prophesize(AggregatorInterface::class);
$aggregatorEnabled->denormalizeFormData([], 1)->shouldBeCalled()->willReturn([]);
$aggregatorDisabled = $this->prophesize(AggregatorInterface::class);
$aggregatorDisabled->denormalizeFormData(Argument::any(), Argument::type('int'))->shouldNotBeCalled();
$aggregatorDisabled->getFormDefaultData()->willReturn([]);
$export = $this->prophesize(ExportInterface::class);
$export->denormalizeFormData([], 1)->shouldBeCalled()->willReturn([]);
$formatter = $this->prophesize(FormatterInterface::class);
$formatter->denormalizeFormData([], 1)->shouldBeCalled()->willReturn([]);
$exportManager = $this->prophesize(ExportManager::class);
$exportManager->getFormatter('xlsx')->shouldBeCalled()->willReturn($formatter->reveal());
$exportManager->getFilter('filterEnabled')->willReturn($filterEnabled->reveal());
$exportManager->getFilter('filterDisabled')->willReturn($filterDisabled->reveal());
$exportManager->getAggregator('aggregatorEnabled')->willReturn($aggregatorEnabled->reveal());
$exportManager->getAggregator('aggregatorDisabled')->willReturn($aggregatorDisabled->reveal());
$exportManager->getExport('export')->willReturn($export->reveal());
$centerRepository = $this->prophesize(CenterRepositoryInterface::class);
$centerRepository->find(10)->willReturn($center = new Center());
$regroupmentRepository = $this->prophesize(RegroupmentRepositoryInterface::class);
$serialized = [
'centers' => ['centers' => [10], 'regroupments' => []],
'export' => ['form' => [], 'version' => 1],
'filters' => [
'filterEnabled' => ['enabled' => true, 'form' => [], 'version' => 1],
'filterDisabled' => ['enabled' => false],
],
'aggregators' => [
'aggregatorEnabled' => ['enabled' => true, 'form' => [], 'version' => 1],
'aggregatorDisabled' => ['enabled' => false],
],
'pick_formatter' => 'xlsx',
'formatter' => [
'form' => [],
'version' => 1,
],
];
$expected = [
'export' => [],
'filters' => [
'filterEnabled' => ['enabled' => true, 'form' => []],
'filterDisabled' => ['enabled' => false, 'form' => []],
],
'aggregators' => [
'aggregatorEnabled' => ['enabled' => true, 'form' => []],
'aggregatorDisabled' => ['enabled' => false, 'form' => []],
],
'pick_formatter' => 'xlsx',
'formatter' => [],
'centers' => ['centers' => [$center], 'regroupments' => []],
];
$exportConfigNormalizer = new ExportConfigNormalizer($exportManager->reveal(), $centerRepository->reveal(), $regroupmentRepository->reveal());
$actual = $exportConfigNormalizer->denormalizeConfig('export', $serialized, true);
self::assertEquals($expected, $actual);
}
}

View File

@@ -0,0 +1,121 @@
<?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\Export;
use Chill\MainBundle\Export\ExportDataNormalizerTrait;
use Doctrine\Persistence\ObjectRepository;
use PHPUnit\Framework\TestCase;
use Prophecy\Argument;
use Prophecy\PhpUnit\ProphecyTrait;
/**
* @internal
*
* @coversNothing
*/
class ExportDataNormalizerTraitTest extends TestCase
{
use ProphecyTrait;
private function buildTrait(): object
{
return new class () {
use ExportDataNormalizerTrait;
public function normalizeEntity(object|iterable $entity): array|int|string
{
return $this->normalizeDoctrineEntity($entity);
}
public function denormalizeEntity(mixed $entity, ObjectRepository $repository)
{
return $this->denormalizeDoctrineEntity($entity, $repository);
}
public function normalizeD(\DateTimeImmutable|\DateTime $date): string
{
return $this->normalizeDate($date);
}
public function denormalizeD(string $date): \DateTimeImmutable|\DateTime
{
return $this->denormalizeDate($date);
}
};
}
public function testNormalizationDoctrineEntitySingle(): void
{
$entity = new class () {
public function getId(): int
{
return 1;
}
};
$repository = $this->prophesize(ObjectRepository::class);
$repository->find(1)->willReturn($entity);
$normalized = $this->buildTrait()->normalizeEntity($entity);
$actual = $this->buildTrait()->denormalizeEntity($normalized, $repository->reveal());
self::assertSame($entity, $actual);
}
public function testNormalizationDoctrineEntityMulti(): void
{
$entityA = new class () {
public function getId(): int
{
return 1;
}
};
$entityB = new class () {
public function getId(): int
{
return 2;
}
};
$repository = $this->prophesize(ObjectRepository::class);
$repository->findBy(
Argument::that(static fn ($arg): bool => in_array(1, $arg['id'] ?? []) && in_array(2, $arg['id'] ?? []))
)->willReturn([$entityA, $entityB]);
$normalized = $this->buildTrait()->normalizeEntity([$entityA, $entityB]);
$actual = $this->buildTrait()->denormalizeEntity($normalized, $repository->reveal());
self::assertContains(1, array_map(static fn (object $item) => $item->getId(), $actual));
self::assertContains(2, array_map(static fn (object $item) => $item->getId(), $actual));
self::assertCount(2, $actual);
}
/**
* @dataProvider provideDate
*/
public function testNormalizationDate(\DateTimeImmutable|\DateTime $date): void
{
$normalized = $this->buildTrait()->normalizeD($date);
$actual = $this->buildTrait()->denormalizeD($normalized);
self::assertEquals($date, $actual);
}
public static function provideDate(): iterable
{
yield [new \DateTimeImmutable('2024-01-15T18:57:20', new \DateTimeZone('Europe/Athens'))];
yield [new \DateTimeImmutable('2024-01-15T18:57:30', new \DateTimeZone('America/Havana'))];
yield [new \DateTime('2024-01-15T18:57:40', new \DateTimeZone('Europe/Madrid'))];
yield [new \DateTime('2024-01-15T18:57:50', new \DateTimeZone('Africa/Kinshasa'))];
}
}

View File

@@ -0,0 +1,174 @@
<?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\Export;
use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Export\AggregatorInterface;
use Chill\MainBundle\Export\ExportConfigNormalizer;
use Chill\MainBundle\Export\ExportConfigProcessor;
use Chill\MainBundle\Export\ExportDescriptionHelper;
use Chill\MainBundle\Export\ExportGenerationContext;
use Chill\MainBundle\Export\ExportInterface;
use Chill\MainBundle\Export\ExportManager;
use Chill\MainBundle\Export\FilterInterface;
use Chill\MainBundle\Security\Authorization\AuthorizationHelperInterface;
use PHPUnit\Framework\TestCase;
use Prophecy\Argument;
use Prophecy\PhpUnit\ProphecyTrait;
use Psr\Log\NullLogger;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorage;
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
use Symfony\Component\Security\Core\Security;
use Symfony\Contracts\Translation\TranslatableInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
/**
* @internal
*
* @coversNothing
*/
class ExportDescriptionHelperTest extends TestCase
{
use ProphecyTrait;
private const JSON_HAPPY_SCENARIO = <<<'JSON'
{
"export": {
"form": [],
"version": 1
},
"centers": {
"centers": [
1
],
"regroupments": []
},
"filters": {
"my_filter_string": {
"form": {
"accepted_socialissues": [
2,
3
]
},
"enabled": true,
"version": 1
},
"my_filter_array": {
"form": {
"misc": true
},
"enabled": true,
"version": 1
},
"my_filter_translatable": {
"form": {
"misc": true
},
"enabled": true,
"version": 1
}
},
"formatter": {
"form": {
"format": "xlsx",
"activity_user_aggregator": {
"order": 1
}
},
"version": 1
},
"aggregators": {
"my_aggregator": {
"form": {"key": 1},
"enabled": true,
"version": 1
}
},
"pick_formatter": "spreadsheet"
}
JSON;
public function testDescribeHappyScenario(): void
{
$options = json_decode(self::JSON_HAPPY_SCENARIO, true);
$security = $this->prophesize(Security::class);
$security->getUser()->willReturn($user = new User());
$exportConfigNormalizer = $this->prophesize(ExportConfigNormalizer::class);
$exportConfigNormalizer->denormalizeConfig('my_export', Argument::type('array'))->willReturn($options);
$export = $this->prophesize(ExportInterface::class);
$export->getTitle()->willReturn('Title');
$myFilterString = $this->prophesize(FilterInterface::class);
$myFilterString->describeAction(Argument::type('array'), Argument::type(ExportGenerationContext::class))->willReturn($string0 = 'This is a filter description');
$myFilterArray = $this->prophesize(FilterInterface::class);
$myFilterArray->describeAction(Argument::type('array'), Argument::type(ExportGenerationContext::class))->willReturn([$string1 = 'This is a filter with %argument%', $arg1 = ['%argument%' => 'zero']]);
$myFilterTranslatable = $this->prophesize(FilterInterface::class);
$myFilterTranslatable->describeAction(Argument::type('array'), Argument::type(ExportGenerationContext::class))
->willReturn(new class () implements TranslatableInterface {
public function trans(TranslatorInterface $translator, ?string $locale = null): string
{
return 'translatable';
}
});
$myAggregator = $this->prophesize(AggregatorInterface::class);
$myAggregator->getTitle()->willReturn('Some aggregator');
$token = new UsernamePasswordToken($user, 'main', ['ROLE_USER']);
$tokenStorage = new TokenStorage();
$tokenStorage->setToken($token);
$exportManager = new ExportManager(
new NullLogger(),
$security->reveal(),
$this->prophesize(AuthorizationHelperInterface::class)->reveal(),
$tokenStorage,
['my_export' => $export->reveal()],
['my_aggregator' => $myAggregator->reveal()],
[
'my_filter_string' => $myFilterString->reveal(),
'my_filter_array' => $myFilterArray->reveal(),
'my_filter_translatable' => $myFilterTranslatable->reveal(),
],
[],
);
$exportConfigProcessor = new ExportConfigProcessor($exportManager);
$translator = $this->prophesize(TranslatorInterface::class);
$translator->trans('Title')->shouldBeCalled()->willReturn('Title');
$translator->trans($string0)->shouldBeCalled()->willReturn($string0);
$translator->trans($string1, $arg1)->shouldBeCalled()->willReturn($string1);
$translator->trans('Some aggregator')->shouldBeCalled()->willReturn('Some aggregator');
$exportDescriptionHelper = new ExportDescriptionHelper(
$exportManager,
$exportConfigNormalizer->reveal(),
$exportConfigProcessor,
$translator->reveal(),
$security->reveal(),
);
$actual = $exportDescriptionHelper->describe('my_export', $options);
self::assertIsArray($actual);
self::assertEquals($actual[0], 'Title');
self::assertEquals($actual[1], 'This is a filter description');
self::assertEquals($actual[2], 'This is a filter with %argument%');
self::assertEquals($actual[3], 'translatable');
self::assertEquals($actual[4], 'Some aggregator');
}
}

View File

@@ -0,0 +1,413 @@
<?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\Export;
use Chill\MainBundle\Entity\Center;
use Chill\MainBundle\Entity\Regroupment;
use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Export\AggregatorInterface;
use Chill\MainBundle\Export\DirectExportInterface;
use Chill\MainBundle\Export\ExportConfigNormalizer;
use Chill\MainBundle\Export\ExportConfigProcessor;
use Chill\MainBundle\Export\ExportGenerationContext;
use Chill\MainBundle\Export\ExportGenerator;
use Chill\MainBundle\Export\ExportInterface;
use Chill\MainBundle\Export\ExportManager;
use Chill\MainBundle\Export\FilterInterface;
use Chill\MainBundle\Export\FormattedExportGeneration;
use Chill\MainBundle\Export\FormatterInterface;
use Chill\MainBundle\Repository\CenterRepositoryInterface;
use Chill\MainBundle\Security\Authorization\AuthorizationHelperInterface;
use Chill\MainBundle\Service\Regroupement\CenterRegroupementResolver;
use Doctrine\ORM\NativeQuery;
use Doctrine\ORM\Query;
use Doctrine\ORM\QueryBuilder;
use PHPUnit\Framework\TestCase;
use Prophecy\Argument;
use Prophecy\PhpUnit\ProphecyTrait;
use Psr\Log\NullLogger;
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBag;
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
/**
* @internal
*
* @coversNothing
*/
class ExportGeneratorTest extends TestCase
{
use ProphecyTrait;
private function buildParameter(bool $filterStat): ParameterBagInterface
{
return new ParameterBag(
['chill_main' => ['acl' => ['filter_stats_by_center' => $filterStat]]]
);
}
public function testGenerateHappyScenario()
{
$initialData = ['initial' => 'test'];
$fullConfig = [
'export' => $formExportData = ['key' => 'form1'],
'filters' => [
'dummy_filter' => ['enabled' => true, 'form' => $formFilterData = ['key' => 'form2']],
'disabled_filter' => ['enabled' => false],
],
'aggregators' => [
'dummy_aggregator' => ['enabled' => true, 'form' => $formAggregatorData = ['key' => 'form3']],
'disabled_aggregator' => ['enabled' => false],
],
'pick_formatter' => 'xlsx',
'formatter' => $formatterData = ['key' => 'form4'],
'centers' => ['centers' => [$centerA = new Center()], 'regroupments' => [(new Regroupment())->addCenter($centerB = new Center())]],
];
$user = new User();
$export = $this->prophesize(ExportInterface::class);
$filter = $this->prophesize(FilterInterface::class);
$filter->applyOn()->willReturn('tagada');
$aggregator = $this->prophesize(AggregatorInterface::class);
$aggregator->applyOn()->willReturn('tsointsoin');
$formatter = $this->prophesize(FormatterInterface::class);
$query = $this->prophesize(QueryBuilder::class);
$query->getDQL()->willReturn('dummy');
$dqlQuery = $this->prophesize(Query::class);
$dqlQuery->getSQL()->willReturn('dummy');
$query->getQuery()->willReturn($dqlQuery->reveal());
// required methods
$export->initiateQuery(
['tagada', 'tsointsoin'],
Argument::that(function ($arg) use ($centerB, $centerA) {
if (!is_array($arg)) {
return false;
}
if (2 !== count($arg)) {
return false;
}
foreach ($arg as $item) {
if ([] !== $item['circles']) {
return false;
}
if (!in_array($item['center'], [$centerA, $centerB], true)) {
return false;
}
}
return true;
}),
$formExportData,
Argument::that(static fn ($context) => $context instanceof ExportGenerationContext && $context->byUser === $user),
)->shouldBeCalled()->willReturn($query->reveal());
$export->getResult($query->reveal(), $formExportData, Argument::that(static fn (ExportGenerationContext $context) => $context->byUser === $user))
->shouldBeCalled()->willReturn([['result0' => '0']]);
$export->requiredRole()->willReturn('dummy_role');
$filter->alterQuery($query->reveal(), $formFilterData, Argument::that(static fn (ExportGenerationContext $context) => $context->byUser === $user))
->shouldBeCalled();
$aggregator->alterQuery($query->reveal(), $formAggregatorData, Argument::that(static fn (ExportGenerationContext $context) => $context->byUser === $user))
->shouldBeCalled();
$formatter->generate(
[['result0' => '0']],
$formatterData,
'dummy',
$formExportData,
['dummy_filter' => $formFilterData],
['dummy_aggregator' => $formAggregatorData],
Argument::that(static fn ($context) => $context instanceof ExportGenerationContext && $context->byUser === $user),
)
->shouldBeCalled()
->willReturn(new FormattedExportGeneration('export result', 'text/text'));
$exportConfigNormalizer = $this->prophesize(ExportConfigNormalizer::class);
$exportConfigNormalizer->denormalizeConfig('dummy', $initialData)->willReturn($fullConfig);
$exportManager = $this->prophesize(ExportManager::class);
$exportManager->getExport('dummy')->willReturn($export->reveal());
$exportManager->getFilter('dummy_filter')->willReturn($filter->reveal());
$exportManager->hasFilter('dummy_filter')->willReturn(true);
$exportManager->hasFilter('disabled_filter')->willReturn(true);
$exportManager->getAggregator('dummy_aggregator')->willReturn($aggregator->reveal());
$exportManager->hasAggregator('dummy_aggregator')->willReturn(true);
$exportManager->hasAggregator('disabled_aggregator')->willReturn(true);
$exportManager->getFormatter('xlsx')->willReturn($formatter->reveal());
$authorizationHelper = $this->prophesize(AuthorizationHelperInterface::class);
$authorizationHelper->getReachableCenters($user, 'dummy_role')->willReturn([$centerA, $centerB]);
$centerRepository = $this->prophesize(CenterRepositoryInterface::class);
$centerRepository->findActive()->shouldNotBeCalled();
$generator = new ExportGenerator(
$exportManager->reveal(),
$exportConfigNormalizer->reveal(),
new NullLogger(),
$authorizationHelper->reveal(),
new CenterRegroupementResolver(),
new ExportConfigProcessor($exportManager->reveal()),
$this->buildParameter(true),
$centerRepository->reveal(),
);
$actual = $generator->generate('dummy', $initialData, $user);
self::assertEquals('export result', $actual->content);
self::assertEquals('text/text', $actual->contentType);
}
public function testGenerateNativeSqlHappyScenario()
{
$initialData = ['initial' => 'test'];
$fullConfig = [
'export' => $formExportData = ['key' => 'form1'],
'filters' => [],
'aggregators' => [],
'pick_formatter' => 'xlsx',
'formatter' => $formatterData = ['key' => 'form4'],
'centers' => ['centers' => [$centerA = new Center(), $centerB = new Center()], 'regroupments' => []],
];
$user = new User();
$export = $this->prophesize(ExportInterface::class);
$formatter = $this->prophesize(FormatterInterface::class);
$query = $this->prophesize(NativeQuery::class);
// required methods
$export->initiateQuery(
[],
[['center' => $centerA, 'circles' => []], ['center' => $centerB, 'circles' => []]],
['key' => 'form1'],
Argument::that(static fn ($context) => $context instanceof ExportGenerationContext && $context->byUser === $user),
)->shouldBeCalled()->willReturn($query->reveal());
$export->getResult($query->reveal(), $formExportData, Argument::that(static fn (ExportGenerationContext $context) => $context->byUser === $user))
->shouldBeCalled()->willReturn([['result0' => '0']]);
$export->supportsModifiers()->willReturn([]);
$export->requiredRole()->willReturn('dummy_role');
$formatter->generate(
[['result0' => '0']],
$formatterData,
'dummy',
$formExportData,
[],
[],
Argument::that(static fn ($context) => $context instanceof ExportGenerationContext && $context->byUser === $user),
)
->shouldBeCalled()
->willReturn(new FormattedExportGeneration('export result', 'text/text'));
$exportConfigNormalizer = $this->prophesize(ExportConfigNormalizer::class);
$exportConfigNormalizer->denormalizeConfig('dummy', $initialData)->willReturn($fullConfig);
$exportManager = $this->prophesize(ExportManager::class);
$exportManager->getExport('dummy')->willReturn($export->reveal());
$exportManager->getFormatter('xlsx')->willReturn($formatter->reveal());
$authorizationHelper = $this->prophesize(AuthorizationHelperInterface::class);
$authorizationHelper->getReachableCenters($user, 'dummy_role')->willReturn([$centerA, $centerB]);
$centerRepository = $this->prophesize(CenterRepositoryInterface::class);
$centerRepository->findActive()->shouldNotBeCalled();
$generator = new ExportGenerator(
$exportManager->reveal(),
$exportConfigNormalizer->reveal(),
new NullLogger(),
$authorizationHelper->reveal(),
new CenterRegroupementResolver(),
new ExportConfigProcessor($exportManager->reveal()),
$this->buildParameter(true),
$centerRepository->reveal(),
);
$actual = $generator->generate('dummy', $initialData, $user);
self::assertInstanceOf(FormattedExportGeneration::class, $actual);
self::assertEquals('export result', $actual->content);
self::assertEquals('text/text', $actual->contentType);
}
public function testGenerateDirectExportHappyScenario()
{
$initialData = ['initial' => 'test'];
$fullConfig = [
'export' => $formExportData = ['key' => 'form1'],
'filters' => [],
'aggregators' => [],
'pick_formatter' => 'xlsx',
'formatter' => ['form' => $formatterData = ['key' => 'form4']],
'centers' => ['centers' => [$centerA = new Center(), $centerB = new Center()], 'regroupments' => []],
];
$user = new User();
$export = $this->prophesize(DirectExportInterface::class);
// required methods
$export->generate(
[['center' => $centerA, 'circles' => []], ['center' => $centerB, 'circles' => []]],
['key' => 'form1'],
Argument::that(static fn (ExportGenerationContext $context) => $user === $context->byUser),
)->shouldBeCalled()
->willReturn(new FormattedExportGeneration('export result', 'text/text'));
$export->requiredRole()->willReturn('dummy_role');
$exportConfigNormalizer = $this->prophesize(ExportConfigNormalizer::class);
$exportConfigNormalizer->denormalizeConfig('dummy', $initialData)->willReturn($fullConfig);
$exportManager = $this->prophesize(ExportManager::class);
$exportManager->getExport('dummy')->willReturn($export->reveal());
$authorizationHelper = $this->prophesize(AuthorizationHelperInterface::class);
$authorizationHelper->getReachableCenters($user, 'dummy_role')->willReturn([$centerA, $centerB]);
$centerRepository = $this->prophesize(CenterRepositoryInterface::class);
$centerRepository->findActive()->shouldNotBeCalled();
$generator = new ExportGenerator(
$exportManager->reveal(),
$exportConfigNormalizer->reveal(),
new NullLogger(),
$authorizationHelper->reveal(),
new CenterRegroupementResolver(),
new ExportConfigProcessor($exportManager->reveal()),
$this->buildParameter(true),
$centerRepository->reveal(),
);
$actual = $generator->generate('dummy', $initialData, $user);
self::assertInstanceOf(FormattedExportGeneration::class, $actual);
self::assertEquals('export result', $actual->content);
self::assertEquals('text/text', $actual->contentType);
}
public function testGenerateHappyScenarioWithoutCenterFiltering()
{
$initialData = ['initial' => 'test'];
$fullConfig = [
'export' => $formExportData = ['key' => 'form1'],
'filters' => [
'dummy_filter' => ['enabled' => true, 'form' => $formFilterData = ['key' => 'form2']],
'disabled_filter' => ['enabled' => false],
],
'aggregators' => [
'dummy_aggregator' => ['enabled' => true, 'form' => $formAggregatorData = ['key' => 'form3']],
'disabled_aggregator' => ['enabled' => false],
],
'pick_formatter' => 'xlsx',
'formatter' => $formatterData = ['key' => 'form4'],
'centers' => ['centers' => [], 'regroupments' => []],
];
$user = new User();
$centerA = new Center();
$centerB = new Center();
$export = $this->prophesize(ExportInterface::class);
$filter = $this->prophesize(FilterInterface::class);
$filter->applyOn()->willReturn('tagada');
$aggregator = $this->prophesize(AggregatorInterface::class);
$aggregator->applyOn()->willReturn('tsointsoin');
$formatter = $this->prophesize(FormatterInterface::class);
$query = $this->prophesize(QueryBuilder::class);
$query->getDQL()->willReturn('dummy');
$dqlQuery = $this->prophesize(Query::class);
$dqlQuery->getSQL()->willReturn('dummy');
$query->getQuery()->willReturn($dqlQuery->reveal());
// required methods
$export->initiateQuery(
['tagada', 'tsointsoin'],
Argument::that(function ($arg) use ($centerB, $centerA) {
if (!is_array($arg)) {
return false;
}
if (2 !== count($arg)) {
return false;
}
foreach ($arg as $item) {
if ([] !== $item['circles']) {
return false;
}
if (!in_array($item['center'], [$centerA, $centerB], true)) {
return false;
}
}
return true;
}),
['key' => 'form1'],
Argument::that(static fn ($context) => $context instanceof ExportGenerationContext && $context->byUser === $user),
)->shouldBeCalled()->willReturn($query->reveal());
$export->getResult($query->reveal(), $formExportData, Argument::that(static fn (ExportGenerationContext $context) => $context->byUser === $user))
->shouldBeCalled()->willReturn([['result0' => '0']]);
$export->requiredRole()->willReturn('dummy_role');
$filter->alterQuery($query->reveal(), $formFilterData, Argument::that(static fn (ExportGenerationContext $context) => $context->byUser === $user))
->shouldBeCalled();
$aggregator->alterQuery($query->reveal(), $formAggregatorData, Argument::that(static fn (ExportGenerationContext $context) => $context->byUser === $user))
->shouldBeCalled();
$formatter->generate(
[['result0' => '0']],
$formatterData,
'dummy',
$formExportData,
['dummy_filter' => $formFilterData],
['dummy_aggregator' => $formAggregatorData],
Argument::that(static fn ($context) => $context instanceof ExportGenerationContext && $context->byUser === $user),
)
->shouldBeCalled()
->willReturn(new FormattedExportGeneration('export result', 'text/text'));
$exportConfigNormalizer = $this->prophesize(ExportConfigNormalizer::class);
$exportConfigNormalizer->denormalizeConfig('dummy', $initialData)->willReturn($fullConfig);
$exportManager = $this->prophesize(ExportManager::class);
$exportManager->getExport('dummy')->willReturn($export->reveal());
$exportManager->getFilter('dummy_filter')->willReturn($filter->reveal());
$exportManager->hasFilter('dummy_filter')->willReturn(true);
$exportManager->hasFilter('disabled_filter')->willReturn(true);
$exportManager->getAggregator('dummy_aggregator')->willReturn($aggregator->reveal());
$exportManager->hasAggregator('dummy_aggregator')->willReturn(true);
$exportManager->hasAggregator('disabled_aggregator')->willReturn(true);
$exportManager->getFormatter('xlsx')->willReturn($formatter->reveal());
$authorizationHelper = $this->prophesize(AuthorizationHelperInterface::class);
$authorizationHelper->getReachableCenters($user, 'dummy_role')->shouldNotBeCalled();
$centerRepository = $this->prophesize(CenterRepositoryInterface::class);
$centerRepository->findActive()->willReturn([$centerA, $centerB])->shouldBeCalled();
$generator = new ExportGenerator(
$exportManager->reveal(),
$exportConfigNormalizer->reveal(),
new NullLogger(),
$authorizationHelper->reveal(),
new CenterRegroupementResolver(),
new ExportConfigProcessor($exportManager->reveal()),
$this->buildParameter(false),
$centerRepository->reveal(),
);
$actual = $generator->generate('dummy', $initialData, $user);
self::assertEquals('export result', $actual->content);
self::assertEquals('text/text', $actual->contentType);
}
}

View File

@@ -14,10 +14,10 @@ namespace Chill\MainBundle\Tests\Export;
use Chill\MainBundle\Entity\Center;
use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Export\AggregatorInterface;
use Chill\MainBundle\Export\ExportGenerationContext;
use Chill\MainBundle\Export\ExportInterface;
use Chill\MainBundle\Export\ExportManager;
use Chill\MainBundle\Export\FilterInterface;
use Chill\MainBundle\Form\Type\Export\ExportType;
use Chill\MainBundle\Repository\UserRepositoryInterface;
use Chill\MainBundle\Security\Authorization\AuthorizationHelper;
use Chill\MainBundle\Test\PrepareCenterTrait;
@@ -30,12 +30,10 @@ use Prophecy\Prophet;
use Psr\Log\LoggerInterface;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorage;
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
/**
* Test the export manager.
@@ -194,176 +192,6 @@ final class ExportManagerTest extends KernelTestCase
$this->assertNotContains($formatterBar->reveal(), $obtained);
}
/**
* Test the generation of an export.
*/
public function testGenerate()
{
$center = $this->prepareCenter(100, 'center');
$user = $this->prepareUser([]);
$authorizationChecker = $this->prophet->prophesize();
$authorizationChecker->willImplement(AuthorizationCheckerInterface::class);
$authorizationChecker->isGranted('CHILL_STAT_DUMMY', $center)
->willReturn(true);
$exports = [];
$filters = [];
$aggregators = [];
$em = self::getContainer()->get(EntityManagerInterface::class);
$export = $this->prophet->prophesize();
$export->willImplement(ExportInterface::class);
$export->initiateQuery(
Argument::is(['foo']),
Argument::Type('array'),
Argument::is(['a' => 'b'])
)
->will(static function () use ($em) {
$qb = $em->createQueryBuilder();
return $qb->addSelect('COUNT(user.id) as export')
->from(User::class, 'user');
});
$export->initiateQuery(
Argument::is(['foo']),
Argument::Type('array'),
Argument::is(['a' => 'b'])
)->shouldBeCalled();
$export->supportsModifiers()->willReturn(['foo']);
$export->requiredRole()->willReturn('CHILL_STAT_DUMMY');
$export->getResult(Argument::Type(QueryBuilder::class), Argument::Type('array'))->willReturn([
[
'aggregator' => 'cat a',
'export' => 0,
],
[
'aggregator' => 'cat b',
'export' => 1,
],
]);
$export->getLabels(
Argument::is('export'),
Argument::is([0, 1]),
Argument::Type('array')
)
->willReturn(static function ($value) {
switch ($value) {
case 0:
case 1:
return $value;
case '_header':
return 'export';
default: throw new \RuntimeException(sprintf('The value %s is not valid', $value));
}
});
$export->getQueryKeys(Argument::Type('array'))->willReturn(['export']);
$export->getTitle()->willReturn('dummy title');
$exports['dummy'] = $export->reveal();
$filter = $this->prophet->prophesize();
$filter->willImplement(FilterInterface::class);
$filter->alterQuery(Argument::Type(QueryBuilder::class), Argument::Type('array'))
->willReturn(null);
$filter->alterQuery(Argument::Type(QueryBuilder::class), Argument::Type('array'))
->shouldBeCalled();
$filter->addRole()->shouldBeCalled();
$filter->addRole()->willReturn(null);
$filter->applyOn()->willReturn('foo');
$filter->describeAction(Argument::cetera())->willReturn('filtered string');
$filters['filter_foo'] = $filter->reveal();
$aggregator = $this->prophet->prophesize();
$aggregator->willImplement(AggregatorInterface::class);
$aggregator->addRole()->willReturn(null);
$aggregator->applyOn()->willReturn('foo');
$aggregator->alterQuery(Argument::Type(QueryBuilder::class), Argument::Type('array'))
->willReturn(null);
$aggregator->alterQuery(Argument::Type(QueryBuilder::class), Argument::Type('array'))
->shouldBeCalled();
$aggregator->getQueryKeys(Argument::Type('array'))->willReturn(['aggregator']);
$aggregator->getLabels(
Argument::is('aggregator'),
Argument::is(['cat a', 'cat b']),
Argument::is([])
)
->willReturn(static fn ($value) => match ($value) {
'_header' => 'foo_header',
'cat a' => 'label cat a',
'cat b' => 'label cat b',
default => throw new \RuntimeException(sprintf('This value (%s) is not valid', $value)),
});
$aggregator->addRole()->willReturn(null);
$aggregator->addRole()->shouldBeCalled();
$aggregators['aggregator_foo'] = $aggregator->reveal();
$exportManager = $this->createExportManager(
null,
null,
$authorizationChecker->reveal(),
null,
$user,
$exports,
$aggregators,
$filters
);
// add formatter interface
$formatter = new \Chill\MainBundle\Export\Formatter\SpreadSheetFormatter(
self::getContainer()->get(TranslatorInterface::class),
$exportManager
);
$exportManager->addFormatter($formatter, 'spreadsheet');
$response = $exportManager->generate(
'dummy',
[$center],
[
ExportType::FILTER_KEY => [
'filter_foo' => [
'enabled' => true,
'form' => [],
],
],
ExportType::AGGREGATOR_KEY => [
'aggregator_foo' => [
'enabled' => true,
'form' => [],
],
],
ExportType::PICK_FORMATTER_KEY => [
'alias' => 'spreadsheet',
],
ExportType::EXPORT_KEY => [
'a' => 'b',
],
],
[
'format' => 'csv',
'aggregator_foo' => [
'order' => 1,
],
]
);
$this->assertInstanceOf(Response::class, $response);
$expected = <<<'EOT'
"dummy title",""
"",""
"filtered string",""
"foo_header","export"
"label cat a","0"
"label cat b","1"
EOT;
$this->assertEquals($expected, $response->getContent());
}
public function testIsGrantedForElementWithExportAndUserIsGranted()
{
$center = $this->prepareCenter(100, 'center A');
@@ -506,6 +334,7 @@ final class ExportManagerTest extends KernelTestCase
array $exports = [],
array $aggregators = [],
array $filters = [],
array $formatters = [],
): ExportManager {
$localUser = $user ?? self::getContainer()->get(
UserRepositoryInterface::class
@@ -516,13 +345,14 @@ final class ExportManagerTest extends KernelTestCase
$tokenStorage->setToken($token);
return new ExportManager(
$logger ?? self::getContainer()->get('logger'),
$logger ?? self::getContainer()->get(LoggerInterface::class),
$authorizationChecker ?? self::getContainer()->get('security.authorization_checker'),
$authorizationHelper ?? self::getContainer()->get('chill.main.security.authorization.helper'),
$tokenStorage,
$exports,
$aggregators,
$filters
$filters,
$formatters,
);
}
}
@@ -534,19 +364,34 @@ class DummyFilterWithApplying implements FilterInterface
private readonly string $applyOn,
) {}
public function getTitle()
public function getTitle(): string|\Symfony\Contracts\Translation\TranslatableInterface
{
return 'dummy';
}
public function buildForm(FormBuilderInterface $builder) {}
public function buildForm(FormBuilderInterface $builder): void {}
public function getNormalizationVersion(): int
{
return 1;
}
public function normalizeFormData(array $formData): array
{
return [];
}
public function denormalizeFormData(array $formData, int $fromVersion): array
{
return [];
}
public function getFormDefaultData(): array
{
return [];
}
public function describeAction($data, $format = 'string')
public function describeAction($data, ExportGenerationContext $context): string|\Symfony\Contracts\Translation\TranslatableInterface|array
{
return ['dummy filter', []];
}
@@ -556,9 +401,9 @@ class DummyFilterWithApplying implements FilterInterface
return $this->role;
}
public function alterQuery(QueryBuilder $qb, $data) {}
public function alterQuery(QueryBuilder $qb, $data, ExportGenerationContext $exportGenerationContext): void {}
public function applyOn()
public function applyOn(): string
{
return $this->applyOn;
}
@@ -574,13 +419,28 @@ class DummyExport implements ExportInterface
private readonly array $supportedModifiers,
) {}
public function getTitle()
public function getTitle(): string|\Symfony\Contracts\Translation\TranslatableInterface
{
return 'dummy';
}
public function buildForm(FormBuilderInterface $builder) {}
public function getNormalizationVersion(): int
{
return 1;
}
public function normalizeFormData(array $formData): array
{
return [];
}
public function denormalizeFormData(array $formData, int $fromVersion): array
{
return [];
}
public function getFormDefaultData(): array
{
return [];
@@ -601,24 +461,24 @@ class DummyExport implements ExportInterface
return [];
}
public function getQueryKeys($data)
public function getQueryKeys($data): array
{
return [];
}
public function getResult($query, $data)
public function getResult($query, $data, ExportGenerationContext $context): array
{
return [];
}
public function getType()
public function getType(): string
{
return 'dummy';
}
public function initiateQuery(array $requiredModifiers, array $acl, array $data = [])
public function initiateQuery(array $requiredModifiers, array $acl, array $data, ExportGenerationContext $context): QueryBuilder
{
return null;
throw new \RuntimeException('not implemented');
}
public function requiredRole(): string

View File

@@ -0,0 +1,138 @@
<?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\Export\Formatter;
use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Export\ExportGenerationContext;
use Chill\MainBundle\Export\ExportInterface;
use Chill\MainBundle\Export\ExportManager;
use Chill\MainBundle\Export\Formatter\SpreadSheetFormatter;
use Chill\MainBundle\Service\RollingDate\RollingDate;
use PhpOffice\PhpSpreadsheet\IOFactory;
use PHPUnit\Framework\TestCase;
use Prophecy\PhpUnit\ProphecyTrait;
use Symfony\Component\Translation\TranslatableMessage;
/**
* @internal
*
* @coversNothing
*/
class SpreadsheetFormatterTest extends TestCase
{
use ProphecyTrait;
public function testGenerate(): void
{
$translator = $this->prophesize(\Symfony\Contracts\Translation\TranslatorInterface::class);
$translator->getLocale()->willReturn('en');
$exportManager = $this->prophesize(ExportManager::class);
$result =
[
['export_count_activity' => 1, 'person_age' => 65, 'aggregator_some' => 'label0'], // row 0
];
$exportAlias = 'count_activity_linked_to_person';
$formatterData =
['format' => 'xlsx', 'person_age_aggregator' => ['order' => 1], 'aggregator2' => ['order' => 2]];
$exportData = [];
$filtersData =
[
'person_age_filter' => ['min_age' => 18, 'max_age' => 120, 'date_calc' => new RollingDate(RollingDate::T_TODAY)],
'filter2' => [],
];
$aggregatorsData =
[
'person_age_aggregator' => ['date_age_calculation' => new RollingDate(RollingDate::T_TODAY)],
'aggregator2' => [],
];
$context =
new ExportGenerationContext($user = new User());
$export = $this->prophesize(ExportInterface::class);
$export->getTitle()->willReturn('Count activity linked to person');
$translator->trans('Count activity linked to person')->willReturn('Count activity linked to person');
$export->getQueryKeys($exportData)->willReturn(['export_count_activity']);
$export->getLabels('export_count_activity', [1], $exportData)
->willReturn(fn (int|string $value): int|string => '_header' === $value ? 'Count activities' : $value);
$translator->trans('Count activities')->willReturn('Count activities');
$exportManager->getExport($exportAlias)->willReturn($export->reveal());
$aggregator = $this->prophesize(\Chill\MainBundle\Export\AggregatorInterface::class);
$aggregator->getTitle()->willReturn('Person age');
$aggregator->getQueryKeys($aggregatorsData['person_age_aggregator'])->willReturn(['person_age']);
$aggregator->getLabels('person_age', [65], $aggregatorsData['person_age_aggregator'])
->willReturn(fn (int|string $value): int|string => '_header' === $value ? 'Group by age' : $value);
$translator->trans('Group by age')->willReturn('Group by age');
$exportManager->getAggregator('person_age_aggregator')->willReturn($aggregator->reveal());
$aggregator2 = $this->prophesize(\Chill\MainBundle\Export\AggregatorInterface::class);
$aggregator2->getTitle()->willReturn(new TranslatableMessage('Some'));
$aggregator2->getQueryKeys($aggregatorsData['aggregator2'])->willReturn(['aggregator_some']);
$aggregator2->getLabels('aggregator_some', ['label0'], $aggregatorsData['aggregator2'])
->willReturn(fn (int|string $value): TranslatableMessage => new TranslatableMessage('_header' === $value ? 'Aggregator 2 header' : $value));
$translator->trans('Aggregator 2 header', [], null, 'en')->willReturn('Aggregator 2 header');
$translator->trans('label0', [], null, 'en')->willReturn('label0');
$exportManager->getAggregator('aggregator2')->willReturn($aggregator2->reveal());
$filter = $this->prophesize(\Chill\MainBundle\Export\FilterInterface::class);
$filter->getTitle()->willReturn('Person by age');
$filter->describeAction($filtersData['person_age_filter'], $context)
->willReturn(['Filter by age, from {{ start }} to {{ end }}', ['{{ start }}' => '18', '{{ end }}' => '120']]);
$translator->trans('Filter by age, from {{ start }} to {{ end }}', ['{{ start }}' => '18', '{{ end }}' => '120'])
->willReturn('Filter by age, from 18 to 120');
$exportManager->getFilter('person_age_filter')->willReturn($filter->reveal());
$filter2 = $this->prophesize(\Chill\MainBundle\Export\FilterInterface::class);
$filter2->getTitle()->willReturn(new TranslatableMessage('Some other filter'));
$filter2->describeAction($filtersData['filter2'], $context)
->willReturn(new TranslatableMessage('Other filter description'));
$translator->trans('Other filter description', [], null, 'en')
->willReturn('Some other filter description');
$exportManager->getFilter('filter2')->willReturn($filter2->reveal());
// create the formatter
$formatter = new SpreadSheetFormatter($translator->reveal());
$formatter->setExportManager($exportManager->reveal());
$result = $formatter->generate(
$result,
$formatterData,
$exportAlias,
$exportData,
$filtersData,
$aggregatorsData,
$context,
);
$tempFile = tempnam(sys_get_temp_dir(), 'test_spreadsheet_formatter_');
file_put_contents($tempFile, $result->content);
$spreadsheet = IOFactory::load($tempFile);
$cells = $spreadsheet->getActiveSheet()->rangeToArray(
'A1:G6',
null,
false,
true,
true,
);
unlink($tempFile);
self::assertEquals('application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', $result->contentType);
self::assertEquals($cells[1], ['A' => 'Count activity linked to perso…', 'B' => null, 'C' => null, 'D' => null, 'E' => null, 'F' => null, 'G' => null]);
self::assertEquals($cells[2], ['A' => null, 'B' => null, 'C' => null, 'D' => null, 'E' => null, 'F' => null, 'G' => null]);
self::assertEquals($cells[3], ['A' => 'Filter by age, from 18 to 120', 'B' => null, 'C' => null, 'D' => null, 'E' => null, 'F' => null, 'G' => null]);
self::assertEquals($cells[4], ['A' => 'Some other filter description', 'B' => null, 'C' => null, 'D' => null, 'E' => null, 'F' => null, 'G' => null]);
self::assertEquals($cells[5], ['A' => 'Group by age', 'B' => 'Aggregator 2 header', 'C' => 'Count activities', 'D' => null, 'E' => null, 'F' => null, 'G' => null]);
self::assertEquals($cells[6], ['A' => 65, 'B' => 'label0', 'C' => 1, 'D' => null, 'E' => null, 'F' => null, 'G' => null]);
}
}

View File

@@ -0,0 +1,62 @@
<?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\Export\Messenger;
use Chill\DocStoreBundle\Entity\StoredObject;
use Chill\MainBundle\Entity\ExportGeneration;
use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Export\Exception\ExportGenerationException;
use Chill\MainBundle\Export\Messenger\ExportRequestGenerationMessage;
use Chill\MainBundle\Export\Messenger\OnExportGenerationFails;
use Chill\MainBundle\Repository\ExportGenerationRepository;
use Doctrine\ORM\EntityManagerInterface;
use PHPUnit\Framework\TestCase;
use Prophecy\PhpUnit\ProphecyTrait;
use Psr\Log\NullLogger;
use Symfony\Component\Messenger\Envelope;
use Symfony\Component\Messenger\Event\WorkerMessageFailedEvent;
/**
* @internal
*
* @coversNothing
*/
class OnExportGenerationFailsTest extends TestCase
{
use ProphecyTrait;
public function testOnExportGenerationFails(): void
{
$exportGeneration = new ExportGeneration('dummy');
$exportGeneration->setCreatedAt(new \DateTimeImmutable('10 seconds ago'));
$repository = $this->prophesize(ExportGenerationRepository::class);
$repository->find($exportGeneration->getId())->willReturn($exportGeneration);
$entityManager = $this->prophesize(EntityManagerInterface::class);
$entityManager->flush()->shouldBeCalled();
$user = $this->prophesize(User::class);
$user->getId()->willReturn(1);
$subscriber = new OnExportGenerationFails(new NullLogger(), $repository->reveal(), $entityManager->reveal());
$subscriber->onMessageFailed(new WorkerMessageFailedEvent(
new Envelope(new ExportRequestGenerationMessage($exportGeneration, $user->reveal())),
'dummyReceiver',
new ExportGenerationException('dummy_exception'),
));
self::assertEquals(StoredObject::STATUS_FAILURE, $exportGeneration->getStoredObject()->getStatus());
self::assertStringContainsString('dummy_exception', $exportGeneration->getStoredObject()->getGenerationErrors());
}
}

View File

@@ -0,0 +1,76 @@
<?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\Export\Messenger;
use Chill\MainBundle\Entity\ExportGeneration;
use Chill\MainBundle\Export\Messenger\RemoveExportGenerationMessage;
use Chill\MainBundle\Export\Messenger\RemoveExportGenerationMessageHandler;
use Chill\MainBundle\Repository\ExportGenerationRepository;
use Doctrine\ORM\EntityManagerInterface;
use PHPUnit\Framework\TestCase;
use Prophecy\PhpUnit\ProphecyTrait;
use Psr\Log\NullLogger;
use Symfony\Component\Clock\MockClock;
/**
* @internal
*
* @coversNothing
*/
class RemoveExportGenerationMessageHandlerTest extends TestCase
{
use ProphecyTrait;
public function testInvokeUpdatesDeleteAtAndRemovesAndFlushes()
{
// Arrange
// 1. Create a MockClock at a fixed point in time
$now = new \DateTimeImmutable('2024-06-01T12:00:00');
$clock = new MockClock($now);
// 2. Create an ExportGeneration entity with a stored object
$exportGeneration = new ExportGeneration('test-alias', ['foo' => 'bar']);
$storedObject = $exportGeneration->getStoredObject();
// 3. Mock ExportGenerationRepository to return the ExportGeneration
$exportGenerationRepository = $this->prophesize(ExportGenerationRepository::class);
$exportGenerationRepository
->find($exportGeneration->getId())
->willReturn($exportGeneration);
// 4. Mock EntityManagerInterface and set expectations
$entityManager = $this->prophesize(EntityManagerInterface::class);
$entityManager->remove($exportGeneration)->shouldBeCalled();
$entityManager->flush()->shouldBeCalled();
// 6. Create message
$message = new RemoveExportGenerationMessage($exportGeneration);
// 7. Handler instantiation
$handler = new RemoveExportGenerationMessageHandler(
$exportGenerationRepository->reveal(),
$entityManager->reveal(),
new NullLogger(),
$clock
);
// Pre-condition: deleteAt not set.
$this->assertNull($storedObject->getDeleteAt());
// Act
$handler->__invoke($message);
// Assert
$this->assertEquals($now, $storedObject->getDeleteAt(), 'deleteAt of stored object was updated');
}
}

File diff suppressed because one or more lines are too long

View File

@@ -12,12 +12,14 @@ declare(strict_types=1);
namespace Chill\MainBundle\Tests\Export;
use Chill\MainBundle\Export\AggregatorInterface;
use Chill\MainBundle\Export\ExportGenerationContext;
use Chill\MainBundle\Export\ExportManager;
use Chill\MainBundle\Export\FilterInterface;
use Chill\MainBundle\Export\SortExportElement;
use Doctrine\ORM\QueryBuilder;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Contracts\Translation\TranslatableInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
/**
@@ -48,6 +50,10 @@ class SortExportElementTest extends KernelTestCase
$previousName = null;
foreach ($filters as $filter) {
if ($filter->getTitle() instanceof TranslatableInterface) {
continue;
}
if (null === $previousName) {
$previousName = $translator->trans($filter->getTitle());
continue;
@@ -119,24 +125,39 @@ class SortExportElementTest extends KernelTestCase
return new class ($title) implements AggregatorInterface {
public function __construct(private readonly string $title) {}
public function buildForm(FormBuilderInterface $builder) {}
public function buildForm(FormBuilderInterface $builder): void {}
public function getNormalizationVersion(): int
{
return 1;
}
public function normalizeFormData(array $formData): array
{
return [];
}
public function denormalizeFormData(array $formData, int $fromVersion): array
{
return [];
}
public function getFormDefaultData(): array
{
return [];
}
public function getLabels($key, array $values, mixed $data)
public function getLabels($key, array $values, mixed $data): callable
{
return fn ($v) => $v;
}
public function getQueryKeys($data)
public function getQueryKeys($data): array
{
return [];
}
public function getTitle()
public function getTitle(): string|TranslatableInterface
{
return $this->title;
}
@@ -146,11 +167,11 @@ class SortExportElementTest extends KernelTestCase
return null;
}
public function alterQuery(QueryBuilder $qb, $data) {}
public function alterQuery(QueryBuilder $qb, $data, ExportGenerationContext $exportGenerationContext): void {}
public function applyOn()
public function applyOn(): string
{
return [];
return '';
}
};
}
@@ -160,19 +181,34 @@ class SortExportElementTest extends KernelTestCase
return new class ($title) implements FilterInterface {
public function __construct(private readonly string $title) {}
public function getTitle()
public function getTitle(): string|TranslatableInterface
{
return $this->title;
}
public function buildForm(FormBuilderInterface $builder) {}
public function buildForm(FormBuilderInterface $builder): void {}
public function getNormalizationVersion(): int
{
return 1;
}
public function normalizeFormData(array $formData): array
{
return [];
}
public function denormalizeFormData(array $formData, int $fromVersion): array
{
return [];
}
public function getFormDefaultData(): array
{
return [];
}
public function describeAction($data, $format = 'string')
public function describeAction($data, ExportGenerationContext $context): string|TranslatableInterface|array
{
return ['a', []];
}
@@ -182,11 +218,11 @@ class SortExportElementTest extends KernelTestCase
return null;
}
public function alterQuery(QueryBuilder $qb, $data) {}
public function alterQuery(QueryBuilder $qb, $data, ExportGenerationContext $exportGenerationContext): void {}
public function applyOn()
public function applyOn(): string
{
return [];
return '';
}
};
}

View File

@@ -0,0 +1,145 @@
<?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\Security\Authorization;
use Chill\MainBundle\Entity\SavedExport;
use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Entity\UserGroup;
use Chill\MainBundle\Export\ExportInterface;
use Chill\MainBundle\Export\ExportManager;
use Chill\MainBundle\Security\Authorization\SavedExportVoter;
use PHPUnit\Framework\TestCase;
use Prophecy\Argument;
use Prophecy\PhpUnit\ProphecyTrait;
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
use Symfony\Component\Security\Core\Authorization\AccessDecisionManagerInterface;
use Symfony\Component\Security\Core\Authorization\Voter\VoterInterface;
/**
* @internal
*
* @coversNothing
*/
class SavedExportVoterTest extends TestCase
{
use ProphecyTrait;
/**
* @dataProvider voteProvider
*/
public function testVote(string $attribute, mixed $savedExport, User $user, $expectedResult, ?bool $isGranted = null): void
{
$export = $this->prophesize(ExportInterface::class);
$exportManager = $this->prophesize(ExportManager::class);
$exportManager->getExport('dummy_export')->willReturn($export->reveal());
$exportManager->isGrantedForElement(Argument::any())->willReturn($isGranted);
$accessDecisionManager = $this->prophesize(AccessDecisionManagerInterface::class);
$voter = new SavedExportVoter($exportManager->reveal(), $accessDecisionManager->reveal());
$token = new UsernamePasswordToken($user, 'default', ['ROLE_USER']);
self::assertEquals($expectedResult, $voter->vote($token, $savedExport, [$attribute]));
}
public static function voteProvider(): iterable
{
$alls = [SavedExportVoter::GENERATE, SavedExportVoter::GENERATE, SavedExportVoter::EDIT, SavedExportVoter::DELETE];
$userA = new User();
$userB = new User();
$userC = new User();
$group = new UserGroup();
$group->addUser($userC);
$savedExport = new SavedExport();
$savedExport->setExportAlias('dummy_export');
$savedExport->setUser($userA);
// abstain
foreach ($alls as $attribute) {
yield [
$attribute,
new \stdClass(),
$userA,
VoterInterface::ACCESS_ABSTAIN,
true,
];
}
yield [
'dummy',
$savedExport,
$userA,
VoterInterface::ACCESS_ABSTAIN,
false,
];
foreach ($alls as $attribute) {
yield [
$attribute,
$savedExport,
$userA,
VoterInterface::ACCESS_GRANTED,
true,
];
}
yield [
SavedExportVoter::GENERATE,
$savedExport,
$userA,
VoterInterface::ACCESS_DENIED,
false,
];
foreach ($alls as $attribute) {
yield [
$attribute,
$savedExport,
$userB,
VoterInterface::ACCESS_DENIED,
true,
];
}
$savedExport = new SavedExport();
$savedExport->setExportAlias('dummy_export');
$savedExport->setUser($userA);
$savedExport->addShare($userB);
yield [
SavedExportVoter::GENERATE,
$savedExport,
$userB,
VoterInterface::ACCESS_DENIED,
false,
];
yield [
SavedExportVoter::GENERATE,
$savedExport,
$userB,
VoterInterface::ACCESS_GRANTED,
true,
];
foreach ([SavedExportVoter::EDIT, SavedExportVoter::DELETE] as $attribute) {
yield [
$attribute,
$savedExport,
$userB,
VoterInterface::ACCESS_DENIED,
true,
];
}
}
}

View File

@@ -0,0 +1,78 @@
<?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\Services\Regroupement;
use Chill\MainBundle\Entity\Center;
use Chill\MainBundle\Entity\Regroupment;
use Chill\MainBundle\Service\Regroupement\CenterRegroupementResolver;
use PHPUnit\Framework\TestCase;
/**
* @internal
*
* @coversNothing
*/
class CenterRegroupementResolverTest extends TestCase
{
private static CenterRegroupementResolver $resolver;
public static function setUpBeforeClass(): void
{
self::$resolver = new CenterRegroupementResolver();
}
/**
* @dataProvider provideData
*/
public function testResolveCenter(array $groups, array $centers, array $expected): void
{
$actual = self::$resolver->resolveCenters($groups, $centers);
self::assertEquals(count($expected), count($actual));
foreach ($expected as $center) {
self::assertContains($center, $actual);
}
}
public static function provideData(): iterable
{
$centerA = new Center();
$centerB = new Center();
$centerC = new Center();
$centerD = new Center();
$groupA = new Regroupment();
$groupA->addCenter($centerA)->addCenter($centerB);
$groupB = new Regroupment();
$groupB->addCenter($centerA)->addCenter($centerB)->addCenter($centerC);
yield [
[$groupA],
[],
[$centerA, $centerB],
];
yield [
[$groupA, $groupB],
[],
[$centerA, $centerB, $centerC],
];
yield [
[$groupA, $groupB],
[$centerB, $centerD],
[$centerA, $centerB, $centerC, $centerD],
];
}
}

View File

@@ -0,0 +1,96 @@
<?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\Services\Regroupement;
use Chill\MainBundle\Entity\Center;
use Chill\MainBundle\Entity\Regroupment;
use Chill\MainBundle\Service\Regroupement\RegroupementFiltering;
use PHPUnit\Framework\TestCase;
/**
* @internal
*
* @coversNothing
*/
class RegroupementFilteringTest extends TestCase
{
private static RegroupementFiltering $regroupementFiltering;
public static function setUpBeforeClass(): void
{
self::$regroupementFiltering = new RegroupementFiltering();
}
/**
* @dataProvider provideDataForFilterContainsAtLeastOnCenter
*/
public function testFilterContainsAtLeastOnCenter(array $groups, array $centers, array $expected): void
{
$actual = self::$regroupementFiltering->filterContainsAtLeastOneCenter($groups, $centers);
self::assertEquals(count($expected), count($actual));
self::assertTrue(array_is_list($actual));
foreach ($expected as $center) {
self::assertContains($center, $actual);
}
}
public static function provideDataForFilterContainsAtLeastOnCenter(): iterable
{
$centerA = new Center();
$centerB = new Center();
$centerC = new Center();
$centerD = new Center();
$groupA = new Regroupment();
$groupA->addCenter($centerA)->addCenter($centerB);
$groupB = new Regroupment();
$groupB->addCenter($centerA)->addCenter($centerB)->addCenter($centerC);
$groupC = new Regroupment();
$groupC->addCenter($centerA)->addCenter($centerD);
yield [
[$groupA, $groupB],
[],
[],
];
yield [
[$groupA, $groupB],
[$centerA, $centerB, $centerC],
[$groupA, $groupB],
];
yield [
[$groupA, $groupC],
[$centerD],
[$groupC],
];
yield [
[$groupA],
[$centerB, $centerD],
[$groupA],
];
yield [
[$groupA],
[new Center()],
[],
];
}
}

View File

@@ -14,6 +14,7 @@ namespace Services\RollingDate;
use Chill\MainBundle\Service\RollingDate\RollingDate;
use Chill\MainBundle\Service\RollingDate\RollingDateConverter;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Clock\MockClock;
/**
* @internal
@@ -22,11 +23,9 @@ use PHPUnit\Framework\TestCase;
*/
final class RollingDateConverterTest extends TestCase
{
private RollingDateConverter $converter;
protected function setUp(): void
private function buildConverter(\DateTimeImmutable|string $pivot = 'now'): RollingDateConverter
{
$this->converter = new RollingDateConverter();
return new RollingDateConverter(new MockClock($pivot));
}
public function testConversionFixedDate()
@@ -35,7 +34,7 @@ final class RollingDateConverterTest extends TestCase
$this->assertEquals(
'2022-01-01',
$this->converter->convert($rollingDate)->format('Y-m-d')
$this->buildConverter()->convert($rollingDate)->format('Y-m-d')
);
}
@@ -43,7 +42,7 @@ final class RollingDateConverterTest extends TestCase
{
$rollingDate = new RollingDate(RollingDate::T_YEAR_PREVIOUS_START);
$actual = $this->converter->convert($rollingDate);
$actual = $this->buildConverter()->convert($rollingDate);
$this->assertEquals(
(int) (new \DateTimeImmutable('now'))->format('Y') - 1,
@@ -63,7 +62,21 @@ final class RollingDateConverterTest extends TestCase
$this->assertEquals(
\DateTime::createFromFormat($format, $expectedDateTime),
$this->converter->convert($rollingDate)
$this->buildConverter()->convert($rollingDate)
);
}
/**
* @dataProvider generateDataConversionDate
*/
public function testConvertOnClock(string $roll, string $expectedDateTime, string $format)
{
$pivot = \DateTimeImmutable::createFromFormat('Y-m-d His', '2022-11-07 000000');
$rollingDate = new RollingDate($roll, null);
$this->assertEquals(
\DateTime::createFromFormat($format, $expectedDateTime),
$this->buildConverter($pivot)->convert($rollingDate)
);
}

View File

@@ -0,0 +1,45 @@
<?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\Services\RollingDate;
use Chill\MainBundle\Service\RollingDate\RollingDate;
use PHPUnit\Framework\TestCase;
/**
* @internal
*
* @coversNothing
*/
class RollingDateTest extends TestCase
{
public function testNormalizationDenormalizationProcessWithoutPivotDate(): void
{
$date = new RollingDate(RollingDate::T_YEAR_PREVIOUS_START);
$actual = RollingDate::fromNormalized($date->normalize());
self::assertEquals(RollingDate::T_YEAR_PREVIOUS_START, $actual->getRoll());
self::assertNull($actual->getFixedDate());
self::assertEquals($date->getPivotDate()?->getTimestamp(), $actual->getPivotDate()?->getTimestamp());
}
public function testNormalizationDenormalizationProcessWithPivotDate(): void
{
$date = new RollingDate(RollingDate::T_FIXED_DATE, $fixed = new \DateTimeImmutable('now'));
$actual = RollingDate::fromNormalized($date->normalize());
self::assertEquals(RollingDate::T_FIXED_DATE, $actual->getRoll());
self::assertEquals($fixed, $actual->getFixedDate());
self::assertEquals($date->getPivotDate()?->getTimestamp(), $actual->getPivotDate()?->getTimestamp());
}
}

View File

@@ -0,0 +1,58 @@
<?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\Services\UserGroup;
use Chill\MainBundle\Entity\CronJobExecution;
use Chill\MainBundle\Service\UserGroup\UserGroupRelatedToUserJobSyncCronJob;
use Chill\MainBundle\Service\UserGroup\UserGroupRelatedToUserJobSyncInterface;
use PHPUnit\Framework\TestCase;
use Prophecy\PhpUnit\ProphecyTrait;
use Symfony\Component\Clock\MockClock;
/**
* @internal
*
* @coversNothing
*/
class UserGroupRelatedToUserJobSyncCronJobTest extends TestCase
{
use ProphecyTrait;
/**
* @dataProvider canRunDataProvider
*/
public function testCanRun(\DateTimeImmutable $now, ?\DateTimeImmutable $lastStartExecution, bool $exected): void
{
$clock = new MockClock($now);
$job = $this->prophesize(UserGroupRelatedToUserJobSyncInterface::class);
$cronJob = new UserGroupRelatedToUserJobSyncCronJob($clock, $job->reveal());
if (null !== $lastStartExecution) {
$lastExecution = new CronJobExecution('user-group-related-to-user-job-sync');
$lastExecution->setLastStart($lastStartExecution);
}
$actual = $cronJob->canRun($lastExecution ?? null);
self::assertEquals($exected, $actual);
}
public static function canRunDataProvider(): iterable
{
$now = new \DateTimeImmutable('2025-04-27T00:00:00Z');
yield 'never executed' => [$now, null, true];
yield 'executed 12 hours ago' => [$now, new \DateTimeImmutable('2025-04-26T12:00:00Z'), false];
yield 'executed more than 12 hours ago' => [$now, new \DateTimeImmutable('2025-04-25T12:00:00Z'), true];
}
}