Sfoglia il codice sorgente

Merge branch 'V8-4743_fix_doctrine_time_filters' into develop

Olivier Massot 2 anni fa
parent
commit
044aef5566
32 ha cambiato i file con 896 aggiunte e 365 eliminazioni
  1. 4 4
      config/packages/doctrine.yaml
  2. 0 3
      config/services.yaml
  3. 1 1
      sql/schema-extensions/002-view_federation_structures.sql
  4. 2 2
      src/Entity/Access/Access.php
  5. 1 1
      src/Entity/Public/FederationStructure.php
  6. 1 1
      src/Entity/Public/PublicEvent.php
  7. 0 55
      src/EventListener/DoctrineFilter/DoctrineFilterListener.php
  8. 52 0
      src/EventListener/OnKernelRequestPreRead.php
  9. 2 2
      src/Filter/ApiPlatform/Person/FullNameFilter.php
  10. 1 1
      src/Filter/ApiPlatform/Utils/DistanceFilter.php
  11. 1 1
      src/Filter/ApiPlatform/Utils/FindInSetFilter.php
  12. 1 1
      src/Filter/ApiPlatform/Utils/InFilter.php
  13. 166 0
      src/Filter/Doctrine/TimeConstraint/AbstractTimeFilter.php
  14. 30 0
      src/Filter/Doctrine/TimeConstraint/ActivityYearFilter.php
  15. 29 0
      src/Filter/Doctrine/TimeConstraint/DatetimeFilter.php
  16. 0 64
      src/Filter/DoctrineFilter/AbstractTimeFilter.php
  17. 0 62
      src/Filter/DoctrineFilter/ActivityYearFilter.php
  18. 0 58
      src/Filter/DoctrineFilter/DateTimeFilter.php
  19. 1 1
      src/Security/Voter/EntityVoter/AbstractEntityVoter.php
  20. 2 1
      src/Service/Access/AdminAccessUtils.php
  21. 4 4
      src/Service/Constraint/ActivityYearConstraint.php
  22. 1 1
      src/Service/Constraint/DateTimeConstraint.php
  23. 12 0
      src/Service/Constraint/TimeConstraintInterface.php
  24. 53 0
      src/Service/Doctrine/FiltersConfigurationService.php
  25. 24 24
      src/Service/Export/Encoder/DocXEncoder.php
  26. 0 1
      src/Service/Typo3/SubdomainService.php
  27. 2 0
      src/State/Processor/Organization/SubdomainProcessor.php
  28. 1 1
      src/State/Provider/Organization/Subdomain/SubdomainAvailabilityProvider.php
  29. 415 0
      tests/Unit/Filter/Doctrine/TimeConstraint/AbstractTimeFilterTest.php
  30. 0 76
      tests/Unit/Filter/DoctrineFilter/DateTimeFilterTest.php
  31. 2 0
      tests/Unit/Service/Access/AdminAccessUtilsTest.php
  32. 88 0
      tests/Unit/Service/Doctrine/FiltersConfigurationService.php

+ 4 - 4
config/packages/doctrine.yaml

@@ -32,11 +32,11 @@ doctrine:
             default:
                 filters:
                     date_time_filter:
-                        class: App\Filter\DoctrineFilter\DateTimeFilter
-                        enabled: true
+                        class: App\Filter\Doctrine\TimeConstraint\DatetimeFilter
+                        enabled: false
                     activity_year_filter:
-                        class: App\Filter\DoctrineFilter\ActivityYearFilter
-                        enabled: true
+                        class: App\Filter\Doctrine\TimeConstraint\ActivityYearFilter
+                        enabled: false
                 connection: default
                 auto_mapping: true
                 mappings:

+ 0 - 3
config/services.yaml

@@ -102,9 +102,6 @@ services:
 
     #########################################
     ##  LISTENER ##
-    App\EventListener\DoctrineFilter\DoctrineFilterListener:
-        tags:
-            - { name: kernel.event_listener, event: kernel.request }
 
     #########################################
     ##  ELASTIC SERVICE ##

+ 1 - 1
sql/schema-extensions/002-view_federation_structures.sql

@@ -19,7 +19,7 @@ AS
              LEFT JOIN opentalent.AddressPostal a on oa.addressPostal_id = a.id
              LEFT JOIN opentalent.Country c ON (c.id = a.addressCountry_id)
              LEFT JOIN opentalent.organization_contactpoint ocp ON ocp.organization_id = o.id
-             LEFT JOIN (SELECT * FROM opentalent.ContactPoint WHERE `contactType`='PRINCIPAL') cp ON cp.id = ocp.contactPoint_id
+             INNER JOIN (SELECT * FROM opentalent.ContactPoint WHERE `contactType`='PRINCIPAL') cp ON cp.id = ocp.contactPoint_id
              LEFT JOIN (
                 SELECT oar_.organization_id, CONCAT('[', GROUP_CONCAT(COLUMN_JSON(COLUMN_CREATE('id', oar_.id, 'title', oar_.title, 'date', DATE_FORMAT(oar_.date, '%Y-%m-%dT%TZ'), 'link', oar_.link))), ']') as articles
                 FROM (SELECT * FROM OrganizationArticle WHERE link is not null and link != '' ORDER BY date DESC) as oar_

+ 2 - 2
src/Entity/Access/Access.php

@@ -41,14 +41,14 @@ use App\Entity\Person\CompanyPerson;
 use App\Entity\Person\Medal;
 use App\Entity\Product\Equipment;
 use App\Entity\Product\EquipmentLoan;
-use App\Filter\Person\FullNameFilter;
+use App\Filter\ApiPlatform\Person\FullNameFilter;
 use App\Entity\Billing\AccessIntangible;
 use App\Entity\Billing\AccessPayer;
 use App\Entity\Core\Notification;
 use App\Entity\Core\NotificationUser;
 use App\Entity\Organization\Organization;
 use App\Entity\Organization\OrganizationLicence;
-use App\Filter\Utils\InFilter;
+use App\Filter\ApiPlatform\Utils\InFilter;
 use App\Repository\Access\AccessRepository;
 use App\Entity\Person\Person;
 use App\Entity\Person\PersonActivity;

+ 1 - 1
src/Entity/Public/FederationStructure.php

@@ -9,7 +9,7 @@ use ApiPlatform\Metadata\ApiResource;
 use ApiPlatform\Doctrine\Orm\Filter\NumericFilter;
 use ApiPlatform\Doctrine\Orm\Filter\SearchFilter;
 use ApiPlatform\Metadata\ApiFilter;
-use App\Filter\Utils\FindInSetFilter;
+use App\Filter\ApiPlatform\Utils\FindInSetFilter;
 use App\Repository\Public\FederationStructureRepository;
 use Doctrine\ORM\Mapping as ORM;
 use Symfony\Component\Serializer\Annotation\Groups;

+ 1 - 1
src/Entity/Public/PublicEvent.php

@@ -11,7 +11,7 @@ use ApiPlatform\Doctrine\Orm\Filter\DateFilter;
 use ApiPlatform\Doctrine\Orm\Filter\NumericFilter;
 use ApiPlatform\Doctrine\Orm\Filter\SearchFilter;
 use ApiPlatform\Metadata\ApiFilter;
-use App\Filter\Utils\DistanceFilter;
+use App\Filter\ApiPlatform\Utils\DistanceFilter;
 use App\Repository\Public\PublicEventRepository;
 use Doctrine\ORM\Mapping as ORM;
 

+ 0 - 55
src/EventListener/DoctrineFilter/DoctrineFilterListener.php

@@ -1,55 +0,0 @@
-<?php
-declare(strict_types=1);
-
-namespace App\EventListener\DoctrineFilter;
-
-use App\Entity\Access\Access;
-use App\Filter\DoctrineFilter\ActivityYearFilter;
-use App\Filter\DoctrineFilter\DateTimeFilter;
-use App\Service\Constraint\ActivityYearConstraint;
-use App\Service\Constraint\DateTimeConstraint;
-use Doctrine\ORM\EntityManagerInterface;
-use Symfony\Component\HttpFoundation\RequestStack;
-use Symfony\Component\HttpKernel\Event\RequestEvent;
-use Symfony\Bundle\SecurityBundle\Security;
-use Symfony\Component\Security\Core\User\UserInterface;
-
-/**
- * Classe DoctrineFilterListener qui permet d'assurer l'injection de dépendance pour le SQL Filter
- */
-class DoctrineFilterListener
-{
-    public function __construct(
-        private EntityManagerInterface $entityManager,
-        private DateTimeConstraint $dateTimeConstraint,
-        private ActivityYearConstraint $activityYearConstraint,
-        private Security $security,
-        private RequestStack $requestStack
-    )
-    {
-    }
-
-    public function onKernelRequest(RequestEvent $event): void
-    {
-        if (!$event->isMainRequest()) {
-            // don't do anything if it's not the main request
-            return;
-        }
-
-        /** @var Access $access */
-        $access = $this->security->getUser();
-        if ($access) {
-            /** @var DateTimeFilter $dateTimeFilter */
-            $dateTimeFilter = $this->entityManager->getFilters()->getFilter('date_time_filter');
-            $dateTimeFilter->setParameter('accessId', $access->getId());
-            $dateTimeFilter->setParameter('_time_constraint', $this->requestStack->getMainRequest()->get('_time_constraint', true));
-            $dateTimeFilter->setDateTimeConstraint($this->dateTimeConstraint);
-
-            /** @var ActivityYearFilter $activityYearFilter */
-            $activityYearFilter = $this->entityManager->getFilters()->getFilter('activity_year_filter');
-            $activityYearFilter->setParameter('accessId', $access->getId());
-            $activityYearFilter->setParameter('_time_constraint', $this->requestStack->getMainRequest()->get('_time_constraint', true));
-            $activityYearFilter->setActivityYearConstraint($this->activityYearConstraint);
-        }
-    }
-}

+ 52 - 0
src/EventListener/OnKernelRequestPreRead.php

@@ -0,0 +1,52 @@
+<?php
+
+namespace App\EventListener;
+
+use ApiPlatform\Symfony\EventListener\EventPriorities;
+use App\Entity\Access\Access;
+use App\Service\Doctrine\FiltersConfigurationService;
+use App\Service\OnStart\SessionParametersSetter;
+use Symfony\Component\EventDispatcher\EventSubscriberInterface;
+use Symfony\Component\HttpKernel\KernelEvents;
+use Symfony\Component\HttpKernel\Event\RequestEvent;
+use Symfony\Component\HttpFoundation\RequestStack;
+use Symfony\Bundle\SecurityBundle\Security;
+use App\Service\Utils\StringsUtils;
+
+class OnKernelRequestPreRead implements EventSubscriberInterface
+{
+    public function __construct(
+        private RequestStack $requestStack,
+        private Security $security,
+        private FiltersConfigurationService $filtersConfigurationService
+    ) {}
+
+    public static function getSubscribedEvents()
+    {
+        return [
+            KernelEvents::REQUEST => ['onKernelRequest', EventPriorities::PRE_READ]
+        ];
+    }
+
+    public function onKernelRequest(RequestEvent $event): void {
+        if (!$event->isMainRequest()) {
+            // don't do anything if it's not the main request
+            return;
+        }
+
+        $mainRequest = $this->requestStack->getMainRequest();
+
+        /** @var Access $access */
+        $access = $this->security->getUser();
+        if ($access) {
+
+            $timeConstraintEnabled = (bool) StringsUtils::unquote(
+                $this->requestStack->getMainRequest()->get('_time_constraint', true)
+            );
+
+            if ($timeConstraintEnabled) {
+                $this->filtersConfigurationService->configureTimeConstraintFilters($access->getId());
+            }
+        }
+    }
+}

+ 2 - 2
src/Filter/Person/FullNameFilter.php → src/Filter/ApiPlatform/Person/FullNameFilter.php

@@ -1,7 +1,7 @@
 <?php
 declare(strict_types=1);
 
-namespace App\Filter\Person;
+namespace App\Filter\ApiPlatform\Person;
 
 use ApiPlatform\Doctrine\Orm\Filter\AbstractFilter;
 use ApiPlatform\Doctrine\Orm\Util\QueryNameGeneratorInterface;
@@ -55,4 +55,4 @@ class FullNameFilter extends AbstractFilter {
             ]
         ];
     }
-}
+}

+ 1 - 1
src/Filter/Utils/DistanceFilter.php → src/Filter/ApiPlatform/Utils/DistanceFilter.php

@@ -1,7 +1,7 @@
 <?php
 declare(strict_types=1);
 
-namespace App\Filter\Utils;
+namespace App\Filter\ApiPlatform\Utils;
 
 use ApiPlatform\Doctrine\Orm\Filter\AbstractFilter;
 use ApiPlatform\Doctrine\Orm\Util\QueryNameGeneratorInterface;

+ 1 - 1
src/Filter/Utils/FindInSetFilter.php → src/Filter/ApiPlatform/Utils/FindInSetFilter.php

@@ -1,7 +1,7 @@
 <?php
 declare(strict_types=1);
 
-namespace App\Filter\Utils;
+namespace App\Filter\ApiPlatform\Utils;
 
 use ApiPlatform\Doctrine\Orm\Filter\AbstractFilter;
 use ApiPlatform\Doctrine\Orm\Util\QueryNameGeneratorInterface;

+ 1 - 1
src/Filter/Utils/InFilter.php → src/Filter/ApiPlatform/Utils/InFilter.php

@@ -1,7 +1,7 @@
 <?php
 declare(strict_types=1);
 
-namespace App\Filter\Utils;
+namespace App\Filter\ApiPlatform\Utils;
 
 use ApiPlatform\Doctrine\Orm\Filter\AbstractFilter;
 use ApiPlatform\Doctrine\Orm\Util\QueryNameGeneratorInterface;

+ 166 - 0
src/Filter/Doctrine/TimeConstraint/AbstractTimeFilter.php

@@ -0,0 +1,166 @@
+<?php
+
+namespace App\Filter\Doctrine\TimeConstraint;
+
+use App\Entity\Access\Access;
+use App\Service\Constraint\AbstractTimeConstraintUtils;
+use App\Service\Constraint\ActivityYearConstraint;
+use App\Service\Constraint\DateTimeConstraint;
+use App\Service\Constraint\TimeConstraintInterface;
+use Doctrine\ORM\Query\Filter\SQLFilter;
+use Symfony\Bundle\SecurityBundle\Security;
+use Symfony\Component\HttpFoundation\RequestStack;
+use Doctrine\ORM\Mapping\ClassMetadata;
+use Symfony\Contracts\Service\Attribute\Required;
+
+/**
+ * Applique les contraintes temporelles aux entités possédant l'annotation requise
+ */
+abstract class AbstractTimeFilter extends SQLFilter
+{
+    protected ?int $accessId = null;
+    protected ?TimeConstraintInterface $timeConstraint = null;
+
+    /**
+     * Annotation expected, have to be re-defined in subclasses
+     * @var string|null
+     */
+    protected static ?string $constraintAnnotation = null;
+
+    /**
+     * Parameter of the annotation that design the field containing the starting date of the period
+     * Have to be re-defined in subclasses
+     * @var string|null
+     */
+    protected static ?string $annotationStartField = null;
+
+    /**
+     * Parameter of the annotation that design the field containing the ending date of the period
+     * Have to be re-defined in subclasses
+     * @var string|null
+     */
+    protected static ?string $annotationEndField = null;
+
+    public function setAccessId(int $accessId): void {
+        $this->accessId = $accessId;
+    }
+
+    public function setTimeConstraint(TimeConstraintInterface $timeConstraint): void {
+        $this->timeConstraint = $timeConstraint;
+    }
+
+    /**
+     * Retourne les noms des champs contenant les dates de début et de fin de la période de la ressource testée.
+     *
+     * @param array<string, string> $arguments
+     * @return array<string>
+     */
+    protected function getStartAndEndFields(array $arguments): array {
+        if (static::$annotationStartField === null || static::$annotationEndField === null) {
+            throw new \RuntimeException('Constraint annotation has not been properly configured');
+        }
+
+        $startField = $arguments[static::$annotationStartField] ?? null;
+        $endField = $arguments[static::$annotationEndField] ?? null;
+
+        return [$startField, $endField];
+    }
+
+    /**
+     * Invoke the TimeConstraintUtils to retrieve the active constraints for the given user
+     *
+     * @param int $accessId
+     * @return array<string, array<string, list<int>>>
+     * @throws \Exception
+     */
+    protected function getConstraints(int $accessId): array
+    {
+        return $this->timeConstraint->invoke($accessId);
+    }
+
+    /**
+     * Méthode surchargée de SQLFilter permettant d'appliquer un filtre supplémentaire aux requêtes SQL
+     *
+     * @param ClassMetadata $targetEntity
+     * @param string $targetTableAlias
+     * @return string
+     */
+    public function addFilterConstraint(ClassMetadata $targetEntity, $targetTableAlias): string
+    {
+        if (static::$constraintAnnotation === null) {
+            throw new \RuntimeException('Constraint annotation has not been set');
+        }
+        if ($this->accessId === null) {
+            throw new \RuntimeException('AccessId has not been set');
+        }
+
+        $constraintAnnotation = $targetEntity->getReflectionClass()->getAttributes(static::$constraintAnnotation)[0] ?? null;
+        if (!$constraintAnnotation) {
+            return '';
+        }
+
+        [$startFieldName, $endFieldName] = $this->getStartAndEndFields($constraintAnnotation->getArguments());
+        if (!$startFieldName || !$endFieldName) {
+            throw new \RuntimeException('Missing start and/or end field names in constraint annotation');
+        }
+
+        $constraints = $this->getConstraints($this->accessId);
+
+        $fields = [
+            ActivityYearConstraint::START_KEY => $startFieldName,
+            ActivityYearConstraint::END_KEY => $endFieldName
+        ];
+
+        return $this->constructQuery($constraints, $targetTableAlias, $fields);
+    }
+
+    /**
+     * Fonction permettant de construire la requête SQL correspondante aux contraintes
+     *
+     * @param array<string, array<string, int[]>> $constraints
+     * @param string $targetTableAlias
+     * @param array<string, mixed> $fields
+     * @return string
+     */
+    protected function constructQuery(array $constraints, string $targetTableAlias, array $fields): string {
+        $queryConditionsAND = [];
+        foreach ($constraints as $key => $constraint) {
+            $queryConditionsOR = [];
+            foreach ($constraint as $date => $conditions){
+                foreach ($conditions as $condition){
+                    $arithmetic = $this->getArithmeticValue($condition);
+                    if(!is_null($arithmetic)) {
+                        $queryConditionsOR[] = sprintf("%s.%s %s '%s'", $targetTableAlias, $fields[$key], $arithmetic, $date);
+                    }
+                    else {
+                        $queryConditionsOR[] = sprintf("%s.%s IS NULL", $targetTableAlias, $fields[$key]);
+                    }
+                }
+            }
+            if(!empty($queryConditionsOR)) {
+                $queryConditionsAND[] = sprintf("(%s)", join(' OR ', $queryConditionsOR));
+            }
+        }
+
+        return implode(" AND ", $queryConditionsAND);
+    }
+
+    /**
+     * Fonction retournant la valeur arithmétique correspondant à la condition de la contrainte
+     *
+     * @param int $condition
+     * @return string|null
+     * @see DateTimeFilterTest::testGetArithmeticValue()
+     */
+    protected function getArithmeticValue(int $condition): ?string
+    {
+        return match ($condition) {
+            DateTimeConstraint::INF => '<',
+            DateTimeConstraint::EQUAL => '=',
+            DateTimeConstraint::SUP => '>',
+            DateTimeConstraint::INF + DateTimeConstraint::EQUAL => '<=',
+            DateTimeConstraint::SUP + DateTimeConstraint::EQUAL => '>=',
+            default => null,
+        };
+    }
+}

+ 30 - 0
src/Filter/Doctrine/TimeConstraint/ActivityYearFilter.php

@@ -0,0 +1,30 @@
+<?php
+
+namespace App\Filter\Doctrine\TimeConstraint;
+
+use App\Attribute\ActivityYearConstraintAware;
+use App\Service\Constraint\ActivityYearConstraint;
+use Symfony\Contracts\Service\Attribute\Required;
+
+/**
+ * Filtre Doctrine permettant d'assurer les contraintes de temps sur l'année d'activité actuelle
+ * de l'utilisateur en cours
+ */
+class ActivityYearFilter extends AbstractTimeFilter
+{
+    /**
+     * @inheritdoc
+     * @var string|null
+     */
+    protected static ?string $constraintAnnotation = ActivityYearConstraintAware::class;
+    /**
+     * @inheritdoc
+     * @var string|null
+     */
+    protected static ?string $annotationStartField = 'startYearFieldName';
+    /**
+     * @inheritdoc
+     * @var string|null
+     */
+    protected static ?string $annotationEndField = 'endYearFieldName';
+}

+ 29 - 0
src/Filter/Doctrine/TimeConstraint/DatetimeFilter.php

@@ -0,0 +1,29 @@
+<?php
+
+namespace App\Filter\Doctrine\TimeConstraint;
+
+use App\Attribute\DateTimeConstraintAware;
+use App\Service\Constraint\DateTimeConstraint;
+use Symfony\Contracts\Service\Attribute\Required;
+
+/**
+ * Filtre Doctrine permettant d'assurer les contraintes de temps sur la période choisie par l'utilisateur en cours
+ */
+class DatetimeFilter extends AbstractTimeFilter
+{
+    /**
+     * @inheritdoc
+     * @var string|null
+     */
+    protected static ?string $constraintAnnotation = DateTimeConstraintAware::class;
+    /**
+     * @inheritdoc
+     * @var string|null
+     */
+    protected static ?string $annotationStartField = 'startDateFieldName';
+    /**
+     * @inheritdoc
+     * @var string|null
+     */
+    protected static ?string $annotationEndField = 'endDateFieldName';
+}

+ 0 - 64
src/Filter/DoctrineFilter/AbstractTimeFilter.php

@@ -1,64 +0,0 @@
-<?php
-declare(strict_types=1);
-
-namespace App\Filter\DoctrineFilter;
-
-use App\Attribute\DateTimeConstraintAware;
-use App\Service\Constraint\DateTimeConstraint;
-use App\Tests\Filter\DoctrineFilter\DateTimeFilterTest;
-use Doctrine\ORM\Query\Filter\SQLFilter;
-
-/**
- * Classe AbstractTimeFilterUtils fournie des méthodes pour construire les requetes de contrainte de temps
- */
-abstract class AbstractTimeFilter extends SQLFilter
-{
-    /**
-     * Fonction permettant de construire la requête SQL correspondante aux contraintes
-     * @param array<string, array<string, int[]>> $constraints
-     * @param string $targetTableAlias
-     * @param array<string, mixed> $fields
-     * @return string
-     */
-    protected function constructQuery(array $constraints, string $targetTableAlias, array $fields): string
-    {
-        $queryConditionsAND = [];
-        foreach ($constraints as $key => $constraint) {
-            $queryConditionsOR = [];
-            foreach ($constraint as $date => $conditions){
-                foreach ($conditions as $condition){
-                    $arithmetic = $this->getArithmeticValue($condition);
-                    if(!is_null($arithmetic)) {
-                        $queryConditionsOR[] = sprintf("%s.%s %s '%s'", $targetTableAlias, $fields[$key], $arithmetic, $date);
-                    }
-                    else {
-                        $queryConditionsOR[] = sprintf("%s.%s IS NULL", $targetTableAlias, $fields[$key]);
-                    }
-                }
-            }
-            if(!empty($queryConditionsOR)) {
-                $queryConditionsAND[] = sprintf("(%s)", join(' OR ', $queryConditionsOR));
-            }
-        }
-
-        return implode(" AND ", $queryConditionsAND);
-    }
-
-    /**
-     * Fonction retournant la valeur arithmétique correspondant à la condition de la contrainte
-     * @param int $condition
-     * @return string|null
-     * @see DateTimeFilterTest::testGetArithmeticValue()
-     */
-    protected function getArithmeticValue(int $condition): ?string
-    {
-        return match ($condition) {
-            DateTimeConstraint::INF => '<',
-            DateTimeConstraint::EQUAL => '=',
-            DateTimeConstraint::SUP => '>',
-            DateTimeConstraint::INF + DateTimeConstraint::EQUAL => '<=',
-            DateTimeConstraint::SUP + DateTimeConstraint::EQUAL => '>=',
-            default => null,
-        };
-    }
-}

+ 0 - 62
src/Filter/DoctrineFilter/ActivityYearFilter.php

@@ -1,62 +0,0 @@
-<?php
-declare(strict_types=1);
-
-namespace App\Filter\DoctrineFilter;
-
-use App\Attribute\ActivityYearConstraintAware;
-use App\Service\Constraint\ActivityYearConstraint;
-use App\Service\Utils\StringsUtils;
-use Doctrine\ORM\Mapping\ClassMetadata;
-use Exception;
-
-/**
- * Classe DateTimeFilter qui définie la requête SQL devant être ajoutée aux Entités possédant l'annotation DateTimeConstraintAware
- */
-final class ActivityYearFilter extends AbstractTimeFilter
-{
-    private ActivityYearConstraint $activityYearConstraint;
-
-    /**
-     * Méthode surchargée de SQLFilter
-     * @param ClassMetadata $targetEntity
-     * @param string $targetTableAlias
-     * @return string
-     * @throws Exception
-     */
-    public function addFilterConstraint(ClassMetadata $targetEntity, $targetTableAlias): string
-    {
-        if(!$this->hasParameter('_time_constraint')
-            || !(bool) StringsUtils::unquote($this->getParameter('_time_constraint'))
-            || !$this->hasParameter('accessId')
-        ) {
-            return '';
-        }
-
-        $yearConstraintAware = $targetEntity->getReflectionClass()->getAttributes(ActivityYearConstraintAware::class)[0] ?? null;
-        $startFieldName = $yearConstraintAware?->getArguments()['startYearFieldName'] ?? null;
-        $endFieldName = $yearConstraintAware?->getArguments()['endYearFieldName'] ?? null;
-        if ($startFieldName === '' || is_null($startFieldName) || $endFieldName === '' || is_null($endFieldName)) {
-            return '';
-        }
-
-        $accessId = intval(StringsUtils::unquote($this->getParameter('accessId')));
-
-        $constraints = $this->activityYearConstraint->invoke($accessId);
-
-        $fields = [
-            ActivityYearConstraint::START_KEY => $startFieldName,
-            ActivityYearConstraint::END_KEY => $endFieldName
-        ];
-
-        return $this->constructQuery($constraints, $targetTableAlias, $fields);
-    }
-
-    /**
-     * Permets d'assurer l'injection de dépendance du service YearTimeConstraint
-     * @param ActivityYearConstraint $activityYearConstraint
-     *
-     */
-    public function setActivityYearConstraint(ActivityYearConstraint $activityYearConstraint): void{
-        $this->activityYearConstraint = $activityYearConstraint;
-    }
-}

+ 0 - 58
src/Filter/DoctrineFilter/DateTimeFilter.php

@@ -1,58 +0,0 @@
-<?php
-declare(strict_types=1);
-
-namespace App\Filter\DoctrineFilter;
-
-use App\Attribute\DateTimeConstraintAware;
-use App\Service\Constraint\DateTimeConstraint;
-use App\Service\Utils\StringsUtils;
-use Doctrine\ORM\Mapping\ClassMetadata;
-
-/**
- * Classe DateTimeFilter qui définie la requête SQL devant être ajoutée aux Entités possédant l'annotation DateTimeConstraintAware
- */
-final class DateTimeFilter extends AbstractTimeFilter
-{
-    private DateTimeConstraint $dateTimeConstraint;
-
-    /**
-     * Méthode surchargée de SQLFilter
-     * @param ClassMetadata $targetEntity
-     * @param string $targetTableAlias
-     * @return string
-     * @throws \Exception
-     */
-    public function addFilterConstraint(ClassMetadata $targetEntity, $targetTableAlias): string
-    {
-        if(!$this->hasParameter('_time_constraint')
-            || !boolval(StringsUtils::unquote($this->getParameter('_time_constraint')))
-            || !$this->hasParameter('accessId')
-        )
-            return '';
-
-        $dateTimeConstraintAware = $targetEntity->getReflectionClass()->getAttributes(DateTimeConstraintAware::class)[0] ?? null;
-        $startFieldName = $dateTimeConstraintAware?->getArguments()['startDateFieldName'] ?? null;
-        $endFieldName = $dateTimeConstraintAware?->getArguments()['endDateFieldName'] ?? null;
-        if ($startFieldName === '' || is_null($startFieldName) || $endFieldName === '' || is_null($endFieldName)) {
-            return '';
-        }
-
-        $accessId = intval(StringsUtils::unquote($this->getParameter('accessId')));
-        $constraints = $this->dateTimeConstraint->invoke($accessId);
-
-        $fields = [
-            DateTimeConstraint::START_KEY => $startFieldName,
-            DateTimeConstraint::END_KEY => $endFieldName
-        ];
-
-        return $this->constructQuery($constraints, $targetTableAlias, $fields);
-    }
-
-    /**
-     * Permets d'assurer l'injection de dépendance du service DateTimeConstraint
-     * @param DateTimeConstraint $dateTimeConstraint
-     */
-    public function setDateTimeConstraint(DateTimeConstraint $dateTimeConstraint): void{
-        $this->dateTimeConstraint = $dateTimeConstraint;
-    }
-}

+ 1 - 1
src/Security/Voter/EntityVoter/AbstractEntityVoter.php

@@ -72,7 +72,7 @@ abstract class AbstractEntityVoter extends Voter
             throw new \RuntimeException('Setup the self::$entityClass property, or override the supports() method');
         }
 
-        return $subject::class === static::$entityClass && in_array($attribute, static::$allowedOperations);
+        return $subject !== null && $subject::class === static::$entityClass && in_array($attribute, static::$allowedOperations);
     }
 
     /**

+ 2 - 1
src/Service/Access/AdminAccessUtils.php

@@ -36,8 +36,9 @@ class AdminAccessUtils
         $contactPoint = $this->contactPointUtils->getPersonContactPointPrincipal($administrator);
         if(!$contactPoint) return null;
 
-        $adminAccess = new AdminAccess($organization->getId());
+        $adminAccess = new AdminAccess();
         $adminAccess
+            ->setOrganizationId($organization->getId())
             ->setId($administrator->getId())
             ->setUsername($administrator->getPerson()->getUserIdentifier())
             ->setEmail($contactPoint->getEmail())

+ 4 - 4
src/Service/Constraint/ActivityYearConstraint.php

@@ -11,7 +11,7 @@ use Doctrine\ORM\EntityManagerInterface;
  * Classe ActivityYearConstraint qui définie l'année de début (et de fin dans le cas d'une période custom)
  * par rapport aux contraintes temporelles choisies par un utilisateur.
  */
-class ActivityYearConstraint extends AbstractTimeConstraintUtils
+class ActivityYearConstraint extends AbstractTimeConstraintUtils implements TimeConstraintInterface
 {
     public function __construct(
         private EntityManagerInterface $entityManager,
@@ -22,13 +22,13 @@ class ActivityYearConstraint extends AbstractTimeConstraintUtils
     /**
      * Main method
      *
-     * @param int $accessID
+     * @param int $accessId
      * @return array<string, array<string, list<int>>>
      * @throws \Exception
      */
-    public function invoke(int $accessID): array
+    public function invoke(int $accessId): array
     {
-        $access = $this->entityManager->getRepository(Access::class)->find($accessID);
+        $access = $this->entityManager->getRepository(Access::class)->find($accessId);
         $historical = $access->getHistorical();
 
         $contraints = [

+ 1 - 1
src/Service/Constraint/DateTimeConstraint.php

@@ -13,7 +13,7 @@ use Exception;
  * Classe DateTimeConstraint qui définie les dates de débuts et fin de périodes
  * par rapport au contraintes temporelles choisies par un utilisateur.
  */
-class DateTimeConstraint extends AbstractTimeConstraintUtils
+class DateTimeConstraint extends AbstractTimeConstraintUtils implements TimeConstraintInterface
 {
     public function __construct(
         readonly private EntityManagerInterface $entityManager,

+ 12 - 0
src/Service/Constraint/TimeConstraintInterface.php

@@ -0,0 +1,12 @@
+<?php
+
+namespace App\Service\Constraint;
+
+interface TimeConstraintInterface
+{
+    /**
+     * @param int $accessId
+     * @return array<string, array<string, list<int>>>
+     */
+    public function invoke(int $accessId): array;
+}

+ 53 - 0
src/Service/Doctrine/FiltersConfigurationService.php

@@ -0,0 +1,53 @@
+<?php
+
+namespace App\Service\Doctrine;
+
+use App\Filter\Doctrine\TimeConstraint\AbstractTimeFilter;
+use App\Filter\Doctrine\TimeConstraint\ActivityYearFilter;
+use App\Filter\Doctrine\TimeConstraint\DatetimeFilter;
+use App\Service\Constraint\AbstractTimeConstraintUtils;
+use App\Service\Constraint\ActivityYearConstraint;
+use App\Service\Constraint\DateTimeConstraint;
+use Doctrine\ORM\EntityManagerInterface;
+use Doctrine\ORM\Query\Filter\SQLFilter;
+use Doctrine\ORM\Query\FilterCollection;
+
+/**
+ * Service de configuration des filtres doctrine
+ *
+ * @see src/EventListener/OnKernelRequestPreRead.php
+ */
+class FiltersConfigurationService
+{
+    public function __construct(
+        private EntityManagerInterface $entityManager,
+        private DateTimeConstraint $dateTimeConstraint,
+        private ActivityYearConstraint $activityYearConstraint
+    ) {}
+
+    /**
+     * Return the existing Doctrine SQLFilters
+     * @return FilterCollection
+     */
+    protected function getFilters(): FilterCollection {
+        return $this->entityManager->getFilters();
+    }
+
+    /**
+     * Enable and preconfigure the doctrine sql filters that apply the time constraints
+     *
+     * @param int $accessId
+     * @return void
+     */
+    public function configureTimeConstraintFilters(int $accessId): void {
+        /** @var DatetimeFilter $dateTimeFilter */
+        $dateTimeFilter = $this->getFilters()->enable('date_time_filter');
+        $dateTimeFilter->setAccessId($accessId);
+        $dateTimeFilter->setTimeConstraint($this->dateTimeConstraint);
+
+        /** @var ActivityYearFilter $activityYearFilter */
+        $activityYearFilter = $this->getFilters()->enable('activity_year_filter');
+        $activityYearFilter->setAccessId($accessId);
+        $activityYearFilter->setTimeConstraint($this->activityYearConstraint);
+    }
+}

+ 24 - 24
src/Service/Export/Encoder/DocXEncoder.php

@@ -13,29 +13,29 @@ use App\Enum\Export\ExportFormatEnum;
 class DocXEncoder implements EncoderInterface
 {
 
-    public function support(string $format): bool
-    {
-        return $format === ExportFormatEnum::DOCX()->getValue();
-    }
+  public function support(string $format): bool
+  {
+    return $format === ExportFormatEnum::DOCX()->getValue();
+  }
 
-    /**
-     * Encode the given HTML content into docX, and
-     * return the encoded content
-     *
-     * @param string $html
-     * @param array<mixed> $options
-     * @return string
-     */
-    public function encode(string $html, array $options = []): string
-    {
-//        $docx = new CreateDocx();
-//        $docx->embedHTML($html);
-//        $tempFile = tempnam(sys_get_temp_dir(), 'docx');
-//        $docx->createDocx($tempFile);
-//        $content = file_get_contents($tempFile);
-//        unlink($tempFile);
-//        return $content;
-
-        return '';
-    }
+//  TODO: resolve Phpstan errors
+  /**
+   * Encode the given HTML content into docX, and
+   * return the encoded content
+   *
+   * @param string $html
+   * @param array<mixed> $options
+   * @return string
+   */
+  public function encode(string $html, array $options = []): string
+  {
+//    $docx = new CreateDocx();
+//    $docx->embedHTML($html);
+//    $tempFile = tempnam(sys_get_temp_dir(), 'docx');
+//    $docx->createDocx($tempFile);
+//    $content = file_get_contents($tempFile);
+//    unlink($tempFile);
+//    return $content;
+      return "";
+  }
 }

+ 0 - 1
src/Service/Typo3/SubdomainService.php

@@ -13,7 +13,6 @@ use App\Service\Mailer\Model\SubdomainChangeModel;
 use App\Service\Organization\Utils as OrganizationUtils;
 use Doctrine\DBAL\Connection;
 use Doctrine\ORM\EntityManagerInterface;
-use Symfony\Bundle\SecurityBundle\Security;
 use Symfony\Component\Console\Exception\InvalidArgumentException;
 use Symfony\Component\Messenger\MessageBusInterface;
 use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;

+ 2 - 0
src/State/Processor/Organization/SubdomainProcessor.php

@@ -8,6 +8,7 @@ use ApiPlatform\Metadata\Operation;
 use ApiPlatform\Metadata\Post;
 use ApiPlatform\Metadata\Put;
 use ApiPlatform\State\ProcessorInterface;
+use App\Entity\Access\Access;
 use App\Entity\Organization\Subdomain;
 use App\Repository\Organization\SubdomainRepository;
 use App\Service\Typo3\SubdomainService;
@@ -38,6 +39,7 @@ class SubdomainProcessor implements ProcessorInterface
             throw new \RuntimeException('not supported', 500);
         }
 
+        /** @var Access $access */
         $access = $this->security->getUser();
         if ($data->getOrganization()->getId() !== $access->getOrganization()->getId()) {
             // TODO: voir à déplacer dans un voter?

+ 1 - 1
src/State/Provider/Organization/Subdomain/SubdomainAvailabilityProvider.php

@@ -24,7 +24,7 @@ final class SubdomainAvailabilityProvider implements ProviderInterface
      * @param Operation $operation
      * @param array<mixed> $uriVariables
      * @param array<mixed> $context
-     * @return Response|RedirectResponse
+     * @return SubdomainAvailability | null
      * @throws FileNotFoundException
      */
     public function provide(Operation $operation, array $uriVariables = [], array $context = []): ?SubdomainAvailability

+ 415 - 0
tests/Unit/Filter/Doctrine/TimeConstraint/AbstractTimeFilterTest.php

@@ -0,0 +1,415 @@
+<?php
+namespace App\Tests\Unit\Filter\TimeConstraint;
+
+use App\Attribute\DateTimeConstraintAware;
+use App\Filter\Doctrine\TimeConstraint\AbstractTimeFilter;
+use App\Filter\Doctrine\TimeConstraint\DatetimeFilter;
+use App\Service\Constraint\DateTimeConstraint;
+use App\Service\Constraint\TimeConstraintInterface;
+use App\Tests\Unit\TestToolsTrait;
+use Doctrine\ORM\EntityManagerInterface;
+use Doctrine\ORM\Mapping\ClassMetadata;
+use PHPUnit\Framework\MockObject\MockObject;
+use PHPUnit\Framework\TestCase;
+
+class TestableAbstractTimeFilter extends AbstractTimeFilter {
+    protected static ?string $annotationStartField = 'startFieldName';
+    protected static ?string $annotationEndField = 'endFieldName';
+    public function getAccessId() { return $this->accessId; }
+    public function getTimeConstraint() { return $this->timeConstraint; }
+    public function getStartAndEndFields(array $arguments): array { return parent::getStartAndEndFields($arguments); }
+    public function getConstraints(int $accessId): array { return parent::getConstraints($accessId); }
+    public function constructQuery(array $constraints, string $targetTableAlias, array $fields): string { return parent::constructQuery($constraints, $targetTableAlias, $fields); }
+    public function getArithmeticValue(int $condition): ?string { return parent::getArithmeticValue($condition); }
+    public static function setConstraintAnnotation(?string $constraintAnnotation) { static::$constraintAnnotation = $constraintAnnotation; }
+    public static function setAnnotationStartField(?string $startField) { static::$annotationStartField = $startField; }
+    public static function setAnnotationEndField(?string $endField) { static::$annotationEndField = $endField; }
+}
+
+#[\Attribute(\Attribute::TARGET_CLASS)]
+class TestableConstraintAware
+{
+    public string $startDateFieldName;
+    public string $endDateFieldName;
+
+    public function __construct(
+        string $startDateFieldName,
+        string $endDateFieldName
+    ){
+        $this->startDateFieldName = $startDateFieldName;
+        $this->endDateFieldName = $endDateFieldName;
+    }
+
+    public function getArguments() {
+        return null;
+    }
+}
+
+class AbstractTimeFilterTest extends TestCase
+{
+    private MockObject | EntityManagerInterface $em;
+
+    public function setUp(): void
+    {
+        $this->em = $this->getMockBuilder(EntityManagerInterface::class)->disableOriginalConstructor()->getMock();
+
+        // reset static values
+        TestableAbstractTimeFilter::setConstraintAnnotation(null);
+        TestableAbstractTimeFilter::setAnnotationStartField('startFieldName');
+        TestableAbstractTimeFilter::setAnnotationEndField('endFieldName');
+    }
+
+
+    private function getAbstractTimeFilterMockFor(array $methodNames): TestableAbstractTimeFilter | MockObject {
+
+        return $this
+            ->getMockBuilder(TestableAbstractTimeFilter::class)
+            ->setConstructorArgs([$this->em])
+            ->setMethodsExcept(array_merge(['setEntityManager'], $methodNames))
+            ->getMock();
+    }
+
+    public function testSetAccessId(): void {
+        $filter = $this->getAbstractTimeFilterMockFor(['setAccessId', 'getAccessId']);
+        $filter->setAccessId(123);
+
+        $this->assertEquals(123, $filter->getAccessId());
+    }
+
+    public function testSetTimeConstraint(): void {
+        $filter = $this->getAbstractTimeFilterMockFor(['setTimeConstraint', 'getTimeConstraint']);
+
+        $timeConstraint = $this->getMockBuilder(DateTimeConstraint::class)->disableOriginalConstructor()->getMock();
+        $filter->setTimeConstraint($timeConstraint);
+
+        $this->assertEquals($timeConstraint, $filter->getTimeConstraint());
+    }
+
+    public function testGetStartAndEndFields(): void {
+        $filter = $this->getAbstractTimeFilterMockFor(['getStartAndEndFields']);
+
+        $this->assertEquals(
+            ['startDate', 'endDate'],
+            $filter->getStartAndEndFields(['startFieldName' => 'startDate', 'endFieldName' => 'endDate'])
+        );
+    }
+
+    public function testGetStartAndEndFieldsNoValue(): void {
+        $filter = $this->getAbstractTimeFilterMockFor(['getStartAndEndFields']);
+
+        $this->assertEquals(
+            ['startDate', null],
+            $filter->getStartAndEndFields(['startFieldName' => 'startDate'])
+        );
+
+        $this->assertEquals(
+            [null, 'endDate'],
+            $filter->getStartAndEndFields(['endFieldName' => 'endDate'])
+        );
+
+        $this->assertEquals(
+            [null, null],
+            $filter->getStartAndEndFields([])
+        );
+    }
+
+    public function testGetStartAndEndFieldsMissingAnnotationStartField(): void {
+        $filter = $this->getAbstractTimeFilterMockFor(['getStartAndEndFields']);
+
+        TestableAbstractTimeFilter::setAnnotationStartField(null);
+
+        $this->expectException(\RuntimeException::class);
+
+        $filter->getStartAndEndFields([]);
+    }
+
+    public function testGetStartAndEndFieldsMissingAnnotationEndField(): void {
+        $filter = $this->getAbstractTimeFilterMockFor(['getStartAndEndFields']);
+
+        TestableAbstractTimeFilter::setAnnotationEndField(null);
+
+        $this->expectException(\RuntimeException::class);
+
+        $filter->getStartAndEndFields([]);
+    }
+
+    public function testGetConstraints(): void {
+        $filter = $this->getAbstractTimeFilterMockFor(['getConstraints', 'setTimeConstraint']);
+
+        $timeConstraint = $this->getMockBuilder(DateTimeConstraint::class)->disableOriginalConstructor()->getMock();
+        $filter->setTimeConstraint($timeConstraint);
+
+        $accessId = 123;
+        $constraints = ['a' => ['b' => [4, 5, 6]]];
+
+        $timeConstraint->expects(self::once())->method('invoke')->with($accessId)->willReturn($constraints);
+
+        $this->assertEquals(
+            $constraints,
+            $filter->getConstraints($accessId)
+        );
+    }
+
+    public function testAddFilterConstraint(): void {
+        $filter = $this->getAbstractTimeFilterMockFor(['addFilterConstraint', 'setAccessId']);
+
+        $targetEntity = $this->getMockBuilder(ClassMetadata::class)->disableOriginalConstructor()->getMock();
+        $targetTableAlias = 'abc';
+
+        $constraintAnnotationClass = DateTimeConstraintAware::class;
+        TestableAbstractTimeFilter::setConstraintAnnotation($constraintAnnotationClass);
+
+        $startFieldName = 'startDate';
+        $endFieldName = 'endDate';
+
+        $constraintAnnotation = $this
+            ->getMockBuilder(TestableConstraintAware::class)
+            ->setConstructorArgs([$startFieldName, $endFieldName])
+            ->getMock();
+
+        $constraintAnnotation->method('getArguments')->willReturn([$startFieldName, $endFieldName]);
+
+        $reflectionClass = $this->getMockBuilder(\ReflectionClass::class)->disableOriginalConstructor()->getMock();
+        $reflectionClass
+            ->expects(self::once())
+            ->method('getAttributes')
+            ->with(DateTimeConstraintAware::class)
+            ->willReturn([$constraintAnnotation]);
+
+        $targetEntity->expects(self::once())->method('getReflectionClass')->willReturn($reflectionClass);
+
+        $fields = ['startDate', 'endDate'];
+
+        $filter->expects(self::once())->method('getStartAndEndFields')->with([$startFieldName, $endFieldName])->willReturn($fields);
+
+        $accessId = 123;
+        $filter->setAccessId(123);
+
+        $constraints = [
+            'start' => ['2021-12-20' => [4]],
+            'end' => ['2021-12-20' => [5], 'NULL' => [0]]
+        ];
+
+        $filter->expects(self::once())->method('getConstraints')->with($accessId)->willReturn($constraints);
+
+        $formattedFields = ['start' => 'startDate', 'end' => 'endDate'];
+
+        $generatedSql = 'azerty=1';
+
+        $filter
+            ->expects(self::once())
+            ->method('constructQuery')
+            ->with($constraints, $targetTableAlias, $formattedFields)
+            ->willReturn($generatedSql);
+
+        $this->assertEquals(
+            $generatedSql,
+            $filter->addFilterConstraint($targetEntity, $targetTableAlias)
+        );
+    }
+
+    public function testAddFilterConstraintMissingConstraintAnnotation(): void {
+        $filter = $this->getAbstractTimeFilterMockFor(['addFilterConstraint']);
+
+        $targetEntity = $this->getMockBuilder(ClassMetadata::class)->disableOriginalConstructor()->getMock();
+        $targetTableAlias = 'abc';
+
+        TestableAbstractTimeFilter::setConstraintAnnotation(null);
+
+        $this->expectException(\RuntimeException::class);
+
+        $filter->addFilterConstraint($targetEntity, $targetTableAlias);
+    }
+
+    public function testAddFilterConstraintMissingAccessId(): void {
+        $filter = $this->getAbstractTimeFilterMockFor(['addFilterConstraint']);
+
+        $targetEntity = $this->getMockBuilder(ClassMetadata::class)->disableOriginalConstructor()->getMock();
+        $targetTableAlias = 'abc';
+
+        $constraintAnnotationClass = DateTimeConstraintAware::class;
+        TestableAbstractTimeFilter::setConstraintAnnotation($constraintAnnotationClass);
+
+        $this->expectException(\RuntimeException::class);
+
+        $filter->addFilterConstraint($targetEntity, $targetTableAlias);
+    }
+
+    public function testAddFilterConstraintMissingTargetHasNoAnnotation(): void {
+        $filter = $this->getAbstractTimeFilterMockFor(['addFilterConstraint', 'setAccessId']);
+
+        $targetEntity = $this->getMockBuilder(ClassMetadata::class)->disableOriginalConstructor()->getMock();
+        $targetTableAlias = 'abc';
+
+        $constraintAnnotationClass = DateTimeConstraintAware::class;
+        TestableAbstractTimeFilter::setConstraintAnnotation($constraintAnnotationClass);
+
+        $accessId = 123;
+        $filter->setAccessId(123);
+
+        $reflectionClass = $this->getMockBuilder(\ReflectionClass::class)->disableOriginalConstructor()->getMock();
+        $reflectionClass
+            ->expects(self::once())
+            ->method('getAttributes')
+            ->with(DateTimeConstraintAware::class)
+            ->willReturn([]);
+
+        $targetEntity->expects(self::once())->method('getReflectionClass')->willReturn($reflectionClass);
+
+        $this->assertEquals(
+            '',
+            $filter->addFilterConstraint($targetEntity, $targetTableAlias)
+        );
+    }
+
+    public function testAddFilterConstraintAnnotationMissingStartDate(): void {
+        $filter = $this->getAbstractTimeFilterMockFor(['addFilterConstraint', 'setAccessId']);
+
+        $targetEntity = $this->getMockBuilder(ClassMetadata::class)->disableOriginalConstructor()->getMock();
+        $targetTableAlias = 'abc';
+
+        $constraintAnnotationClass = DateTimeConstraintAware::class;
+        TestableAbstractTimeFilter::setConstraintAnnotation($constraintAnnotationClass);
+
+        $accessId = 123;
+        $filter->setAccessId(123);
+
+        $startFieldName = 'startDate';
+        $endFieldName = 'endDate';
+
+        $constraintAnnotation = $this
+            ->getMockBuilder(TestableConstraintAware::class)
+            ->setConstructorArgs(['', $endFieldName])
+            ->getMock();
+
+        $constraintAnnotation->method('getArguments')->willReturn([$startFieldName, $endFieldName]);
+
+        $reflectionClass = $this->getMockBuilder(\ReflectionClass::class)->disableOriginalConstructor()->getMock();
+        $reflectionClass
+            ->expects(self::once())
+            ->method('getAttributes')
+            ->with(DateTimeConstraintAware::class)
+            ->willReturn([$constraintAnnotation]);
+
+        $targetEntity->expects(self::once())->method('getReflectionClass')->willReturn($reflectionClass);
+
+        $fields = ['startDate', 'endDate'];
+
+        $filter->expects(self::once())->method('getStartAndEndFields')->willReturn(['', $endFieldName]);
+
+        $this->expectException(\RuntimeException::class);
+
+        $filter->addFilterConstraint($targetEntity, $targetTableAlias);
+    }
+
+    public function testAddFilterConstraintAnnotationMissingEndDate(): void {
+        $filter = $this->getAbstractTimeFilterMockFor(['addFilterConstraint', 'setAccessId']);
+
+        $targetEntity = $this->getMockBuilder(ClassMetadata::class)->disableOriginalConstructor()->getMock();
+        $targetTableAlias = 'abc';
+
+        $constraintAnnotationClass = DateTimeConstraintAware::class;
+        TestableAbstractTimeFilter::setConstraintAnnotation($constraintAnnotationClass);
+
+        $accessId = 123;
+        $filter->setAccessId(123);
+
+        $startFieldName = 'startDate';
+        $endFieldName = 'endDate';
+
+        $constraintAnnotation = $this
+            ->getMockBuilder(TestableConstraintAware::class)
+            ->setConstructorArgs([$startFieldName, ''])
+            ->getMock();
+
+        $constraintAnnotation->method('getArguments')->willReturn([$startFieldName, $endFieldName]);
+
+        $reflectionClass = $this->getMockBuilder(\ReflectionClass::class)->disableOriginalConstructor()->getMock();
+        $reflectionClass
+            ->expects(self::once())
+            ->method('getAttributes')
+            ->with(DateTimeConstraintAware::class)
+            ->willReturn([$constraintAnnotation]);
+
+        $targetEntity->expects(self::once())->method('getReflectionClass')->willReturn($reflectionClass);
+
+        $fields = ['startDate', 'endDate'];
+
+        $filter->expects(self::once())->method('getStartAndEndFields')->willReturn([$startFieldName, '']);
+
+        $this->expectException(\RuntimeException::class);
+
+        $filter->addFilterConstraint($targetEntity, $targetTableAlias);
+    }
+
+    /**
+     * @see DateTimeFilter::constructQuery()
+     */
+    public function testConstructQuery(): void
+    {
+        // Exceptionnellement on ne mock jamais une méthode (getArithmeticValue), il n'y a aucune plus value à le faire et ça alourdirait beaucoup les tests
+        $filter = $this->getAbstractTimeFilterMockFor(['constructQuery', 'getArithmeticValue']);
+
+        $constraints = [
+            'start' => ['2021-12-20' => [4]],
+            'end' => ['2021-12-20' => [5], 'NULL' => [0]]
+        ];
+
+        $tableAlias = 'o';
+
+        $fields = [
+            'start' => 'startDate',
+            'end' => 'endDate'
+        ];
+
+        $queryExpected = "(o.startDate <= '2021-12-20') AND (o.endDate > '2021-12-20' OR o.endDate IS NULL)";
+
+        $this->assertEquals(
+            $queryExpected,
+            $filter->constructQuery($constraints, $tableAlias, $fields)
+        );
+    }
+
+    /**
+     * @see DateTimeFilter::constructQuery()
+     */
+    public function testConstructQueryWithOr(): void
+    {
+        // Exceptionnellement on ne mock jamais une méthode (getArithmeticValue), il n'y a aucune plus value à le faire et ça alourdirait beaucoup les tests
+        $filter = $this->getAbstractTimeFilterMockFor(['constructQuery', 'getArithmeticValue']);
+
+        $constraints = [
+            'start' => ['2021-12-20' => [1], '2021-12-30' => [5]],
+            'end' => ['2022-01-20' => [1]]
+        ];
+
+        $tableAlias = 'o';
+
+        $fields = [
+            'start' => 'startDate',
+            'end' => 'endDate'
+        ];
+
+        $queryExpected = "(o.startDate < '2021-12-20' OR o.startDate > '2021-12-30') AND (o.endDate < '2022-01-20')";
+
+        $this->assertEquals(
+            $queryExpected,
+            $filter->constructQuery($constraints, $tableAlias, $fields)
+        );
+    }
+
+    /**
+     * @see DateTimeFilter::getArithmeticValue()
+     */
+    public function testGetArithmeticValue(): void
+    {
+        $filter = $this->getAbstractTimeFilterMockFor(['getArithmeticValue']);
+
+        $this->assertEquals('<', $filter->getArithmeticValue(1));
+        $this->assertEquals('<=', $filter->getArithmeticValue(4));
+        $this->assertEquals('=', $filter->getArithmeticValue(3));
+        $this->assertEquals('>=', $filter->getArithmeticValue(8));
+        $this->assertEquals('>', $filter->getArithmeticValue(5));
+        $this->assertEquals(null, $filter->getArithmeticValue(0));
+    }
+}

+ 0 - 76
tests/Unit/Filter/DoctrineFilter/DateTimeFilterTest.php

@@ -1,76 +0,0 @@
-<?php
-namespace App\Tests\Unit\Filter\DoctrineFilter;
-
-use App\Filter\DoctrineFilter\DateTimeFilter;
-use App\Tests\Unit\TestToolsTrait;
-use Doctrine\ORM\EntityManagerInterface;
-use PHPUnit\Framework\TestCase;
-
-
-class DateTimeFilterTest extends TestCase
-{
-    use TestToolsTrait;
-
-    private DateTimeFilter $dateTimeFilter;
-
-    public function setUp(): void
-    {
-        $em = $this->getMockBuilder(EntityManagerInterface::class)->disableOriginalConstructor()->getMock();
-        $this->dateTimeFilter = new DateTimeFilter($em);
-    }
-
-    /**
-     * @see DateTimeFilter::constructQuery()
-     */
-    public function testConstructQuery():void
-    {
-        $queryExpected = "(o.startDate <= '2021-12-20') AND (o.endDate > '2021-12-20' OR o.endDate IS NULL)";
-        $this->assertEquals($queryExpected, $this->invokeMethod($this->dateTimeFilter, 'constructQuery', [
-            [
-                'start' => [
-                    '2021-12-20' => [4]
-                ],
-                'end' => [
-                    '2021-12-20' => [5],
-                    'NULL' => [0]
-                ]
-            ],
-            'o',
-            [
-                'start' => 'startDate',
-                'end' => 'endDate'
-            ]
-        ]));
-
-        $queryExpected = "(o.startDate < '2021-12-20' OR o.startDate > '2021-12-30') AND (o.endDate < '2022-01-20')";
-        $this->assertEquals($queryExpected, $this->invokeMethod($this->dateTimeFilter, 'constructQuery', [
-            [
-                'start' => [
-                    '2021-12-20' => [1],
-                    '2021-12-30' => [5]
-                ],
-                'end' => [
-                    '2022-01-20' => [1]
-                ]
-            ],
-            'o',
-            [
-                'start' => 'startDate',
-                'end' => 'endDate'
-            ]
-        ]));
-    }
-
-    /**
-     * @see DateTimeFilter::getArithmeticValue()
-     */
-    public function testGetArithmeticValue():void
-    {
-        $this->assertEquals('<', $this->invokeMethod($this->dateTimeFilter, 'getArithmeticValue', [1]));
-        $this->assertEquals('<=', $this->invokeMethod($this->dateTimeFilter, 'getArithmeticValue', [4]));
-        $this->assertEquals('=', $this->invokeMethod($this->dateTimeFilter, 'getArithmeticValue', [3]));
-        $this->assertEquals('>=', $this->invokeMethod($this->dateTimeFilter, 'getArithmeticValue', [8]));
-        $this->assertEquals('>', $this->invokeMethod($this->dateTimeFilter, 'getArithmeticValue', [5]));
-        $this->assertEquals(null, $this->invokeMethod($this->dateTimeFilter, 'getArithmeticValue', [0]));
-    }
-}

+ 2 - 0
tests/Unit/Service/Access/AdminAccessUtilsTest.php

@@ -102,6 +102,8 @@ class AdminAccessUtilsTest extends TestCase
             ->with($this->access)
             ->willReturn($contactPoint);
 
+        $this->organization->method('getId')->willReturn(123);
+
         $returned = $adminAccessUtils->getAdminAccess($this->organization);
 
         $this->assertInstanceOf(AdminAccess::class, $returned);

+ 88 - 0
tests/Unit/Service/Doctrine/FiltersConfigurationService.php

@@ -0,0 +1,88 @@
+<?php
+
+namespace App\Tests\Unit\Service\Doctrine;
+
+use App\Filter\Doctrine\TimeConstraint\ActivityYearFilter;
+use App\Filter\Doctrine\TimeConstraint\DatetimeFilter;
+use App\Service\Constraint\ActivityYearConstraint;
+use App\Service\Constraint\DateTimeConstraint;
+use App\Service\Doctrine\FiltersConfigurationService;
+use Doctrine\ORM\EntityManagerInterface;
+use Doctrine\ORM\Query\FilterCollection;
+use PHPUnit\Framework\MockObject\MockObject;
+use PHPUnit\Framework\TestCase;
+
+class TestableFiltersConfigurationService extends FiltersConfigurationService {
+    public function getFilters(): FilterCollection
+    {
+        return parent::getFilters();
+    }
+}
+
+
+class FiltersConfigurationServiceTest extends TestCase
+{
+    private \PHPUnit\Framework\MockObject\MockObject|EntityManagerInterface $em;
+    private DateTimeConstraint|\PHPUnit\Framework\MockObject\MockObject $dateTimeConstraint;
+    private ActivityYearConstraint|\PHPUnit\Framework\MockObject\MockObject $activityYearConstraint;
+
+    public function setUp(): void {
+        $this->em = $this->getMockBuilder(EntityManagerInterface::class)->disableOriginalConstructor()->getMock();
+        $this->dateTimeConstraint = $this->getMockBuilder(DateTimeConstraint::class)->disableOriginalConstructor()->getMock();
+        $this->activityYearConstraint = $this->getMockBuilder(ActivityYearConstraint::class)->disableOriginalConstructor()->getMock();
+    }
+
+    public function getSUTMockForMethod(string $methodName): MockObject | TestableFiltersConfigurationService {
+        return $this
+            ->getMockBuilder(TestableFiltersConfigurationService::class)
+            ->setConstructorArgs([$this->em, $this->dateTimeConstraint, $this->activityYearConstraint])
+            ->setMethodsExcept([$methodName])
+            ->getMock();
+    }
+
+    public function testGetFilters(): void {
+        $filterConfigurationService = $this->getSUTMockForMethod('getFilters');
+
+        $filterCollection = $this->getMockBuilder(FilterCollection::class)->disableOriginalConstructor()->getMock();
+
+        $this->em->expects(self::once())->method('getFilters')->willReturn($filterCollection);
+
+        $this->assertEquals(
+            $filterCollection,
+            $filterConfigurationService->getFilters()
+        );
+
+    }
+
+    public function testConfigureTimeConstraintFilters(): void {
+        $filterConfigurationService = $this->getSUTMockForMethod('configureTimeConstraintFilters');
+
+        $accessId = 123;
+
+        $filterCollection = $this->getMockBuilder(FilterCollection::class)->disableOriginalConstructor()->getMock();
+
+        $filterConfigurationService
+            ->method('getFilters')
+            ->willReturn($filterCollection);
+
+        $datetimeFilter = $this->getMockBuilder(DatetimeFilter::class)->disableOriginalConstructor()->getMock();
+        $activityYearFilter = $this->getMockBuilder(ActivityYearFilter::class)->disableOriginalConstructor()->getMock();
+
+        $filterCollection
+            ->expects(self::exactly(2))
+            ->method('enable')
+            ->willReturnMap([
+                ['date_time_filter', $datetimeFilter],
+                ['activity_year_filter', $activityYearFilter],
+            ]);
+
+        $datetimeFilter->expects(self::once())->method('setAccessId')->with($accessId);
+        $datetimeFilter->expects(self::once())->method('setTimeConstraint')->with($this->dateTimeConstraint);
+
+        $activityYearFilter->expects(self::once())->method('setAccessId')->with($accessId);
+        $activityYearFilter->expects(self::once())->method('setTimeConstraint')->with($this->activityYearConstraint);
+
+        $filterConfigurationService->configureTimeConstraintFilters($accessId);
+    }
+
+}