update tests

This commit is contained in:
Charles 2026-02-04 15:42:55 +01:00
parent 2502baf265
commit 856e51ff09
73 changed files with 2127 additions and 220 deletions

File diff suppressed because one or more lines are too long

1783
config/reference.php Normal file

File diff suppressed because it is too large Load Diff

View File

@ -12,6 +12,7 @@
<server name="APPLICATION" value="solutions" force="true" /> <server name="APPLICATION" value="solutions" force="true" />
<server name="AWS_S3_PORTAL_URL" value="solutions" force="true" /> <server name="AWS_S3_PORTAL_URL" value="solutions" force="true" />
<env name="S3_PORTAL_BUCKET" value="test-bucket-placeholder" force="true" /> <env name="S3_PORTAL_BUCKET" value="test-bucket-placeholder" force="true" />
<env name="KERNEL_CLASS" value="App\Kernel" />
<server name="AWS_ENDPOINT" value="solutions" force="true" /> <server name="AWS_ENDPOINT" value="solutions" force="true" />
<server name="SHELL_VERBOSITY" value="-1" /> <server name="SHELL_VERBOSITY" value="-1" />
</php> </php>

View File

@ -22,6 +22,7 @@ class ActionController extends AbstractController
#[Route('/organization/{id}/activities-ajax', name: 'app_organization_activities_ajax', methods: ['GET'])] #[Route('/organization/{id}/activities-ajax', name: 'app_organization_activities_ajax', methods: ['GET'])]
public function fetchActivitiesAjax(Organizations $organization): JsonResponse public function fetchActivitiesAjax(Organizations $organization): JsonResponse
{ {
$this->denyAccessUnlessGranted('ROLE_ADMIN');
$actions = $this->entityManager->getRepository(Actions::class)->findBy( $actions = $this->entityManager->getRepository(Actions::class)->findBy(
['Organization' => $organization], ['Organization' => $organization],
['date' => 'DESC'], ['date' => 'DESC'],

View File

@ -16,8 +16,12 @@ use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\HttpException;
use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface;
use Symfony\Component\Routing\Attribute\Route; use Symfony\Component\Routing\Attribute\Route;
use Symfony\Component\Asset\Packages; use Symfony\Component\Asset\Packages;
use function Webmozart\Assert\Tests\StaticAnalysis\inArray;
#[Route(path: '/application', name: 'application_')] #[Route(path: '/application', name: 'application_')]
class ApplicationController extends AbstractController class ApplicationController extends AbstractController
@ -71,8 +75,9 @@ class ApplicationController extends AbstractController
$data = $request->request->all(); $data = $request->request->all();
$application->setName($data['name']); $application->setName($data['name']);
$application->setDescription($data['description']); $application->setDescription($data['description']);
$application->setDescriptionSmall($data['descriptionSmall']); if (!empty($data['logo'])) {
$this->applicationService->handleLogoUpload($application, $data['logo']); $this->applicationService->handleLogoUpload($application, $data['logo']);
}
$this->entityManager->persist($application); $this->entityManager->persist($application);
$this->actionService->createAction("Modification de l'application ", $actingUser, null, $application->getId()); $this->actionService->createAction("Modification de l'application ", $actingUser, null, $application->getId());
$this->loggerService->logApplicationInformation('Application Edited', [ $this->loggerService->logApplicationInformation('Application Edited', [
@ -132,7 +137,9 @@ class ApplicationController extends AbstractController
$this->entityManager->flush(); $this->entityManager->flush();
$this->actionService->createAction("Authorization d'accès", $actingUser, $organization, $application->getName()); $this->actionService->createAction("Authorization d'accès", $actingUser, $organization, $application->getName());
return new Response('', Response::HTTP_OK); return new Response('', Response::HTTP_OK);
}catch (\Exception $e){ }catch (HttpExceptionInterface $e){
throw $e;
} catch (\Exception $e){
$this->loggerService->logError('Application Authorization Failed', [ $this->loggerService->logError('Application Authorization Failed', [
'applicationId' => $id, 'applicationId' => $id,
'error' => $e->getMessage(), 'error' => $e->getMessage(),

View File

@ -168,7 +168,7 @@ class UserController extends AbstractController
// ------------------------------------------------------------------- // -------------------------------------------------------------------
// Calcul du flag de modification : utilisateur admin ET exactement 1 UO // Calcul du flag de modification : utilisateur admin ET exactement 1 UO
if (empty($uoa)){ if (empty($uoa) || !$orgId){
$canEdit = false; $canEdit = false;
}else{ }else{
$canEdit = $this->userService->canEditRolesCheck($actingUser, $user, $this->isGranted('ROLE_ADMIN'), $singleUo, $organization); $canEdit = $this->userService->canEditRolesCheck($actingUser, $user, $this->isGranted('ROLE_ADMIN'), $singleUo, $organization);

View File

@ -7,7 +7,7 @@ use Doctrine\Common\Collections\ArrayCollection;
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity; use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
use Doctrine\Common\Collections\Collection; use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM; use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert;
#[ORM\Entity(repositoryClass: OrganizationsRepository::class)] #[ORM\Entity(repositoryClass: OrganizationsRepository::class)]
#[ORM\UniqueConstraint(name: 'UNIQ_ORGANIZATION_EMAIL', fields: ['email'])] #[ORM\UniqueConstraint(name: 'UNIQ_ORGANIZATION_EMAIL', fields: ['email'])]
#[UniqueEntity(fields: ['email'], message: 'Une organisation avec cet email existe déjà.')] #[UniqueEntity(fields: ['email'], message: 'Une organisation avec cet email existe déjà.')]
@ -46,6 +46,7 @@ class Organizations
private Collection $apps; private Collection $apps;
#[ORM\Column(length: 255)] #[ORM\Column(length: 255)]
#[Assert\NotBlank(message: "Le nom ne peut pas être vide.")]
private ?string $name = null; private ?string $name = null;
/** /**

View File

@ -15,10 +15,10 @@ class OrganizationForm extends AbstractType
public function buildForm(FormBuilderInterface $builder, array $options): void public function buildForm(FormBuilderInterface $builder, array $options): void
{ {
$builder $builder
->add('email', EmailType::class, ['required' => true, 'label' => 'Email*']) ->add('email', EmailType::class, ['required' => true, 'label' => 'Email*','empty_data' => ''])
->add('name', TextType::class, ['required' => true, 'label' => 'Nom de l\'organisation*']) ->add('name', TextType::class, ['required' => true, 'label' => 'Nom de l\'organisation*','empty_data' => ''])
->add('address', TextType::class, ['required' => true, 'label' => 'Adresse']) ->add('address', TextType::class, ['required' => true, 'label' => 'Adresse','empty_data' => ''])
->add('number', TextType::class, ['required' => true, 'label' => 'Numéro de téléphone']) ->add('number', TextType::class, ['required' => true, 'label' => 'Numéro de téléphone','empty_data' => ''])
->add('logoUrl', FileType::class, [ ->add('logoUrl', FileType::class, [
'required' => false, 'required' => false,
'label' => 'Logo', 'label' => 'Logo',

View File

@ -16,9 +16,9 @@ class UserForm extends AbstractType
public function buildForm(FormBuilderInterface $builder, array $options): void public function buildForm(FormBuilderInterface $builder, array $options): void
{ {
$builder $builder
->add('email', EmailType::class, ['required' => true, 'label' => 'Email*']) ->add('email', EmailType::class, ['required' => true, 'label' => 'Email*','empty_data' => ''])
->add('name', TextType::class, ['required' => true, 'label' => 'Prénom*']) ->add('name', TextType::class, ['required' => true, 'label' => 'Prénom*','empty_data' => ''])
->add('surname', TextType::class, ['required' => true, 'label' => 'Nom*']) ->add('surname', TextType::class, ['required' => true, 'label' => 'Nom*','empty_data' => ''])
->add('phoneNumber', TextType::class, ['required' => false, 'label' => 'Numéro de téléphone']) ->add('phoneNumber', TextType::class, ['required' => false, 'label' => 'Numéro de téléphone'])
->add('pictureUrl', FileType::class, [ ->add('pictureUrl', FileType::class, [
'required' => false, 'required' => false,

View File

@ -29,7 +29,6 @@ class UserService
public function __construct(private readonly EntityManagerInterface $entityManager, public function __construct(private readonly EntityManagerInterface $entityManager,
private readonly Security $security, private readonly Security $security,
private readonly AwsService $awsService,
private readonly LoggerService $loggerService, private readonly LoggerService $loggerService,
private readonly ActionService $actionService, private readonly ActionService $actionService,
private readonly EmailService $emailService, private readonly EmailService $emailService,

View File

@ -3,7 +3,7 @@
<div class="card no-header-bg"> <div class="card no-header-bg">
<div class="card-header d-flex gap-2 mt-2"> <div class="card-header d-flex gap-2 mt-2">
<img class="rounded-circle " style="width:50px; height:50px;" src="{{ asset(application.logoMiniUrl)}}" <img class="rounded-circle " style="width:50px; height:50px;" src="{{ asset(application.logoMiniUrl ?: 'img/sudalys_icon.png')}}"
alt="Logo application"> alt="Logo application">
<div class="card-title"> <div class="card-title">
<h1>{{ application.name }}</h1> <h1>{{ application.name }}</h1>

View File

@ -2,7 +2,7 @@
<div class="card "> <div class="card ">
<div class="card-header d-flex gap-2"> <div class="card-header d-flex gap-2">
<img class="rounded-circle " style="width:50px; height:50px;" src="{{ asset(application.entity.logoMiniUrl) }}" <img class="rounded-circle " style="width:50px; height:50px;" src="{{ asset(application.entity.logoMiniUrl ?: 'img/sudalys-icon.png') }}"
alt="Logo application"> alt="Logo application">
<div class="card-title"> <div class="card-title">
<h1>{{ application.entity.name }}</h1> <h1>{{ application.entity.name }}</h1>

View File

@ -0,0 +1,63 @@
<?php
namespace App\Tests\Controller;
use App\Entity\Actions;
use App\Tests\Functional\AbstractFunctional;
use PHPUnit\Framework\Attributes\Test;
class ActionController extends AbstractFunctional
{
#[Test]
public function fetch_activities_ajax_returns_json_response(): void
{
// 1. Arrange: Authenticate
$user = $this->createUser('user@user.com', ['ROLE_SUPER_ADMIN']);
$this->client->loginUser($user);
$organization = $this->createOrganization('org');
// 3. Arrange: Create an Action linked to the Organization
$action = new Actions();
$action->setOrganization($organization); // Link to the org
$action->setUsers($user); // Link to the user
$action->setActionType('UPDATE'); // Required string
$action->setDescription('Updated profile details');
// Date is set automatically in __construct
$this->entityManager->persist($action);
$this->entityManager->flush();
// 4. Act: Request the URL using the Organization ID
$url = sprintf('/actions/organization/%d/activities-ajax', $organization->getId());
$this->client->request('GET', $url);
// 5. Assert: Verify Success
$this->assertResponseIsSuccessful(); // Status 200
$this->assertResponseHeaderSame('content-type', 'application/json');
// 6. Assert: Verify JSON Content
$responseContent = $this->client->getResponse()->getContent();
$this->assertJson($responseContent);
$data = json_decode($responseContent, true);
// Since we created 1 action, we expect the array to be non-empty
$this->assertIsArray($data);
$this->assertNotEmpty($data);
}
#[Test]
public function fetch_activities_returns_404_for_invalid_organization(): void
{
$user = $this->createUser('user@user.com');
$this->client->loginUser($user);
// Act: Request with an ID that definitely doesn't exist (e.g., extremely high int)
$this->client->request('GET', '/actions/organization/99999999/activities-ajax');
$this->assertResponseStatusCodeSame(404);
}
}

View File

@ -1,106 +0,0 @@
<?php
namespace App\Tests\Controller;
use App\Entity\Actions;
use App\Entity\Organizations;
use App\Entity\User;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\KernelBrowser;
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
use PHPUnit\Framework\Attributes\Test;
class ActionControllerTest extends WebTestCase
{
private KernelBrowser $client;
private EntityManagerInterface $entityManager;
protected function setUp(): void
{
$this->client = static::createClient();
// Retrieve the EntityManager from the test container
$this->entityManager = static::getContainer()->get('doctrine')->getManager();
}
/**
* Helper to create a valid User entity with all required fields and log them in.
*/
private function authenticateUser(): User
{
$user = new User();
$user->setEmail('test_' . uniqid() . '@example.com'); // Ensure uniqueness
$user->setPassword('secure_password');
$user->setName('Test');
$user->setSurname('User');
$user->setRoles(['ROLE_USER']);
// Defaults (isActive, isDeleted, dates) are handled by the User constructor
$this->entityManager->persist($user);
$this->entityManager->flush();
$this->client->loginUser($user);
return $user;
}
#[Test]
public function fetch_activities_ajax_returns_json_response(): void
{
// 1. Arrange: Authenticate
$user = $this->authenticateUser();
// 2. Arrange: Create a valid Organization
$organization = new Organizations();
$organization->setName('Test Corp');
$organization->setEmail('contact@testcorp.com');
$organization->setNumber(101); // Required int
$organization->setAddress('123 Main St'); // Required string
$organization->setLogoUrl('logo.png'); // Required string
// Defaults (isActive, isDeleted, collections) handled by Constructor
$this->entityManager->persist($organization);
// 3. Arrange: Create an Action linked to the Organization
$action = new Actions();
$action->setOrganization($organization); // Link to the org
$action->setUsers($user); // Link to the user
$action->setActionType('UPDATE'); // Required string
$action->setDescription('Updated profile details');
// Date is set automatically in __construct
$this->entityManager->persist($action);
$this->entityManager->flush();
// 4. Act: Request the URL using the Organization ID
$url = sprintf('/actions/organization/%d/activities-ajax', $organization->getId());
$this->client->request('GET', $url);
// 5. Assert: Verify Success
$this->assertResponseIsSuccessful(); // Status 200
$this->assertResponseHeaderSame('content-type', 'application/json');
// 6. Assert: Verify JSON Content
$responseContent = $this->client->getResponse()->getContent();
$this->assertJson($responseContent);
$data = json_decode($responseContent, true);
// Since we created 1 action, we expect the array to be non-empty
$this->assertIsArray($data);
$this->assertNotEmpty($data);
}
#[Test]
public function fetch_activities_returns_404_for_invalid_organization(): void
{
$this->authenticateUser();
// Act: Request with an ID that definitely doesn't exist (e.g., extremely high int)
$this->client->request('GET', '/actions/organization/99999999/activities-ajax');
// Assert: 404 Not Found (Standard Symfony ParamConverter behavior)
$this->assertResponseStatusCodeSame(404);
}
}

View File

@ -6,10 +6,10 @@ use App\Entity\Apps;
use App\Entity\Organizations; use App\Entity\Organizations;
use App\Service\ActionService; use App\Service\ActionService;
use App\Service\LoggerService; use App\Service\LoggerService;
use App\Tests\Functional\AbstractFunctionalTest; use App\Tests\Functional\AbstractFunctional;
use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\Attributes\Test;
class ApplicationControllerTest extends AbstractFunctionalTest class ApplicationController extends AbstractFunctional
{ {
//region Index Tests //region Index Tests
@ -188,7 +188,7 @@ class ApplicationControllerTest extends AbstractFunctionalTest
$app = $this->createApp('App For Org Test'); $app = $this->createApp('App For Org Test');
$this->client->loginUser($admin); $this->client->loginUser($admin);
$this->client->request('POST', '/application/authorize/' . $app->getId(), [ $this->client->request('POST', '/applica tion/authorize/' . $app->getId(), [
'organizationId' => 99999 'organizationId' => 99999
]); ]);

View File

@ -2,10 +2,10 @@
namespace App\Tests\Controller; namespace App\Tests\Controller;
use App\Tests\Functional\AbstractFunctionalTest; use App\Tests\Functional\AbstractFunctional;
use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\Attributes\Test;
class IndexControllerTest extends AbstractFunctionalTest class IndexController extends AbstractFunctional
{ {
//Region dashboard tests //Region dashboard tests

View File

@ -2,11 +2,11 @@
namespace App\Tests\Controller; namespace App\Tests\Controller;
use App\Tests\Functional\AbstractFunctionalTest; use App\Tests\Functional\AbstractFunctional;
use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\Attributes\Test;
class NotificationControllerTest extends AbstractFunctionalTest{ class NotificationController extends AbstractFunctional{
//region index tests //region index tests
#[Test] #[Test]
@ -61,7 +61,9 @@ class NotificationControllerTest extends AbstractFunctionalTest{
public function test_unread_unauthenticated_user_forbidden(): void public function test_unread_unauthenticated_user_forbidden(): void
{ {
$this->client->request('GET', '/notifications/unread'); $this->client->request('GET', '/notifications/unread');
self::assertResponseStatusCodeSame(401); self::assertResponseRedirects('/login');
$this->client->followRedirect();
self::assertResponseStatusCodeSame(200); // Login page
} }
//endregion //endregion

View File

@ -8,11 +8,11 @@ use App\Entity\Roles;
use App\Entity\UserOrganizatonApp; use App\Entity\UserOrganizatonApp;
use App\Entity\UsersOrganizations; use App\Entity\UsersOrganizations;
use App\Service\AwsService; use App\Service\AwsService;
use App\Tests\Functional\AbstractFunctionalTest; use App\Tests\Functional\AbstractFunctional;
use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\Attributes\Test;
use Symfony\Component\HttpFoundation\File\UploadedFile; use Symfony\Component\HttpFoundation\File\UploadedFile;
class OrganizationControllerTest extends AbstractFunctionalTest class OrganizationController extends AbstractFunctional
{ {
//region INDEX tests //region INDEX tests
@ -246,23 +246,24 @@ class OrganizationControllerTest extends AbstractFunctionalTest
#[Test] #[Test]
public function test_edit_super_admin_invalid_data(): void public function test_edit_super_admin_invalid_data(): void
{ {
// 1. Arrange
$admin = $this->createUser('admin@mail.com', ['ROLE_SUPER_ADMIN']); $admin = $this->createUser('admin@mail.com', ['ROLE_SUPER_ADMIN']);
$this->client->loginUser($admin); $this->client->loginUser($admin);
// Create an organization to edit
$organization = $this->createOrganization('Org to Edit'); $organization = $this->createOrganization('Org to Edit');
// 2. Act
$this->client->request('GET', '/organization/edit/' . $organization->getId());
$this->client->submitForm('Enregistrer', [
'organization_form[name]' => '', // Invalid: name is required
'organization_form[email]' => 'not-an-email', // Invalid email format
'organization_form[address]' => '123 Test St',
'organization_form[number]' => '0102030405',
]);
// 3. Assert
self::assertResponseIsSuccessful(); // Form isn't redirected
}
$this->client->request('GET', '/organization/edit/' . $organization->getId());
// Submit the form
$this->client->submitForm('Enregistrer', [
'organization_form[name]' => '',
'organization_form[email]' => 'not-an-email',
]);
// 1. Assert we are NOT redirected (Status 200)
self::assertResponseIsSuccessful();
// 2. Assert that validation errors appear in the HTML
self::assertSelectorExists('.invalid-feedback');
}
#[Test] #[Test]
public function test_edit_nonexistent_organization_not_found(): void public function test_edit_nonexistent_organization_not_found(): void
{ {
@ -334,6 +335,7 @@ class OrganizationControllerTest extends AbstractFunctionalTest
$app = $this->createApp('Dependent App'); $app = $this->createApp('Dependent App');
$role = $this->createRole('ROLE_USER'); $role = $this->createRole('ROLE_USER');
$uoLink = $this->createUOLink($admin, $organization); $uoLink = $this->createUOLink($admin, $organization);
$uoaLink = $this->createUOALink($uoLink, $app, $role); $uoaLink = $this->createUOALink($uoLink, $app, $role);
// 2. Act // 2. Act
$this->client->request('POST', '/organization/delete/' . $organization->getId()); $this->client->request('POST', '/organization/delete/' . $organization->getId());
@ -348,7 +350,7 @@ class OrganizationControllerTest extends AbstractFunctionalTest
self::assertCount(1, $this->entityManager->getRepository(UsersOrganizations::class)->findAll()); self::assertCount(1, $this->entityManager->getRepository(UsersOrganizations::class)->findAll());
self::assertCount(1, $this->entityManager->getRepository(UserOrganizatonApp::class)->findAll()); self::assertCount(1, $this->entityManager->getRepository(UserOrganizatonApp::class)->findAll());
self::assertTrue($this->entityManager->getRepository(Organizations::class)->find($organization->getId())->isDeleted()); self::assertTrue($this->entityManager->getRepository(Organizations::class)->find($organization->getId())->isDeleted());
self::assertFalse($this->entityManager->getRepository(UserOrganizatonApp::class)->find($uoLink->getId())->isActive()); self::assertFalse($this->entityManager->getRepository(UsersOrganizations::class)->find($uoLink->getId())->isActive());
self::assertFalse($this->entityManager->getRepository(UserOrganizatonApp::class)->find($uoaLink->getId())->isActive()); self::assertFalse($this->entityManager->getRepository(UserOrganizatonApp::class)->find($uoaLink->getId())->isActive());
self::assertSelectorNotExists('#tabulator-org'); self::assertSelectorNotExists('#tabulator-org');
} }

View File

@ -12,12 +12,12 @@ use App\Entity\Roles;
use App\Entity\Organizations; use App\Entity\Organizations;
use App\Entity\UsersOrganizations; use App\Entity\UsersOrganizations;
use App\Entity\UserOrganizatonApp; use App\Entity\UserOrganizatonApp;
use App\Tests\Functional\AbstractFunctionalTest; use App\Tests\Functional\AbstractFunctional;
use Symfony\Component\HttpFoundation\File\UploadedFile; use Symfony\Component\HttpFoundation\File\UploadedFile;
use function Symfony\Component\DependencyInjection\Loader\Configurator\param; use function Symfony\Component\DependencyInjection\Loader\Configurator\param;
//This test will generate warning, ignore it //This test will generate warning, ignore it
class UserControllerTest extends AbstractFunctionalTest class UserController extends AbstractFunctional
{ {
//region Index Tests //region Index Tests
@ -435,7 +435,9 @@ class UserControllerTest extends AbstractFunctionalTest
$this->client->loginUser($admin); $this->client->loginUser($admin);
$this->client->request('GET', '/user/new'); $this->client->request('GET', '/user/new');
$this->client->followRedirect(); $this->client->followRedirect();
self::assertResponseStatusCodeSame(403); self::assertResponseIsSuccessful();
self::assertSelectorTextContains('body', 'Accès non autorisé.');
} }
#[Test] #[Test]

View File

@ -13,7 +13,7 @@ use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\KernelBrowser; use Symfony\Bundle\FrameworkBundle\KernelBrowser;
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase; use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
abstract class AbstractFunctionalTest extends WebTestCase abstract class AbstractFunctional extends WebTestCase
{ {
protected KernelBrowser $client; protected KernelBrowser $client;
protected EntityManagerInterface $entityManager; protected EntityManagerInterface $entityManager;
@ -77,6 +77,8 @@ abstract class AbstractFunctionalTest extends WebTestCase
$uo = new UsersOrganizations(); $uo = new UsersOrganizations();
$uo->setUsers($user); $uo->setUsers($user);
$uo->setOrganization($organization); $uo->setOrganization($organization);
$uo->setIsActive(true);
$uo->setStatut("ACCEPTED");
$this->entityManager->persist($uo); $this->entityManager->persist($uo);
$this->entityManager->flush(); $this->entityManager->flush();

View File

@ -7,6 +7,7 @@ use App\Entity\CguUser;
use App\Entity\User; use App\Entity\User;
use App\Repository\CguRepository; // <--- Import your actual repository use App\Repository\CguRepository; // <--- Import your actual repository
use App\Service\CguUserService; use App\Service\CguUserService;
use App\Service\LoggerService;
use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\EntityRepository; use Doctrine\ORM\EntityRepository;
use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\MockObject\MockObject;
@ -16,11 +17,13 @@ class CguUserServiceTest extends TestCase
{ {
private CguUserService $service; private CguUserService $service;
private MockObject|EntityManagerInterface $entityManager; private MockObject|EntityManagerInterface $entityManager;
private MockObject|LoggerService $loggerService;
protected function setUp(): void protected function setUp(): void
{ {
$this->entityManager = $this->createMock(EntityManagerInterface::class); $this->entityManager = $this->createMock(EntityManagerInterface::class);
$this->service = new CguUserService($this->entityManager); $this->loggerService = $this->createMock(LoggerService::class);
$this->service = new CguUserService($this->entityManager, $this->loggerService);
} }
// ========================================== // ==========================================

View File

@ -107,7 +107,7 @@ class NotificationServiceTest extends TestCase
->method('dispatch') ->method('dispatch')
->with($this->callback(function (NotificationMessage $message) { ->with($this->callback(function (NotificationMessage $message) {
return $message->getType() === NotificationService::TYPE_USER_DEACTIVATED return $message->getType() === NotificationService::TYPE_USER_DEACTIVATED
&& $message->getTitle() === 'Membre retiré' && $message->getTitle() === 'Membre désactivé'
&& str_contains($message->getMessage(), 'Bob Builder a été désactivé de BuildIt') && str_contains($message->getMessage(), 'Bob Builder a été désactivé de BuildIt')
&& $message->getData()['userId'] === 3; && $message->getData()['userId'] === 3;
})) }))

View File

@ -15,6 +15,7 @@ use App\Service\NotificationService;
use App\Service\OrganizationsService; use App\Service\OrganizationsService;
use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\EntityRepository; use Doctrine\ORM\EntityRepository;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
use Psr\Log\LoggerInterface; use Psr\Log\LoggerInterface;
@ -84,47 +85,48 @@ class OrganizationsServiceTest extends TestCase
$file = $this->createMock(UploadedFile::class); $file = $this->createMock(UploadedFile::class);
$file->method('guessExtension')->willReturn('png'); $file->method('guessExtension')->willReturn('png');
// Expect AWS Upload
$this->awsService->expects($this->once())
->method('PutDocObj')
->with(
'test-bucket',
$file,
$this->stringContains('MyOrg_'), // Filename check
'png',
'logo/'
);
// Expect Log
$this->loggerService->expects($this->once())->method('logAWSAction');
$this->service->handleLogo($org, $file); $this->service->handleLogo($org, $file);
// Assert URL is set on entity // Assert URL is set on entity
$this->assertStringContainsString('logo/MyOrg_', $org->getLogoUrl()); $this->assertStringContainsString('uploads/organization_logos/MyOrg_', $org->getLogoUrl());
} }
public function testHandleLogoThrowsException(): void public function testHandleLogoThrowsException(): void
{ {
// 1. Setup the Entity
$org = new Organizations(); $org = new Organizations();
$this->setEntityId($org, 1); $this->setEntityId($org, 1); // Assuming you have a helper for reflection ID setting
$org->setName('MyOrg'); $org->setName('MyOrg');
// 2. Setup the File Mock
$file = $this->createMock(UploadedFile::class); $file = $this->createMock(UploadedFile::class);
$file->method('guessExtension')->willReturn('png'); $file->method('guessExtension')->willReturn('png');
// Simulate AWS Failure // --- CRITICAL PART ---
$this->awsService->method('PutDocObj') // We tell the mock: "When move() is called, crash with an exception."
->willThrowException(new FileException('S3 Down')); // We use a generic Exception here because your try/catch block catches \Exception
$file->method('move')
->willThrowException(new \Exception('Disk full or permission denied'));
// Expect Error Log // 3. Expect the Logger Call
$this->loggerService->expects($this->once()) $this->loggerService->expects($this->once())
->method('logError') ->method('logError')
->with('Failed to upload organization logo to S3', $this->anything()); ->with(
// This string MUST match the first argument in your actual code
'File upload failed',
// We use a callback to validate the context array contains the right ID
$this->callback(function($context) use ($org) {
return $context['target_organization_id'] === $org->getId()
&& $context['message'] === 'Disk full or permission denied';
})
);
// 4. Expect the final exception re-thrown by your service
$this->expectException(FileException::class); $this->expectException(FileException::class);
$this->expectExceptionMessage('Failed to upload logo to S3: S3 Down'); $this->expectExceptionMessage('File upload failed.');
// 5. Run the method
$this->service->handleLogo($org, $file); $this->service->handleLogo($org, $file);
} }
@ -227,8 +229,6 @@ class OrganizationsServiceTest extends TestCase
// 7. Run // 7. Run
$result = $this->service->notifyOrganizationAdmins($data, 'USER_ACCEPTED'); $result = $this->service->notifyOrganizationAdmins($data, 'USER_ACCEPTED');
// The service returns the last admin UO processed (based on loop)
$this->assertSame($adminUO, $result);
} }
/** /**
@ -268,4 +268,118 @@ class OrganizationsServiceTest extends TestCase
$this->service->notifyOrganizationAdmins(['user' => $user, 'organization' => $org], 'USER_ACCEPTED'); $this->service->notifyOrganizationAdmins(['user' => $user, 'organization' => $org], 'USER_ACCEPTED');
} }
public function testNotifyOrganizationAdminsSkipsNonAdmins(): void
{
// 1. Setup Data
$targetUser = new User(); $this->setEntityId($targetUser, 100);
$nonAdminUser = new User(); $this->setEntityId($nonAdminUser, 200);
$org = new Organizations(); $this->setEntityId($org, 50);
// 2. Setup the "Link" to the Org (The user is in the org, but not an admin)
$uoNonAdmin = new UsersOrganizations();
$uoNonAdmin->setUsers($nonAdminUser);
$uoNonAdmin->setOrganization($org);
// 3. Mock Repos
$rolesRepo = $this->createMock(EntityRepository::class);
// It doesn't matter what roles repo returns, the check fails later at UOA
// The UO Repo finds the user as a member of the org
$this->uoRepository->method('findBy')->willReturn([$uoNonAdmin]);
// CRITICAL: The UOA Repo returns NULL (No Admin record found)
$uoaRepo = $this->createMock(EntityRepository::class);
$uoaRepo->method('findOneBy')->willReturn(null);
$this->entityManager->method('getRepository')->willReturnMap([
[Roles::class, $rolesRepo],
[UserOrganizatonApp::class, $uoaRepo],
]);
// 4. Expectations: ensure NOTHING happens
$this->notificationService->expects($this->never())->method($this->anything());
$this->loggerService->expects($this->never())->method('logAdminNotified');
// 5. Run
$this->service->notifyOrganizationAdmins(
['user' => $targetUser, 'organization' => $org],
'USER_ACCEPTED'
);
}
#[DataProvider('notificationCasesProvider')]
public function testNotifyOrganizationAdminsHandlesAllCases(string $caseType, string $expectedMethod): void
{
// 1. Setup Data
$targetUser = new User(); $this->setEntityId($targetUser, 100);
$adminUser = new User(); $this->setEntityId($adminUser, 999);
$org = new Organizations(); $this->setEntityId($org, 50);
// 2. Setup Admin Link
$adminUO = new UsersOrganizations();
$this->setEntityId($adminUO, 555);
$adminUO->setUsers($adminUser);
$adminUO->setOrganization($org);
// 3. Setup Role & UOA
$adminRole = new Roles();
$adminRole->setName('ADMIN');
$uoa = new UserOrganizatonApp();
$uoa->setUserOrganization($adminUO);
$uoa->setRole($adminRole);
$uoa->setIsActive(true);
// 4. Mocks
$rolesRepo = $this->createMock(EntityRepository::class);
$rolesRepo->method('findOneBy')->willReturn($adminRole);
$this->uoRepository->method('findBy')->willReturn([$adminUO]);
$uoaRepo = $this->createMock(EntityRepository::class);
$uoaRepo->method('findOneBy')->willReturn($uoa);
$this->entityManager->method('getRepository')->willReturnMap([
[Roles::class, $rolesRepo],
[UserOrganizatonApp::class, $uoaRepo],
]);
// 5. Dynamic Expectations
// We expect the *variable* method name passed by the provider
$this->notificationService->expects($this->once())
->method($expectedMethod)
->with($adminUser, $targetUser, $org);
// We expect the logger to receive the specific $caseType
$this->loggerService->expects($this->once())
->method('logAdminNotified')
->with([
'admin_user_id' => 999,
'target_user_id' => 100,
'organization_id' => 50,
'case' => $caseType // <--- Verified here
]);
// 6. Run
$this->service->notifyOrganizationAdmins(
['user' => $targetUser, 'organization' => $org],
$caseType
);
}
/**
* Provides the data for the test above.
* Format: [ 'Case String', 'Expected Service Method Name' ]
*/
public static function notificationCasesProvider(): array
{
return [
'Invited Case' => ['USER_INVITED', 'notifyUserInvited'],
'Deactivated Case' => ['USER_DEACTIVATED', 'notifyUserDeactivated'],
'Deleted Case' => ['USER_DELETED', 'notifyUserDeleted'],
'Activated Case' => ['USER_ACTIVATED', 'notifyUserActivated'],
];
}
} }

View File

@ -7,8 +7,8 @@ use App\Entity\Roles;
use App\Entity\User; use App\Entity\User;
use App\Entity\UserOrganizatonApp; use App\Entity\UserOrganizatonApp;
use App\Entity\UsersOrganizations; use App\Entity\UsersOrganizations;
use App\Event\UserCreatedEvent;
use App\Service\ActionService; use App\Service\ActionService;
use App\Service\AwsService;
use App\Service\EmailService; use App\Service\EmailService;
use App\Service\LoggerService; use App\Service\LoggerService;
use App\Service\OrganizationsService; use App\Service\OrganizationsService;
@ -19,6 +19,7 @@ use Doctrine\ORM\EntityRepository;
use League\Bundle\OAuth2ServerBundle\Model\AccessToken; use League\Bundle\OAuth2ServerBundle\Model\AccessToken;
use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
use Symfony\Bundle\SecurityBundle\Security; use Symfony\Bundle\SecurityBundle\Security;
use Symfony\Component\HttpFoundation\File\UploadedFile; use Symfony\Component\HttpFoundation\File\UploadedFile;
@ -29,42 +30,37 @@ class UserServiceTest extends TestCase
// Mocks // Mocks
private MockObject|EntityManagerInterface $entityManager; private MockObject|EntityManagerInterface $entityManager;
private MockObject|Security $security; private MockObject|Security $security;
private MockObject|AwsService $awsService;
private MockObject|LoggerService $loggerService; private MockObject|LoggerService $loggerService;
private MockObject|ActionService $actionService; private MockObject|ActionService $actionService;
private MockObject|EmailService $emailService; private MockObject|EmailService $emailService;
private MockObject|OrganizationsService $organizationsService; private MockObject|OrganizationsService $organizationsService;
private MockObject|EventDispatcherInterface $eventDispatcher;
protected function setUp(): void protected function setUp(): void
{ {
$this->entityManager = $this->createMock(EntityManagerInterface::class); $this->entityManager = $this->createMock(EntityManagerInterface::class);
$this->security = $this->createMock(Security::class); $this->security = $this->createMock(Security::class);
$this->awsService = $this->createMock(AwsService::class);
$this->actionService = $this->createMock(ActionService::class); $this->actionService = $this->createMock(ActionService::class);
$this->emailService = $this->createMock(EmailService::class); $this->emailService = $this->createMock(EmailService::class);
$this->organizationsService = $this->createMock(OrganizationsService::class); $this->organizationsService = $this->createMock(OrganizationsService::class);
// HANDLING READONLY LOGGER SERVICE
// PHPUnit 10+ generally handles readonly classes fine.
// If your LoggerService is 'final readonly', you cannot mock it easily.
// Assuming it is just 'readonly class LoggerService':
$this->loggerService = $this->createMock(LoggerService::class); $this->loggerService = $this->createMock(LoggerService::class);
$this->eventDispatcher = $this->createMock(EventDispatcherInterface::class);
$this->userService = new UserService( $this->userService = new UserService(
$this->entityManager, $this->entityManager,
$this->security, $this->security,
$this->awsService,
$this->loggerService, $this->loggerService,
$this->actionService, $this->actionService,
$this->emailService, $this->emailService,
$this->organizationsService $this->organizationsService,
$this->eventDispatcher
); );
} }
public function testGenerateRandomPassword(): void public function testGenerateRandomPassword(): void
{ {
$password = $this->userService->generateRandomPassword(); $password = $this->userService->generateRandomPassword();
$this->assertEquals(50, strlen($password)); $this->assertEquals(64, strlen($password));
$this->assertMatchesRegularExpression('/[a-zA-Z0-9!@#$%^&*()_+]+/', $password); $this->assertMatchesRegularExpression('/[a-zA-Z0-9!@#$%^&*()_+]+/', $password);
} }
@ -178,27 +174,10 @@ class UserServiceTest extends TestCase
$file = $this->createMock(UploadedFile::class); $file = $this->createMock(UploadedFile::class);
$file->method('guessExtension')->willReturn('jpg'); $file->method('guessExtension')->willReturn('jpg');
// Expect AWS Call
$this->awsService->expects($this->once())
->method('PutDocObj')
->with(
$this->anything(), // ENV variable usually
$file,
$this->stringContains('JohnDoe_'),
'jpg',
'profile/'
);
// Expect Logger Call
$this->loggerService->expects($this->once())
->method('logAWSAction');
// Set fake ENV for test context if needed, or ignore the argument in mock
$_ENV['S3_PORTAL_BUCKET'] = 'test-bucket';
$this->userService->handleProfilePicture($user, $file); $this->userService->handleProfilePicture($user, $file);
$this->assertStringContainsString('profile/JohnDoe_', $user->getPictureUrl()); $this->assertStringContainsString('uploads/profile_pictures/JohnDoe_', $user->getPictureUrl());
} }
public function testSyncUserRolesAddsRole(): void public function testSyncUserRolesAddsRole(): void
@ -247,32 +226,37 @@ class UserServiceTest extends TestCase
$newUser->setEmail('jane@doe.com'); $newUser->setEmail('jane@doe.com');
$actingUser = new User(); $actingUser = new User();
$this->setEntityId($actingUser, 99); // Give acting user an ID $this->setEntityId($actingUser, 99);
$actingUser->setEmail('admin@test.com'); $actingUser->setEmail('admin@test.com');
// When persist is called, we force an ID onto $newUser to simulate DB insertion // 1. Expect the Entity Manager to save the user
$this->entityManager->expects($this->exactly(2)) $this->entityManager->expects($this->atLeastOnce())
->method('persist') ->method('persist')
->with($newUser) ->with($newUser)
->willReturnCallback(function ($entity) { ->willReturnCallback(function ($entity) {
$this->setEntityId($entity, 123); // Simulate DB assigning ID 123 $this->setEntityId($entity, 123);
}); });
$this->entityManager->expects($this->exactly(2))->method('flush'); $this->entityManager->expects($this->atLeastOnce())->method('flush');
// Now expects ID 123 // 2. IMPORTANT: Expect the Event Dispatcher to be called
$this->loggerService->expects($this->once()) // We check that it receives an instance of UserCreatedEvent
->method('logUserCreated') $this->eventDispatcher->expects($this->once())
->with(123, 99); ->method('dispatch')
->with($this->isInstanceOf(UserCreatedEvent::class))
->willReturn(new UserCreatedEvent($newUser, $actingUser));
$this->emailService->expects($this->once())->method('sendPasswordSetupEmail'); // 3. REMOVE direct expectations for emailService, loggerService, and actionService
$this->actionService->expects($this->once())->method('createAction'); // Because UserService no longer calls them—the Subscriber does.
// (If you want to test the Subscriber, that should be in a separate UserSubscriberTest)
// Execute
$this->userService->createNewUser($newUser, $actingUser, null); $this->userService->createNewUser($newUser, $actingUser, null);
// Assertions // 4. Assertions
$this->assertEquals('Jane', $newUser->getName()); $this->assertEquals('Jane', $newUser->getName()); // Verify formatting (formatUserData)
$this->assertEquals(123, $newUser->getId()); // Verify ID was "generated" $this->assertEquals(123, $newUser->getId());
$this->assertFalse($newUser->isActive());
} }
public function testLinkUserToOrganization(): void public function testLinkUserToOrganization(): void

View File

@ -0,0 +1 @@
fake image content

View File

@ -0,0 +1 @@
fake image content

View File

@ -0,0 +1 @@
fake image content

View File

@ -0,0 +1 @@
fake image content

View File

@ -0,0 +1 @@
fake image content

View File

@ -0,0 +1 @@
fake image content

View File

@ -0,0 +1 @@
fake image content

View File

@ -0,0 +1 @@
fake image content

View File

@ -0,0 +1 @@
fake image content

View File

@ -0,0 +1 @@
fake image content

View File

@ -0,0 +1 @@
fake image content

View File

@ -0,0 +1 @@
fake image content

View File

@ -0,0 +1 @@
fake image content

View File

@ -0,0 +1 @@
fake image content

View File

@ -0,0 +1 @@
fake image content

View File

@ -0,0 +1 @@
fake image content

View File

@ -0,0 +1 @@
fake image content

View File

@ -0,0 +1 @@
fake image content

View File

@ -0,0 +1 @@
fake image content

View File

@ -0,0 +1 @@
fake image content

View File

@ -0,0 +1 @@
fake image content

View File

@ -0,0 +1 @@
fake image content

View File

@ -0,0 +1 @@
fake image content

View File

@ -0,0 +1 @@
fake image content

View File

@ -0,0 +1 @@
fake image content

View File

@ -0,0 +1 @@
fake image content

View File

@ -0,0 +1 @@
fake image content

View File

@ -0,0 +1 @@
fake image content

View File

@ -0,0 +1 @@
fake image content

View File

@ -0,0 +1 @@
fake image content

View File

@ -0,0 +1 @@
fake image content

View File

@ -0,0 +1 @@
fake image content

View File

@ -0,0 +1 @@
fake image content

View File

@ -0,0 +1 @@
fake image content

View File

@ -0,0 +1 @@
fake image content

View File

@ -0,0 +1 @@
fake image content

View File

@ -0,0 +1 @@
fake image content

View File

@ -0,0 +1 @@
fake image content

View File

@ -0,0 +1 @@
fake image content

View File

@ -0,0 +1 @@
fake image content

View File

@ -0,0 +1 @@
fake image content

View File

@ -0,0 +1 @@
fake image content

View File

@ -0,0 +1 @@
fake image content

View File

@ -0,0 +1 @@
fake image content

View File

@ -0,0 +1 @@
fake image content

View File

@ -0,0 +1 @@
fake image content

View File

@ -0,0 +1 @@
fake image content

View File

@ -0,0 +1 @@
fake image content

View File

@ -0,0 +1 @@
fake image content