From a921009eff79109b7aa6268a5f0761fe9024f4ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Mon, 9 Mar 2026 13:00:30 +0000 Subject: [PATCH 1/4] Add seeds to data fixtures, to avoid random failures in tests --- .changes/unreleased/DX-20260309-131710.yaml | 7 +++++++ config/packages/nelmio_alice.yaml | 1 + .../DataFixtures/ORM/LoadActivity.php | 5 +++-- .../DataFixtures/ORM/LoadAsideActivity.php | 7 +++++-- .../DataFixtures/ORM/LoadOption.php | 5 +++-- .../DataFixtures/ORM/LoadParticipation.php | 5 +++-- .../ORM/LoadAddressReferences.php | 7 ++++--- .../Helper/RandomPersonHelperTrait.php | 2 +- .../DataFixtures/ORM/LoadCustomFields.php | 8 +++++--- .../DataFixtures/ORM/LoadHousehold.php | 19 ++++++++++--------- .../DataFixtures/ORM/LoadPeople.php | 11 ++++++----- .../DataFixtures/ORM/LoadRelationships.php | 9 ++++++--- .../RelationshipApiControllerTest.php | 10 +++++++++- .../DataFixtures/ORM/LoadCustomField.php | 11 ++++++++--- .../DataFixtures/ORM/LoadReports.php | 9 +++++---- .../DataFixtures/ORM/LoadThirdParty.php | 3 ++- 16 files changed, 78 insertions(+), 41 deletions(-) create mode 100644 .changes/unreleased/DX-20260309-131710.yaml diff --git a/.changes/unreleased/DX-20260309-131710.yaml b/.changes/unreleased/DX-20260309-131710.yaml new file mode 100644 index 000000000..865c1fe6f --- /dev/null +++ b/.changes/unreleased/DX-20260309-131710.yaml @@ -0,0 +1,7 @@ +kind: DX +body: Add seeds in DataFixtures and in some tests to avoid random test failures +time: 2026-03-09T13:17:10.915852317+01:00 +custom: + Issue: "504" + MR: "970" + SchemaChange: No schema change diff --git a/config/packages/nelmio_alice.yaml b/config/packages/nelmio_alice.yaml index e82c32982..e6e912228 100644 --- a/config/packages/nelmio_alice.yaml +++ b/config/packages/nelmio_alice.yaml @@ -8,5 +8,6 @@ when@dev: &dev - 'file' - 'md5' - 'sha1' + seed: 1234567890 when@test: *dev diff --git a/src/Bundle/ChillActivityBundle/DataFixtures/ORM/LoadActivity.php b/src/Bundle/ChillActivityBundle/DataFixtures/ORM/LoadActivity.php index 80c1e4254..8736e78f5 100644 --- a/src/Bundle/ChillActivityBundle/DataFixtures/ORM/LoadActivity.php +++ b/src/Bundle/ChillActivityBundle/DataFixtures/ORM/LoadActivity.php @@ -33,6 +33,7 @@ class LoadActivity extends AbstractFixture implements OrderedFixtureInterface public function __construct(private readonly EntityManagerInterface $em) { + mt_srand(123456789); $this->faker = FakerFactory::create('fr_FR'); } @@ -48,7 +49,7 @@ class LoadActivity extends AbstractFixture implements OrderedFixtureInterface ->findAll(); foreach ($persons as $person) { - $activityNbr = random_int(0, 3); + $activityNbr = mt_rand(0, 3); for ($i = 0; $i < $activityNbr; ++$i) { $activity = $this->newRandomActivity($person); @@ -73,7 +74,7 @@ class LoadActivity extends AbstractFixture implements OrderedFixtureInterface // ->setAttendee($this->faker->boolean()) - for ($i = 0; random_int(0, 4) > $i; ++$i) { + for ($i = 0; mt_rand(0, 4) > $i; ++$i) { $reason = $this->getRandomActivityReason(); if (null !== $reason) { diff --git a/src/Bundle/ChillAsideActivityBundle/src/DataFixtures/ORM/LoadAsideActivity.php b/src/Bundle/ChillAsideActivityBundle/src/DataFixtures/ORM/LoadAsideActivity.php index a60398507..cb778ab1e 100644 --- a/src/Bundle/ChillAsideActivityBundle/src/DataFixtures/ORM/LoadAsideActivity.php +++ b/src/Bundle/ChillAsideActivityBundle/src/DataFixtures/ORM/LoadAsideActivity.php @@ -21,7 +21,10 @@ use Doctrine\Persistence\ObjectManager; class LoadAsideActivity extends Fixture implements DependentFixtureInterface { - public function __construct(private readonly UserRepository $userRepository) {} + public function __construct(private readonly UserRepository $userRepository) + { + mt_srand(123456789); + } public function getDependencies(): array { @@ -47,7 +50,7 @@ class LoadAsideActivity extends Fixture implements DependentFixtureInterface $this->getReference('aside_activity_category_0', AsideActivityCategory::class) ) ->setDate((new \DateTimeImmutable('today')) - ->sub(new \DateInterval('P'.\random_int(1, 100).'D'))); + ->sub(new \DateInterval('P'.\mt_rand(1, 100).'D'))); $manager->persist($activity); } diff --git a/src/Bundle/ChillCustomFieldsBundle/DataFixtures/ORM/LoadOption.php b/src/Bundle/ChillCustomFieldsBundle/DataFixtures/ORM/LoadOption.php index 4da28baac..87cf46024 100644 --- a/src/Bundle/ChillCustomFieldsBundle/DataFixtures/ORM/LoadOption.php +++ b/src/Bundle/ChillCustomFieldsBundle/DataFixtures/ORM/LoadOption.php @@ -41,6 +41,7 @@ class LoadOption extends AbstractFixture implements OrderedFixtureInterface public function __construct() { + mt_srand(123456789); $this->fakerFr = \Faker\Factory::create('fr_FR'); $this->fakerEn = \Faker\Factory::create('en_EN'); $this->fakerNl = \Faker\Factory::create('nl_NL'); @@ -104,7 +105,7 @@ class LoadOption extends AbstractFixture implements OrderedFixtureInterface $manager->persist($parent); // Load children - $expected_nb_children = random_int(10, 50); + $expected_nb_children = mt_rand(10, 50); for ($i = 0; $i < $expected_nb_children; ++$i) { $companyName = $this->fakerFr->company; @@ -144,7 +145,7 @@ class LoadOption extends AbstractFixture implements OrderedFixtureInterface $manager->persist($parent); // Load children - $expected_nb_children = random_int(10, 50); + $expected_nb_children = mt_rand(10, 50); for ($i = 0; $i < $expected_nb_children; ++$i) { $manager->persist($this->createChildOption($parent, [ diff --git a/src/Bundle/ChillEventBundle/DataFixtures/ORM/LoadParticipation.php b/src/Bundle/ChillEventBundle/DataFixtures/ORM/LoadParticipation.php index 94167f6b8..c5d7e37fb 100644 --- a/src/Bundle/ChillEventBundle/DataFixtures/ORM/LoadParticipation.php +++ b/src/Bundle/ChillEventBundle/DataFixtures/ORM/LoadParticipation.php @@ -34,6 +34,7 @@ class LoadParticipation extends AbstractFixture implements OrderedFixtureInterfa public function __construct() { + mt_srand(123456789); $this->faker = \Faker\Factory::create('fr_FR'); } @@ -45,7 +46,7 @@ class LoadParticipation extends AbstractFixture implements OrderedFixtureInterfa for ($i = 0; $i < $expectedNumber; ++$i) { $event = (new Event()) ->setDate($this->faker->dateTimeBetween('-2 years', '+6 months')) - ->setName($this->faker->words(random_int(2, 4), true)) + ->setName($this->faker->words(mt_rand(2, 4), true)) ->setType($this->getReference(LoadEventTypes::$refs[array_rand(LoadEventTypes::$refs)], EventType::class)) ->setCenter($center) ->setCircle( @@ -78,7 +79,7 @@ class LoadParticipation extends AbstractFixture implements OrderedFixtureInterfa /** @var Person $person */ foreach ($people as $person) { - $nb = random_int(0, 3); + $nb = mt_rand(0, 3); for ($i = 0; $i < $nb; ++$i) { $event = $events[array_rand($events)]; diff --git a/src/Bundle/ChillMainBundle/DataFixtures/ORM/LoadAddressReferences.php b/src/Bundle/ChillMainBundle/DataFixtures/ORM/LoadAddressReferences.php index 1c71e17cb..55e52b65e 100644 --- a/src/Bundle/ChillMainBundle/DataFixtures/ORM/LoadAddressReferences.php +++ b/src/Bundle/ChillMainBundle/DataFixtures/ORM/LoadAddressReferences.php @@ -31,6 +31,7 @@ class LoadAddressReferences extends AbstractFixture implements ContainerAwareInt public function __construct() { + mt_srand(123456789); $this->faker = \Faker\Factory::create('fr_FR'); } @@ -67,7 +68,7 @@ class LoadAddressReferences extends AbstractFixture implements ContainerAwareInt $ar->setRefId($this->faker->numerify('ref-id-######')); $ar->setStreet($this->faker->streetName); - $ar->setStreetNumber((string) random_int(0, 199)); + $ar->setStreetNumber((string) mt_rand(0, 199)); $ar->setPoint($this->getRandomPoint()); $ar->setPostcode($this->getReference( LoadPostalCodes::$refs[array_rand(LoadPostalCodes::$refs)], @@ -88,8 +89,8 @@ class LoadAddressReferences extends AbstractFixture implements ContainerAwareInt { $lonBrussels = 4.35243; $latBrussels = 50.84676; - $lon = $lonBrussels + 0.01 * random_int(-5, 5); - $lat = $latBrussels + 0.01 * random_int(-5, 5); + $lon = $lonBrussels + 0.01 * mt_rand(-5, 5); + $lat = $latBrussels + 0.01 * mt_rand(-5, 5); return Point::fromLonLat($lon, $lat); } diff --git a/src/Bundle/ChillPersonBundle/DataFixtures/Helper/RandomPersonHelperTrait.php b/src/Bundle/ChillPersonBundle/DataFixtures/Helper/RandomPersonHelperTrait.php index 942f6cc5a..dfe6b37ad 100644 --- a/src/Bundle/ChillPersonBundle/DataFixtures/Helper/RandomPersonHelperTrait.php +++ b/src/Bundle/ChillPersonBundle/DataFixtures/Helper/RandomPersonHelperTrait.php @@ -34,7 +34,7 @@ trait RandomPersonHelperTrait return $qb ->select('p') ->setMaxResults(1) - ->setFirstResult(\random_int(0, $this->nbOfPersons)) + ->setFirstResult(\mt_rand(0, $this->nbOfPersons)) ->getQuery() ->getSingleResult(); } diff --git a/src/Bundle/ChillPersonBundle/DataFixtures/ORM/LoadCustomFields.php b/src/Bundle/ChillPersonBundle/DataFixtures/ORM/LoadCustomFields.php index 5bff01f20..058c80c39 100644 --- a/src/Bundle/ChillPersonBundle/DataFixtures/ORM/LoadCustomFields.php +++ b/src/Bundle/ChillPersonBundle/DataFixtures/ORM/LoadCustomFields.php @@ -37,7 +37,9 @@ class LoadCustomFields extends AbstractFixture implements OrderedFixtureInterfac private readonly EntityManagerInterface $entityManager, private readonly CustomFieldChoice $customFieldChoice, private readonly CustomFieldText $customFieldText, - ) {} + ) { + mt_srand(123456789); + } // put your code here public function getOrder(): int @@ -78,12 +80,12 @@ class LoadCustomFields extends AbstractFixture implements OrderedFixtureInterfac // select a set of people and add data foreach ($personIds as $id) { // add info on 1 person on 2 - if (1 === random_int(0, 1)) { + if (1 === mt_rand(0, 1)) { /** @var Person $person */ $person = $manager->getRepository(Person::class)->find($id); $person->setCFData([ 'remarques' => $this->createCustomFieldText() - ->serialize($faker->text(random_int(150, 250)), $this->cfText), + ->serialize($faker->text(mt_rand(150, 250)), $this->cfText), 'document-d-identite' => $this->createCustomFieldChoice() ->serialize([$choices[array_rand($choices)]], $this->cfChoice), ]); diff --git a/src/Bundle/ChillPersonBundle/DataFixtures/ORM/LoadHousehold.php b/src/Bundle/ChillPersonBundle/DataFixtures/ORM/LoadHousehold.php index fdefa5dc8..bedf29002 100644 --- a/src/Bundle/ChillPersonBundle/DataFixtures/ORM/LoadHousehold.php +++ b/src/Bundle/ChillPersonBundle/DataFixtures/ORM/LoadHousehold.php @@ -36,6 +36,7 @@ class LoadHousehold extends Fixture implements DependentFixtureInterface public function __construct(private readonly MembersEditorFactory $editorFactory, private readonly EntityManagerInterface $em) { + mt_srand(123456789); $this->loader = new NativeLoader(); } @@ -72,12 +73,12 @@ class LoadHousehold extends Fixture implements DependentFixtureInterface private function addAddressToHousehold(Household $household, \DateTimeImmutable $date, ObjectManager $manager) { - if (\random_int(0, 10) > 8) { + if (\mt_rand(0, 10) > 8) { // 20% of household without address return; } - $nb = \random_int(1, 6); + $nb = \mt_rand(1, 6); $i = 0; @@ -85,15 +86,15 @@ class LoadHousehold extends Fixture implements DependentFixtureInterface $address = $this->createAddress(); $address->setValidFrom(\DateTime::createFromImmutable($date)); - if (\random_int(0, 20) < 1) { - $date = $date->add(new \DateInterval('P'.\random_int(8, 52).'W')); + if (\mt_rand(0, 20) < 1) { + $date = $date->add(new \DateInterval('P'.\mt_rand(8, 52).'W')); $address->setValidTo(\DateTime::createFromImmutable($date)); } $household->addAddress($address); $manager->persist($address); - $date = $date->add(new \DateInterval('P'.\random_int(8, 52).'W')); + $date = $date->add(new \DateInterval('P'.\mt_rand(8, 52).'W')); ++$i; } } @@ -127,7 +128,7 @@ class LoadHousehold extends Fixture implements DependentFixtureInterface $k = 0; foreach ($this->getRandomPersons(1, 3) as $person) { - $date = $startDate->add(new \DateInterval('P'.\random_int(1, 200).'W')); + $date = $startDate->add(new \DateInterval('P'.\mt_rand(1, 200).'W')); $position = $this->getReference(LoadHouseholdPosition::ADULT, Position::class); $movement->addMovement($date, $person, $position, 0 === $k, 'self generated'); @@ -136,7 +137,7 @@ class LoadHousehold extends Fixture implements DependentFixtureInterface // load children foreach ($this->getRandomPersons(0, 3) as $person) { - $date = $startDate->add(new \DateInterval('P'.\random_int(1, 200).'W')); + $date = $startDate->add(new \DateInterval('P'.\mt_rand(1, 200).'W')); $position = $this->getReference(LoadHouseholdPosition::CHILD, Position::class); $movement->addMovement($date, $person, $position, 0 === $k, 'self generated'); @@ -145,7 +146,7 @@ class LoadHousehold extends Fixture implements DependentFixtureInterface // load children out foreach ($this->getRandomPersons(0, 2) as $person) { - $date = $startDate->add(new \DateInterval('P'.\random_int(1, 200).'W')); + $date = $startDate->add(new \DateInterval('P'.\mt_rand(1, 200).'W')); $position = $this->getReference(LoadHouseholdPosition::CHILD_OUT, Position::class); $movement->addMovement($date, $person, $position, 0 === $k, 'self generated'); @@ -169,7 +170,7 @@ class LoadHousehold extends Fixture implements DependentFixtureInterface { $persons = []; - $nb = \random_int($min, $max); + $nb = \mt_rand($min, $max); for ($i = 0; $i < $nb; ++$i) { $personId = \array_pop($this->personIds)['id']; diff --git a/src/Bundle/ChillPersonBundle/DataFixtures/ORM/LoadPeople.php b/src/Bundle/ChillPersonBundle/DataFixtures/ORM/LoadPeople.php index 753094ec1..b048ba3d4 100644 --- a/src/Bundle/ChillPersonBundle/DataFixtures/ORM/LoadPeople.php +++ b/src/Bundle/ChillPersonBundle/DataFixtures/ORM/LoadPeople.php @@ -240,6 +240,7 @@ class LoadPeople extends AbstractFixture implements ContainerAwareInterface, Ord protected UserRepository $userRepository, protected GenderRepository $genderRepository, ) { + mt_srand(123456789); $this->faker = Factory::create('fr_FR'); $this->faker->addProvider($this); $this->loader = new NativeLoader($this->faker); @@ -273,7 +274,7 @@ class LoadPeople extends AbstractFixture implements ContainerAwareInterface, Ord $this->cacheCountries = $this->countryRepository->findAll(); } - if (\random_int(0, 100) > $nullPercentage) { + if (\mt_rand(0, 100) > $nullPercentage) { return null; } @@ -289,7 +290,7 @@ class LoadPeople extends AbstractFixture implements ContainerAwareInterface, Ord $this->cacheGenders = $this->genderRepository->findByActiveOrdered(); } - if (\random_int(0, 100) > $nullPercentage) { + if (\mt_rand(0, 100) > $nullPercentage) { return null; } @@ -307,7 +308,7 @@ class LoadPeople extends AbstractFixture implements ContainerAwareInterface, Ord $this->cacheMaritalStatuses = $this->maritalStatusRepository->findAll(); } - if (\random_int(0, 100) > $nullPercentage) { + if (\mt_rand(0, 100) > $nullPercentage) { return null; } @@ -352,7 +353,7 @@ class LoadPeople extends AbstractFixture implements ContainerAwareInterface, Ord $accompanyingPeriod = new AccompanyingPeriod( (new \DateTime()) ->sub( - new \DateInterval('P'.\random_int(0, 180).'D') + new \DateInterval('P'.\mt_rand(0, 180).'D') ) ); $accompanyingPeriod->setCreatedBy($this->getRandomUser()) @@ -360,7 +361,7 @@ class LoadPeople extends AbstractFixture implements ContainerAwareInterface, Ord $person->addAccompanyingPeriod($accompanyingPeriod); $accompanyingPeriod->addSocialIssue($this->getRandomSocialIssue()); - if (\random_int(0, 10) > 3) { + if (\mt_rand(0, 10) > 3) { // always add social scope: $accompanyingPeriod->addScope($this->getReference('scope_social', Scope::class)); $origin = $this->getReference(LoadAccompanyingPeriodOrigin::ACCOMPANYING_PERIOD_ORIGIN, AccompanyingPeriod\Origin::class); diff --git a/src/Bundle/ChillPersonBundle/DataFixtures/ORM/LoadRelationships.php b/src/Bundle/ChillPersonBundle/DataFixtures/ORM/LoadRelationships.php index dcbb3a4c3..957074014 100644 --- a/src/Bundle/ChillPersonBundle/DataFixtures/ORM/LoadRelationships.php +++ b/src/Bundle/ChillPersonBundle/DataFixtures/ORM/LoadRelationships.php @@ -25,7 +25,10 @@ class LoadRelationships extends Fixture implements DependentFixtureInterface { use PersonRandomHelper; - public function __construct(private readonly EntityManagerInterface $em) {} + public function __construct(private readonly EntityManagerInterface $em) + { + mt_srand(123456789); + } public function getDependencies(): array { @@ -47,8 +50,8 @@ class LoadRelationships extends Fixture implements DependentFixtureInterface ->setFromPerson($this->getRandomPerson($this->em)) ->setToPerson($this->getRandomPerson($this->em)) ->setRelation($this->getReference(LoadRelations::RELATION_KEY. - random_int(0, \count(LoadRelations::RELATIONS) - 1), Relation::class)) - ->setReverse((bool) random_int(0, 1)) + mt_rand(0, \count(LoadRelations::RELATIONS) - 1), Relation::class)) + ->setReverse((bool) mt_rand(0, 1)) ->setCreatedBy($user) ->setUpdatedBy($user) ->setCreatedAt($date) diff --git a/src/Bundle/ChillPersonBundle/Tests/Controller/RelationshipApiControllerTest.php b/src/Bundle/ChillPersonBundle/Tests/Controller/RelationshipApiControllerTest.php index 65f2e60a6..ded2e4b1b 100644 --- a/src/Bundle/ChillPersonBundle/Tests/Controller/RelationshipApiControllerTest.php +++ b/src/Bundle/ChillPersonBundle/Tests/Controller/RelationshipApiControllerTest.php @@ -55,6 +55,9 @@ final class RelationshipApiControllerTest extends WebTestCase public static function personProvider(): array { + // fix a seed to avoid random errors + mt_srand(1234588755); + self::bootKernel(); $em = self::getContainer()->get(EntityManagerInterface::class); $personIdHavingRelation = $em->createQueryBuilder() @@ -116,6 +119,9 @@ final class RelationshipApiControllerTest extends WebTestCase public static function relationProvider(): array { + // fix a seed to avoid random errors + mt_srand(1234588755); + self::bootKernel(); $em = self::getContainer()->get(EntityManagerInterface::class); $personIdWithoutRelations = $em->createQueryBuilder() @@ -144,6 +150,8 @@ final class RelationshipApiControllerTest extends WebTestCase ->findAll(); } - return self::$relations[\array_rand(self::$relations)]; + $keys = array_keys(self::$relations); + + return self::$relations[mt_rand(0, \count($keys) - 1)]; } } diff --git a/src/Bundle/ChillReportBundle/DataFixtures/ORM/LoadCustomField.php b/src/Bundle/ChillReportBundle/DataFixtures/ORM/LoadCustomField.php index 707133890..c346f2bff 100644 --- a/src/Bundle/ChillReportBundle/DataFixtures/ORM/LoadCustomField.php +++ b/src/Bundle/ChillReportBundle/DataFixtures/ORM/LoadCustomField.php @@ -22,6 +22,11 @@ use Doctrine\Persistence\ObjectManager; */ class LoadCustomField extends AbstractFixture implements OrderedFixtureInterface { + public function __construct() + { + mt_srand(123456789); + } + public function getOrder(): int { return 15001; @@ -67,15 +72,15 @@ class LoadCustomField extends AbstractFixture implements OrderedFixtureInterface ]; for ($i = 0; 25 >= $i; ++$i) { - $cFType = $cFTypes[random_int(0, \count($cFTypes) - 1)]; + $cFType = $cFTypes[mt_rand(0, \count($cFTypes) - 1)]; $customField = (new CustomField()) ->setSlug("cf_report_{$i}") ->setType($cFType['type']) ->setOptions($cFType['options']) ->setName(['fr' => "CustomField {$i}"]) - ->setOrdering(random_int(0, 1000) / 1000) - ->setCustomFieldsGroup($this->getReference('cf_group_report_'.random_int(0, 3), CustomFieldsGroup::class)); + ->setOrdering(mt_rand(0, 1000) / 1000) + ->setCustomFieldsGroup($this->getReference('cf_group_report_'.mt_rand(0, 3), CustomFieldsGroup::class)); $manager->persist($customField); } diff --git a/src/Bundle/ChillReportBundle/DataFixtures/ORM/LoadReports.php b/src/Bundle/ChillReportBundle/DataFixtures/ORM/LoadReports.php index 3519ef4cd..ef966805e 100644 --- a/src/Bundle/ChillReportBundle/DataFixtures/ORM/LoadReports.php +++ b/src/Bundle/ChillReportBundle/DataFixtures/ORM/LoadReports.php @@ -35,6 +35,7 @@ final class LoadReports extends AbstractFixture implements OrderedFixtureInterfa public function __construct( private readonly EntityManagerInterface $entityManager, ) { + mt_srand(123456789); $this->faker = FakerFactory::create('fr_FR'); } @@ -83,7 +84,7 @@ final class LoadReports extends AbstractFixture implements OrderedFixtureInterfa $report = (new Report()) ->setPerson($person) ->setCFGroup( - random_int(0, 10) > 5 ? + mt_rand(0, 10) > 5 ? $this->getReference('cf_group_report_logement', CustomFieldsGroup::class) : $this->getReference('cf_group_report_education', CustomFieldsGroup::class) ) @@ -106,7 +107,7 @@ final class LoadReports extends AbstractFixture implements OrderedFixtureInterfa // set date. 30% of the dates are 2015-05-01 $expectedDate = new \DateTime('2015-01-05'); - if (random_int(0, 100) < 30) { + if (mt_rand(0, 100) < 30) { $report->setDate($expectedDate); } else { $report->setDate($this->faker->dateTimeBetween('-1 year', 'now') @@ -150,7 +151,7 @@ final class LoadReports extends AbstractFixture implements OrderedFixtureInterfa $selectedPeople = []; foreach ($people as $person) { - if (random_int(0, 100) < $percentage) { + if (mt_rand(0, 100) < $percentage) { $selectedPeople[] = $person; } } @@ -178,7 +179,7 @@ final class LoadReports extends AbstractFixture implements OrderedFixtureInterfa $picked = []; if ($multiple) { - $numberSelected = random_int(1, \count($choices) - 1); + $numberSelected = mt_rand(1, \count($choices) - 1); for ($i = 0; $i < $numberSelected; ++$i) { $picked[] = $this->pickChoice($choices); diff --git a/src/Bundle/ChillThirdPartyBundle/DataFixtures/ORM/LoadThirdParty.php b/src/Bundle/ChillThirdPartyBundle/DataFixtures/ORM/LoadThirdParty.php index dc4b5472b..8ed008ddf 100644 --- a/src/Bundle/ChillThirdPartyBundle/DataFixtures/ORM/LoadThirdParty.php +++ b/src/Bundle/ChillThirdPartyBundle/DataFixtures/ORM/LoadThirdParty.php @@ -30,6 +30,7 @@ class LoadThirdParty extends Fixture implements DependentFixtureInterface public function __construct() { + mt_srand(123456789); $this->phoneNumberUtil = PhoneNumberUtil::getInstance(); } @@ -68,7 +69,7 @@ class LoadThirdParty extends Fixture implements DependentFixtureInterface static fn ($a) => $a['ref'], LoadCenters::$centers ); - $number = random_int(1, \count($references)); + $number = mt_rand(1, \count($references)); if (1 === $number) { yield $this->getReference($references[array_rand($references)], Center::class); From dd429ca02ac2cb2b31d7c4778af766081d3d3378 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Mon, 16 Mar 2026 14:08:35 +0000 Subject: [PATCH 2/4] Resolve "Notification aux groupes utilisateurs" --- .../ChillMainBundle/Entity/Notification.php | 8 +- src/Bundle/ChillMainBundle/Entity/User.php | 5 + .../ChillMainBundle/Entity/UserGroup.php | 15 ++ .../SendImmediateNotificationEmailHandler.php | 22 +- .../SendImmediateNotificationEmailMessage.php | 35 ++- .../Notification/Email/NotificationMailer.php | 32 +-- .../Repository/NotificationRepository.php | 2 +- .../Repository/UserGroupRepository.php | 2 +- ...il_non_system_notification_content.md.twig | 4 + ...l_non_system_notification_content.txt.twig | 4 + ...dImmediateNotificationEmailHandlerTest.php | 187 +++++++++++++++ .../Email/NotificationMailTwigContentTest.php | 71 ++++++ .../Email/NotificationMailerTest.php | 223 ++++++++++++++++-- 13 files changed, 562 insertions(+), 48 deletions(-) create mode 100644 src/Bundle/ChillMainBundle/Tests/Notification/Email/NotificationEmailHandler/SendImmediateNotificationEmailHandlerTest.php create mode 100644 src/Bundle/ChillMainBundle/Tests/Notification/Email/NotificationMailTwigContentTest.php diff --git a/src/Bundle/ChillMainBundle/Entity/Notification.php b/src/Bundle/ChillMainBundle/Entity/Notification.php index 20773b884..8075ac638 100644 --- a/src/Bundle/ChillMainBundle/Entity/Notification.php +++ b/src/Bundle/ChillMainBundle/Entity/Notification.php @@ -215,17 +215,21 @@ class Notification implements TrackUpdateInterface return $this->addressees; } + /** + * @return list + */ public function getAllAddressees(): array { $allUsers = []; foreach ($this->getAddressees() as $user) { - $allUsers[$user->getId()] = $user; + $allUsers['u_'.$user->getId()] = $user; } foreach ($this->getAddresseeUserGroups() as $userGroup) { + $allUsers['ug_'.$userGroup->getId()] = $userGroup; foreach ($userGroup->getUsers() as $user) { - $allUsers[$user->getId()] = $user; + $allUsers['u_'.$user->getId()] = $user; } } diff --git a/src/Bundle/ChillMainBundle/Entity/User.php b/src/Bundle/ChillMainBundle/Entity/User.php index b5539aa83..a273fefd7 100644 --- a/src/Bundle/ChillMainBundle/Entity/User.php +++ b/src/Bundle/ChillMainBundle/Entity/User.php @@ -658,6 +658,11 @@ class User implements UserInterface, \Stringable, PasswordAuthenticatedUserInter return true; } + public function isUserGroup(): bool + { + return false; + } + private function getNotificationFlagData(string $flag): array { return $this->notificationFlags[$flag] ?? [self::NOTIF_FLAG_IMMEDIATE_EMAIL]; diff --git a/src/Bundle/ChillMainBundle/Entity/UserGroup.php b/src/Bundle/ChillMainBundle/Entity/UserGroup.php index 39df04b31..f6586d4c9 100644 --- a/src/Bundle/ChillMainBundle/Entity/UserGroup.php +++ b/src/Bundle/ChillMainBundle/Entity/UserGroup.php @@ -256,6 +256,21 @@ class UserGroup return true; } + public function isUser(): bool + { + return false; + } + + /** + * Return a locale for the userGroup. + * + * Currently hardcoded, should be replaced by a property. + */ + public function getLocale(): string + { + return 'fr'; + } + public function contains(User $user): bool { return $this->users->contains($user); diff --git a/src/Bundle/ChillMainBundle/Notification/Email/NotificationEmailHandlers/SendImmediateNotificationEmailHandler.php b/src/Bundle/ChillMainBundle/Notification/Email/NotificationEmailHandlers/SendImmediateNotificationEmailHandler.php index b27f16423..26648b5a5 100644 --- a/src/Bundle/ChillMainBundle/Notification/Email/NotificationEmailHandlers/SendImmediateNotificationEmailHandler.php +++ b/src/Bundle/ChillMainBundle/Notification/Email/NotificationEmailHandlers/SendImmediateNotificationEmailHandler.php @@ -14,7 +14,8 @@ namespace Chill\MainBundle\Notification\Email\NotificationEmailHandlers; use Chill\MainBundle\Notification\Email\NotificationEmailMessages\SendImmediateNotificationEmailMessage; use Chill\MainBundle\Notification\Email\NotificationMailer; use Chill\MainBundle\Repository\NotificationRepository; -use Chill\MainBundle\Repository\UserRepository; +use Chill\MainBundle\Repository\UserGroupRepository; +use Chill\MainBundle\Repository\UserRepositoryInterface; use Psr\Log\LoggerInterface; use Symfony\Component\Mailer\Exception\TransportExceptionInterface; use Symfony\Component\Messenger\Attribute\AsMessageHandler; @@ -24,7 +25,8 @@ readonly class SendImmediateNotificationEmailHandler { public function __construct( private NotificationRepository $notificationRepository, - private UserRepository $userRepository, + private UserRepositoryInterface $userRepository, + private UserGroupRepository $userGroupRepository, private NotificationMailer $notificationMailer, private LoggerInterface $logger, ) {} @@ -36,7 +38,13 @@ readonly class SendImmediateNotificationEmailHandler public function __invoke(SendImmediateNotificationEmailMessage $message): void { $notification = $this->notificationRepository->find($message->getNotificationId()); - $addressee = $this->userRepository->find($message->getAddresseeId()); + if (null !== $message->getUserId()) { + $addressee = $this->userRepository->find($message->getUserId()); + } elseif (null !== $message->getUserGroupId()) { + $addressee = $this->userGroupRepository->find($message->getUserGroupId()); + } else { + throw new \InvalidArgumentException('Addressee not found: nor an user nor a user group'); + } if (null === $notification) { $this->logger->error('[SendImmediateNotificationEmailHandler] Notification not found', [ @@ -48,10 +56,11 @@ readonly class SendImmediateNotificationEmailHandler if (null === $addressee) { $this->logger->error('[SendImmediateNotificationEmailHandler] Addressee not found', [ - 'addressee_id' => $message->getAddresseeId(), + 'user_id' => $message->getUserId(), + 'user_group_id' => $message->getUserGroupId(), ]); - throw new \InvalidArgumentException(sprintf('User with ID %s not found', $message->getAddresseeId())); + throw new \InvalidArgumentException(sprintf('User with ID %s or user group with id %s not found', $message->getUserId(), $message->getUserGroupId())); } try { @@ -59,7 +68,8 @@ readonly class SendImmediateNotificationEmailHandler } catch (\Exception $e) { $this->logger->error('[SendImmediateNotificationEmailHandler] Failed to send email', [ 'notification_id' => $message->getNotificationId(), - 'addressee_id' => $message->getAddresseeId(), + 'user_id' => $message->getUserId(), + 'user_group_id' => $message->getUserGroupId(), 'stacktrace' => $e->getTraceAsString(), ]); throw $e; diff --git a/src/Bundle/ChillMainBundle/Notification/Email/NotificationEmailMessages/SendImmediateNotificationEmailMessage.php b/src/Bundle/ChillMainBundle/Notification/Email/NotificationEmailMessages/SendImmediateNotificationEmailMessage.php index fb9908b21..82bc84ec1 100644 --- a/src/Bundle/ChillMainBundle/Notification/Email/NotificationEmailMessages/SendImmediateNotificationEmailMessage.php +++ b/src/Bundle/ChillMainBundle/Notification/Email/NotificationEmailMessages/SendImmediateNotificationEmailMessage.php @@ -11,20 +11,45 @@ declare(strict_types=1); namespace Chill\MainBundle\Notification\Email\NotificationEmailMessages; +use Chill\MainBundle\Entity\Notification; +use Chill\MainBundle\Entity\User; +use Chill\MainBundle\Entity\UserGroup; + readonly class SendImmediateNotificationEmailMessage { + private int $notificationId; + + private ?int $userId; + + private ?int $userGroupId; + public function __construct( - private int $notificationId, - private int $addresseeId, - ) {} + Notification $notification, + UserGroup|User $addressee, + ) { + $this->notificationId = $notification->getId(); + + if ($addressee instanceof User) { + $this->userId = $addressee->getId(); + $this->userGroupId = null; + } else { + $this->userGroupId = $addressee->getId(); + $this->userId = null; + } + } public function getNotificationId(): int { return $this->notificationId; } - public function getAddresseeId(): int + public function getUserId(): ?int { - return $this->addresseeId; + return $this->userId; + } + + public function getUserGroupId(): ?int + { + return $this->userGroupId; } } diff --git a/src/Bundle/ChillMainBundle/Notification/Email/NotificationMailer.php b/src/Bundle/ChillMainBundle/Notification/Email/NotificationMailer.php index 237cb178b..c1eb03c49 100644 --- a/src/Bundle/ChillMainBundle/Notification/Email/NotificationMailer.php +++ b/src/Bundle/ChillMainBundle/Notification/Email/NotificationMailer.php @@ -14,6 +14,7 @@ namespace Chill\MainBundle\Notification\Email; use Chill\MainBundle\Entity\Notification; use Chill\MainBundle\Entity\NotificationComment; use Chill\MainBundle\Entity\User; +use Chill\MainBundle\Entity\UserGroup; use Chill\MainBundle\Notification\Email\NotificationEmailMessages\SendImmediateNotificationEmailMessage; use Doctrine\ORM\Event\PostPersistEventArgs; use Psr\Log\LoggerInterface; @@ -26,13 +27,13 @@ use Symfony\Contracts\Translation\TranslatorInterface; // use Symfony\Component\Translation\LocaleSwitcher; -readonly class NotificationMailer +class NotificationMailer { public function __construct( - private MailerInterface $mailer, - private LoggerInterface $logger, - private MessageBusInterface $messageBus, - private TranslatorInterface $translator, + private readonly MailerInterface $mailer, + private readonly LoggerInterface $logger, + private readonly MessageBusInterface $messageBus, + private readonly TranslatorInterface $translator, // private LocaleSwitcher $localeSwitcher, ) {} @@ -100,25 +101,24 @@ readonly class NotificationMailer if (null === $addressee->getEmail()) { continue; } - $this->processNotificationForAddressee($notification, $addressee); } } - private function processNotificationForAddressee(Notification $notification, User $addressee): void + private function processNotificationForAddressee(Notification $notification, User|UserGroup $addressee): void { $notificationType = $notification->getType(); - if ($addressee->isNotificationSendImmediately($notificationType)) { + if ($addressee instanceof UserGroup || $addressee->isNotificationSendImmediately($notificationType)) { $this->scheduleImmediateEmail($notification, $addressee); } } - private function scheduleImmediateEmail(Notification $notification, User $addressee): void + private function scheduleImmediateEmail(Notification $notification, User|UserGroup $addressee): void { $message = new SendImmediateNotificationEmailMessage( - $notification->getId(), - $addressee->getId() + $notification, + $addressee, ); $this->messageBus->dispatch($message); @@ -130,13 +130,17 @@ readonly class NotificationMailer } /** - * This method sends the email but is now called by the immediate notification email message handler. + * Send an email about a Notification. + * + * It is called by immediate notification email message handler: + * + * @see{\Chill\MainBundle\Notification\Email\NotificationEmailHandlers\SendImmediateNotificationEmailHandler} * * @throws TransportExceptionInterface */ - public function sendEmailToAddressee(Notification $notification, User $addressee): void + public function sendEmailToAddressee(Notification $notification, User|UserGroup $addressee): void { - if (null === $addressee->getEmail()) { + if (null === $addressee->getEmail() || '' === $addressee->getEmail()) { return; } diff --git a/src/Bundle/ChillMainBundle/Repository/NotificationRepository.php b/src/Bundle/ChillMainBundle/Repository/NotificationRepository.php index 99fb57094..3e98c7496 100644 --- a/src/Bundle/ChillMainBundle/Repository/NotificationRepository.php +++ b/src/Bundle/ChillMainBundle/Repository/NotificationRepository.php @@ -23,7 +23,7 @@ use Doctrine\ORM\Query; use Doctrine\ORM\QueryBuilder; use Doctrine\Persistence\ObjectRepository; -final class NotificationRepository implements ObjectRepository +class NotificationRepository implements ObjectRepository { private ?Statement $notificationByRelatedEntityAndUserAssociatedStatement = null; diff --git a/src/Bundle/ChillMainBundle/Repository/UserGroupRepository.php b/src/Bundle/ChillMainBundle/Repository/UserGroupRepository.php index 71266f8e5..c7f9c43cc 100644 --- a/src/Bundle/ChillMainBundle/Repository/UserGroupRepository.php +++ b/src/Bundle/ChillMainBundle/Repository/UserGroupRepository.php @@ -18,7 +18,7 @@ use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityRepository; use Symfony\Contracts\Translation\LocaleAwareInterface; -final class UserGroupRepository implements UserGroupRepositoryInterface, LocaleAwareInterface +class UserGroupRepository implements UserGroupRepositoryInterface, LocaleAwareInterface { private readonly EntityRepository $repository; diff --git a/src/Bundle/ChillMainBundle/Resources/views/Notification/email_non_system_notification_content.md.twig b/src/Bundle/ChillMainBundle/Resources/views/Notification/email_non_system_notification_content.md.twig index 5c07e2ef7..854547c92 100644 --- a/src/Bundle/ChillMainBundle/Resources/views/Notification/email_non_system_notification_content.md.twig +++ b/src/Bundle/ChillMainBundle/Resources/views/Notification/email_non_system_notification_content.md.twig @@ -1,5 +1,9 @@ {% apply markdown_to_html %} +{% if dest.isUser %} {{ dest.label }}, +{% else %} +{{ dest.label|localize_translatable_string }}, +{% endif %} {{ notification.sender.label }} a créé une notification pour vous: diff --git a/src/Bundle/ChillMainBundle/Resources/views/Notification/email_non_system_notification_content.txt.twig b/src/Bundle/ChillMainBundle/Resources/views/Notification/email_non_system_notification_content.txt.twig index 58f138322..b14f77ef7 100644 --- a/src/Bundle/ChillMainBundle/Resources/views/Notification/email_non_system_notification_content.txt.twig +++ b/src/Bundle/ChillMainBundle/Resources/views/Notification/email_non_system_notification_content.txt.twig @@ -1,4 +1,8 @@ +{% if dest.isUser %} {{ dest.label }}, +{% else %} +{{ dest.label|localize_translatable_string }}, +{% endif %} {{ notification.sender.label }} a créé une notification pour vous: diff --git a/src/Bundle/ChillMainBundle/Tests/Notification/Email/NotificationEmailHandler/SendImmediateNotificationEmailHandlerTest.php b/src/Bundle/ChillMainBundle/Tests/Notification/Email/NotificationEmailHandler/SendImmediateNotificationEmailHandlerTest.php new file mode 100644 index 000000000..da1621390 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Tests/Notification/Email/NotificationEmailHandler/SendImmediateNotificationEmailHandlerTest.php @@ -0,0 +1,187 @@ +notificationRepository = $this->prophesize(NotificationRepository::class); + $this->userRepository = $this->prophesize(UserRepositoryInterface::class); + $this->userGroupRepository = $this->prophesize(UserGroupRepository::class); + $this->notificationMailer = $this->prophesize(NotificationMailer::class); + + $this->handler = new SendImmediateNotificationEmailHandler( + $this->notificationRepository->reveal(), + $this->userRepository->reveal(), + $this->userGroupRepository->reveal(), + $this->notificationMailer->reveal(), + new NullLogger() + ); + } + + public function testInvokeWithUserAddressee(): void + { + $notificationId = 123; + $userId = 456; + + $notification = $this->prophesize(Notification::class); + $notification->getId()->willReturn($notificationId); + $user = $this->prophesize(User::class); + $user->getId()->willReturn($userId); + + $message = new SendImmediateNotificationEmailMessage($notification->reveal(), $user->reveal()); + + $this->notificationRepository->find($notificationId)->willReturn($notification->reveal()); + $this->userRepository->find($userId)->willReturn($user->reveal()); + + $this->notificationMailer->sendEmailToAddressee($notification->reveal(), $user->reveal()) + ->shouldBeCalledOnce(); + + ($this->handler)($message); + } + + public function testInvokeWithUserGroupAddressee(): void + { + $notificationId = 123; + $userGroupId = 789; + + $notification = $this->prophesize(Notification::class); + $notification->getId()->willReturn($notificationId); + $userGroup = $this->prophesize(UserGroup::class); + $userGroup->getId()->willReturn($userGroupId); + + $message = new SendImmediateNotificationEmailMessage($notification->reveal(), $userGroup->reveal()); + + $this->notificationRepository->find($notificationId)->willReturn($notification->reveal()); + $this->userGroupRepository->find($userGroupId)->willReturn($userGroup->reveal()); + + $this->notificationMailer->sendEmailToAddressee($notification->reveal(), $userGroup->reveal()) + ->shouldBeCalledOnce(); + + ($this->handler)($message); + } + + public function testInvokeThrowsExceptionWhenNotificationNotFound(): void + { + $notificationId = 123; + $userId = 456; + + $notification = $this->prophesize(Notification::class); + $notification->getId()->willReturn($notificationId); + $user = $this->prophesize(User::class); + $user->getId()->willReturn($userId); + + $message = new SendImmediateNotificationEmailMessage($notification->reveal(), $user->reveal()); + + $this->notificationRepository->find($notificationId)->willReturn(null); + $this->userRepository->find($userId)->willReturn($user->reveal()); + + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage(sprintf('Notification with ID %s not found', $notificationId)); + + ($this->handler)($message); + } + + public function testInvokeThrowsExceptionWhenUserNotFound(): void + { + $notificationId = 123; + $userId = 456; + + $notification = $this->prophesize(Notification::class); + $notification->getId()->willReturn($notificationId); + $user = $this->prophesize(User::class); + $user->getId()->willReturn($userId); + + $message = new SendImmediateNotificationEmailMessage($notification->reveal(), $user->reveal()); + + $this->notificationRepository->find($notificationId)->willReturn($notification->reveal()); + $this->userRepository->find($userId)->willReturn(null); + + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage(sprintf('User with ID %s or user group with id %s not found', $userId, '')); + + ($this->handler)($message); + } + + public function testInvokeThrowsExceptionWhenUserGroupNotFound(): void + { + $notificationId = 123; + $userGroupId = 789; + + $notification = $this->prophesize(Notification::class); + $notification->getId()->willReturn($notificationId); + $userGroup = $this->prophesize(UserGroup::class); + $userGroup->getId()->willReturn($userGroupId); + + $message = new SendImmediateNotificationEmailMessage($notification->reveal(), $userGroup->reveal()); + + $this->notificationRepository->find($notificationId)->willReturn($notification->reveal()); + $this->userGroupRepository->find($userGroupId)->willReturn(null); + + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage(sprintf('User with ID %s or user group with id %s not found', '', $userGroupId)); + + ($this->handler)($message); + } + + public function testInvokeRethrowsExceptionWhenMailerFails(): void + { + $notificationId = 123; + $userId = 456; + + $notification = $this->prophesize(Notification::class); + $notification->getId()->willReturn($notificationId); + $user = $this->prophesize(User::class); + $user->getId()->willReturn($userId); + + $message = new SendImmediateNotificationEmailMessage($notification->reveal(), $user->reveal()); + + $this->notificationRepository->find($notificationId)->willReturn($notification->reveal()); + $this->userRepository->find($userId)->willReturn($user->reveal()); + + $exception = new \Exception('Mailer error'); + $this->notificationMailer->sendEmailToAddressee($notification->reveal(), $user->reveal()) + ->willThrow($exception); + + $this->expectException(\Exception::class); + $this->expectExceptionMessage('Mailer error'); + + ($this->handler)($message); + } +} diff --git a/src/Bundle/ChillMainBundle/Tests/Notification/Email/NotificationMailTwigContentTest.php b/src/Bundle/ChillMainBundle/Tests/Notification/Email/NotificationMailTwigContentTest.php new file mode 100644 index 000000000..23ae05261 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Tests/Notification/Email/NotificationMailTwigContentTest.php @@ -0,0 +1,71 @@ +twig = $this->getContainer()->get('twig'); + } + + /** + * @dataProvider provideContent + */ + public function testContent(string $template, array $args): void + { + $actual = $this->twig->render($template, $args); + + self::assertIsString($actual); + } + + public static function provideContent(): iterable + { + $notification = new Notification(); + $notification->setMessage('test message'); + $notification->setSender(new User()); + + $class = new \ReflectionClass($notification); + $method = $class->getProperty('id'); + $method->setValue($notification, 1); + + $txt = '@ChillMain/Notification/email_non_system_notification_content.txt.twig'; + $md = '@ChillMain/Notification/email_non_system_notification_content.md.twig'; + + $user = new User(); + $user->setLocale('fr'); + $user->setLabel('test'); + + $userGroup = new UserGroup(); + $userGroup->setLabel(['fr' => 'test user group']); + + foreach ([$md, $txt] as $template) { + yield 'test with a user for '.$template => [$template, ['notification' => $notification, 'dest' => $user]]; + yield 'test with a group for '.$template => [$template, ['notification' => $notification, 'dest' => $userGroup]]; + } + + } +} diff --git a/src/Bundle/ChillMainBundle/Tests/Notification/Email/NotificationMailerTest.php b/src/Bundle/ChillMainBundle/Tests/Notification/Email/NotificationMailerTest.php index 173cb8a0e..a13018a7f 100644 --- a/src/Bundle/ChillMainBundle/Tests/Notification/Email/NotificationMailerTest.php +++ b/src/Bundle/ChillMainBundle/Tests/Notification/Email/NotificationMailerTest.php @@ -9,11 +9,12 @@ declare(strict_types=1); * the LICENSE file that was distributed with this source code. */ -namespace Notification\Email; +namespace Chill\MainBundle\Tests\Notification\Email; use Chill\MainBundle\Entity\Notification; use Chill\MainBundle\Entity\NotificationComment; use Chill\MainBundle\Entity\User; +use Chill\MainBundle\Entity\UserGroup; use Chill\MainBundle\Notification\Email\NotificationMailer; use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\Event\PostPersistEventArgs; @@ -64,13 +65,22 @@ class NotificationMailerTest extends TestCase // a mail only to user1 and user3 should have been sent $mailer->send(Argument::that(function (Email $email) { foreach ($email->getTo() as $address) { - if ('user1@foo.com' === $address->getAddress() || 'user3@foo.com' === $address->getAddress()) { + if ('user1@foo.com' === $address->getAddress()) { return true; } } return false; - }))->shouldBeCalledTimes(2); + }))->shouldBeCalledTimes(1); + $mailer->send(Argument::that(function (Email $email) { + foreach ($email->getTo() as $address) { + if ('user3@foo.com' === $address->getAddress()) { + return true; + } + } + + return false; + }))->shouldBeCalledTimes(1); $objectManager = $this->prophesize(EntityManagerInterface::class); @@ -121,7 +131,83 @@ class NotificationMailerTest extends TestCase * @throws \ReflectionException * @throws Exception */ - public function testProcessNotificationForAddresseeWithImmediateEmailPreference(): void + public function testPostPersistNotificationToGroup(): void + { + // Create a real notification entity + $notification = new Notification(); + $notification->setType('test_notification_type'); + + // Use reflection to set the ID since it's normally generated by the database + $reflectionNotification = new \ReflectionClass(Notification::class); + $idProperty = $reflectionNotification->getProperty('id'); + $idProperty->setValue($notification, 123); + + // Create a real user entity + $user = new User(); + $user->setEmail('user@example.com'); + $userGroup = new UserGroup(); + $userGroup->addUser($user); + $notification->addAddressee($userGroup); + + // Use reflection to set the ID since it's normally generated by the database + $reflectionUser = new \ReflectionClass($user); + $idProperty = $reflectionUser->getProperty('id'); + $idProperty->setValue($user, 456); + + $reflectionUser = new \ReflectionClass($userGroup); + $idProperty = $reflectionUser->getProperty('id'); + $idProperty->setValue($userGroup, 789); + + // Set notification flags for the user + $user->setNotificationImmediately('test_notification_type', true); + + $messageBus = $this->prophesize(MessageBusInterface::class); + $messageBus->dispatch(Argument::that(fn (SendImmediateNotificationEmailMessage $message) => 123 === $message->getNotificationId() && 456 === $message->getUserId() && null === $message->getUserGroupId()))->willReturn(new Envelope(new \stdClass()))->shouldBeCalled(); + + $messageBus->dispatch(Argument::that(fn (SendImmediateNotificationEmailMessage $message) => 123 === $message->getNotificationId() && null === $message->getUserId() && 789 === $message->getUserGroupId()))->willReturn(new Envelope(new \stdClass()))->shouldBeCalled(); + + $notificationMailer = $this->buildNotificationMailer(null, $messageBus->reveal()); + + $notificationMailer->postPersistNotification($notification, new PostPersistEventArgs($notification, $this->prophesize(EntityManagerInterface::class)->reveal())); + } + + /** + * @throws \ReflectionException + * @throws Exception + */ + public function testPostPersistNotificationWithImmediateEmailPreference(): void + { + // Create a real notification entity + $notification = new Notification(); + $notification->setType('test_notification_type'); + + // Use reflection to set the ID since it's normally generated by the database + $reflectionNotification = new \ReflectionClass(Notification::class); + $idProperty = $reflectionNotification->getProperty('id'); + $idProperty->setValue($notification, 123); + + // Create a real user entity + $user = new User(); + $user->setEmail('user@example.com'); + $notification->addAddressee($user); + + // Use reflection to set the ID since it's normally generated by the database + $reflectionUser = new \ReflectionClass(User::class); + $idProperty = $reflectionUser->getProperty('id'); + $idProperty->setValue($user, 456); + + // Set notification flags for the user + $user->setNotificationImmediately('test_notification_type', true); + + $messageBus = $this->prophesize(MessageBusInterface::class); + $messageBus->dispatch(Argument::that(fn (SendImmediateNotificationEmailMessage $message) => 123 === $message->getNotificationId() && 456 === $message->getUserId() && null === $message->getUserGroupId()))->willReturn(new Envelope(new \stdClass()))->shouldBeCalled(); + + $notificationMailer = $this->buildNotificationMailer(null, $messageBus->reveal()); + + $notificationMailer->postPersistNotification($notification, new PostPersistEventArgs($notification, $this->prophesize(EntityManagerInterface::class)->reveal())); + } + + public function testPostPersistNotificationWithDailyDigestPreference(): void { // Create a real notification entity $notification = new Notification(); @@ -136,6 +222,11 @@ class NotificationMailerTest extends TestCase // Create a real user entity $user = new User(); $user->setEmail('user@example.com'); + // Set notification flags for the user + $user->setNotificationImmediately('test_notification_type', false); + $user->setNotificationDailyDigest('test_notification_type', true); + + $notification->addAddressee($user); // Use reflection to set the ID since it's normally generated by the database $reflectionUser = new \ReflectionClass(User::class); @@ -143,23 +234,15 @@ class NotificationMailerTest extends TestCase $idProperty->setAccessible(true); $idProperty->setValue($user, 456); - // Set notification flags for the user - $user->setNotificationImmediately('test_notification_type', true); + $messageBus = $this->prophesize(MessageBusInterface::class); + $messageBus->dispatch(Argument::that(fn (SendImmediateNotificationEmailMessage $message) => 123 === $message->getNotificationId() && 456 === $message->getUserId() && null === $message->getUserGroupId()))->willReturn(new Envelope(new \stdClass()))->shouldNotBeCalled(); - $messageBus = $this->createMock(MessageBusInterface::class); - $messageBus->expects($this->once()) - ->method('dispatch') - ->with($this->callback(fn (SendImmediateNotificationEmailMessage $message) => 123 === $message->getNotificationId() - && 456 === $message->getAddresseeId())) - ->willReturn(new Envelope(new \stdClass())); + $notificationMailer = $this->buildNotificationMailer( + null, + $messageBus->reveal() + ); - $mailer = $this->buildNotificationMailer(null, $messageBus); - - // Call the method that processes notifications - $reflection = new \ReflectionClass(NotificationMailer::class); - $method = $reflection->getMethod('processNotificationForAddressee'); - $method->setAccessible(true); - $method->invoke($mailer, $notification, $user); + $notificationMailer->postPersistNotification($notification, new PostPersistEventArgs($notification, $this->prophesize(EntityManagerInterface::class)->reveal())); } public function testSendDailyDigest(): void @@ -250,6 +333,108 @@ class NotificationMailerTest extends TestCase $notificationMailer->sendDailyDigest($user, $notifications); } + public function testSendEmailToAddresseeUser(): void + { + $user = new User(); + $user->setEmail('user@example.com'); + $notification = new Notification(); + $notification->setSender(new User()); + $notification->setTitle('Notification 1'); + $notification->setType('test_notification_type'); + $notification->addAddressee($user); + + $mailer = $this->prophesize(MailerInterface::class); + $mailer->send(Argument::that(function ($arg) { + if (!$arg instanceof Email) { + return false; + } + + if ('Notification 1' !== $arg->getSubject()) { + return false; + } + + foreach ($arg->getTo() as $address) { + if ('user@example.com' === $address->getAddress()) { + return true; + } + } + + return false; + }))->shouldBeCalledOnce(); + + $notificationMailer = $this->buildNotificationMailer($mailer->reveal()); + + $notificationMailer->sendEmailToAddressee($notification, $user); + } + + public function testSendEmailToAddresseeGroup(): void + { + $userGroup = new UserGroup(); + $userGroup->setEmail('user@example.com'); + $notification = new Notification(); + $notification->setSender(new User()); + $notification->setTitle('Notification 1'); + $notification->setType('test_notification_type'); + $notification->addAddressee($userGroup); + + $mailer = $this->prophesize(MailerInterface::class); + $mailer->send(Argument::that(function ($arg) { + if (!$arg instanceof Email) { + return false; + } + + if ('Notification 1' !== $arg->getSubject()) { + return false; + } + + foreach ($arg->getTo() as $address) { + if ('user@example.com' === $address->getAddress()) { + return true; + } + } + + return false; + }))->shouldBeCalledOnce(); + + $notificationMailer = $this->buildNotificationMailer($mailer->reveal()); + + $notificationMailer->sendEmailToAddressee($notification, $userGroup); + } + + public function testSendEmailToAddresseeGroupWithNoAddress(): void + { + $userGroup = new UserGroup(); + $notification = new Notification(); + $notification->setSender(new User()); + $notification->setTitle('Notification 1'); + $notification->setType('test_notification_type'); + $notification->addAddressee($userGroup); + + $mailer = $this->prophesize(MailerInterface::class); + $mailer->send(Argument::any())->shouldNotBeCalled(); + + $notificationMailer = $this->buildNotificationMailer($mailer->reveal()); + + $notificationMailer->sendEmailToAddressee($notification, $userGroup); + } + + public function testSendEmailToAddresseeUserWithNoAddress(): void + { + $user = new User(); + $notification = new Notification(); + $notification->setSender(new User()); + $notification->setTitle('Notification 1'); + $notification->setType('test_notification_type'); + $notification->addAddressee($user); + + $mailer = $this->prophesize(MailerInterface::class); + $mailer->send(Argument::any())->shouldNotBeCalled(); + + $notificationMailer = $this->buildNotificationMailer($mailer->reveal()); + + $notificationMailer->sendEmailToAddressee($notification, $user); + } + private function buildNotificationMailer( ?MailerInterface $mailer = null, ?MessageBusInterface $messageBus = null, From 1524ed8ce99f1d42cdb496e1bf8fc6ffebeb853c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Mon, 16 Mar 2026 14:54:47 +0000 Subject: [PATCH 3/4] Replace `ActivityVoter::SEE` with `AccompanyingPeriodVoter::SEE` for correct authorization check --- .changes/unreleased/Security-20260316-153605.yaml | 7 +++++++ .../Repository/ActivityACLAwareRepository.php | 3 ++- 2 files changed, 9 insertions(+), 1 deletion(-) create mode 100644 .changes/unreleased/Security-20260316-153605.yaml diff --git a/.changes/unreleased/Security-20260316-153605.yaml b/.changes/unreleased/Security-20260316-153605.yaml new file mode 100644 index 000000000..1c383d2d6 --- /dev/null +++ b/.changes/unreleased/Security-20260316-153605.yaml @@ -0,0 +1,7 @@ +kind: Security +body: Fix permission in list of activities in person context +time: 2026-03-16T15:36:05.243511868+01:00 +custom: + Issue: "506" + MR: "972" + SchemaChange: No schema change diff --git a/src/Bundle/ChillActivityBundle/Repository/ActivityACLAwareRepository.php b/src/Bundle/ChillActivityBundle/Repository/ActivityACLAwareRepository.php index 34ddfe432..57935a2d9 100644 --- a/src/Bundle/ChillActivityBundle/Repository/ActivityACLAwareRepository.php +++ b/src/Bundle/ChillActivityBundle/Repository/ActivityACLAwareRepository.php @@ -24,6 +24,7 @@ use Chill\MainBundle\Security\Authorization\AuthorizationHelperForCurrentUserInt use Chill\MainBundle\Security\Resolver\CenterResolverManagerInterface; use Chill\PersonBundle\Entity\AccompanyingPeriod; use Chill\PersonBundle\Entity\Person; +use Chill\PersonBundle\Security\Authorization\AccompanyingPeriodVoter; use Doctrine\DBAL\Types\Types; use Doctrine\ORM\AbstractQuery; use Doctrine\ORM\EntityManagerInterface; @@ -340,7 +341,7 @@ final readonly class ActivityACLAwareRepository implements ActivityACLAwareRepos } foreach ($person->getAccompanyingPeriodParticipations() as $participation) { - if (!$this->security->isGranted(ActivityVoter::SEE, $participation->getAccompanyingPeriod())) { + if (!$this->security->isGranted(AccompanyingPeriodVoter::SEE, $participation->getAccompanyingPeriod())) { continue; } From 9ba8ec8f41747ed2d7e996fbe34544746082a56f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Mon, 16 Mar 2026 15:55:52 +0100 Subject: [PATCH 4/4] Release v4.14.1 --- .changes/unreleased/DX-20260309-131710.yaml | 7 ------- .changes/unreleased/Security-20260316-153605.yaml | 7 ------- .changes/v4.14.1.md | 5 +++++ CHANGELOG.md | 6 ++++++ 4 files changed, 11 insertions(+), 14 deletions(-) delete mode 100644 .changes/unreleased/DX-20260309-131710.yaml delete mode 100644 .changes/unreleased/Security-20260316-153605.yaml create mode 100644 .changes/v4.14.1.md diff --git a/.changes/unreleased/DX-20260309-131710.yaml b/.changes/unreleased/DX-20260309-131710.yaml deleted file mode 100644 index 865c1fe6f..000000000 --- a/.changes/unreleased/DX-20260309-131710.yaml +++ /dev/null @@ -1,7 +0,0 @@ -kind: DX -body: Add seeds in DataFixtures and in some tests to avoid random test failures -time: 2026-03-09T13:17:10.915852317+01:00 -custom: - Issue: "504" - MR: "970" - SchemaChange: No schema change diff --git a/.changes/unreleased/Security-20260316-153605.yaml b/.changes/unreleased/Security-20260316-153605.yaml deleted file mode 100644 index 1c383d2d6..000000000 --- a/.changes/unreleased/Security-20260316-153605.yaml +++ /dev/null @@ -1,7 +0,0 @@ -kind: Security -body: Fix permission in list of activities in person context -time: 2026-03-16T15:36:05.243511868+01:00 -custom: - Issue: "506" - MR: "972" - SchemaChange: No schema change diff --git a/.changes/v4.14.1.md b/.changes/v4.14.1.md new file mode 100644 index 000000000..e1d512776 --- /dev/null +++ b/.changes/v4.14.1.md @@ -0,0 +1,5 @@ +## v4.14.1 - 2026-03-16 +### Security +* ([#506](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/506)) ([!972](https://gitlab.com/Chill-Projet/chill-bundles/-/merge_requests/972)) Fix permission in list of activities in person context +### DX +* ([#504](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/504)) ([!970](https://gitlab.com/Chill-Projet/chill-bundles/-/merge_requests/970)) Add seeds in DataFixtures and in some tests to avoid random test failures diff --git a/CHANGELOG.md b/CHANGELOG.md index 842ddce69..32fc721ce 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,12 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html), and is generated by [Changie](https://github.com/miniscruff/changie). +## v4.14.1 - 2026-03-16 +### Security +* ([#506](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/506)) ([!972](https://gitlab.com/Chill-Projet/chill-bundles/-/merge_requests/972)) Fix permission in list of activities in person context +### DX +* ([#504](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/504)) ([!970](https://gitlab.com/Chill-Projet/chill-bundles/-/merge_requests/970)) Add seeds in DataFixtures and in some tests to avoid random test failures + ## v4.14.0 - 2026-03-09 ### Feature * ([#486](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/486)) ([!](https://gitlab.com/Chill-Projet/chill-bundles/-/merge_requests/)) Add filter and aggregator based on referrer's main center for exports of accompanying period