test on app controller

This commit is contained in:
Charles 2025-12-18 16:43:48 +01:00
parent 0ea4829940
commit 2d0eddaf51
12 changed files with 521 additions and 34 deletions

1
.gitignore vendored
View File

@ -1,6 +1,7 @@
###> symfony/framework-bundle ### ###> symfony/framework-bundle ###
/.env.local /.env.local
/.env.test
/.env.local.php /.env.local.php
/.env.*.local /.env.*.local
/config/secrets/prod/prod.decrypt.private.php /config/secrets/prod/prod.decrypt.private.php

View File

@ -107,6 +107,7 @@
} }
}, },
"require-dev": { "require-dev": {
"dama/doctrine-test-bundle": "^8.3",
"phpunit/phpunit": "^11.0", "phpunit/phpunit": "^11.0",
"symfony/browser-kit": "7.2.*", "symfony/browser-kit": "7.2.*",
"symfony/css-selector": "7.2.*", "symfony/css-selector": "7.2.*",

71
composer.lock generated
View File

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically" "This file is @generated automatically"
], ],
"content-hash": "00e62b0a959e7b09d4b1fdb7e0501549", "content-hash": "9c6693b9e0508ab0c1ff3ee95823be7d",
"packages": [ "packages": [
{ {
"name": "aws/aws-crt-php", "name": "aws/aws-crt-php",
@ -9995,6 +9995,75 @@
} }
], ],
"packages-dev": [ "packages-dev": [
{
"name": "dama/doctrine-test-bundle",
"version": "v8.3.1",
"source": {
"type": "git",
"url": "https://github.com/dmaicher/doctrine-test-bundle.git",
"reference": "9bc47e02a0d67cbfef6773837249f71e65c95bf6"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/dmaicher/doctrine-test-bundle/zipball/9bc47e02a0d67cbfef6773837249f71e65c95bf6",
"reference": "9bc47e02a0d67cbfef6773837249f71e65c95bf6",
"shasum": ""
},
"require": {
"doctrine/dbal": "^3.3 || ^4.0",
"doctrine/doctrine-bundle": "^2.11.0",
"php": ">= 8.1",
"psr/cache": "^2.0 || ^3.0",
"symfony/cache": "^6.4 || ^7.2 || ^8.0",
"symfony/framework-bundle": "^6.4 || ^7.2 || ^8.0"
},
"conflict": {
"phpunit/phpunit": "<10.0"
},
"require-dev": {
"behat/behat": "^3.0",
"friendsofphp/php-cs-fixer": "^3.27",
"phpstan/phpstan": "^2.0",
"phpunit/phpunit": "^10.0 || ^11.0 || ^12.0",
"symfony/process": "^6.4 || ^7.2 || ^8.0",
"symfony/yaml": "^6.4 || ^7.2 || ^8.0"
},
"type": "symfony-bundle",
"extra": {
"branch-alias": {
"dev-master": "8.x-dev"
}
},
"autoload": {
"psr-4": {
"DAMA\\DoctrineTestBundle\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "David Maicher",
"email": "mail@dmaicher.de"
}
],
"description": "Symfony bundle to isolate doctrine database tests and improve test performance",
"keywords": [
"doctrine",
"isolation",
"performance",
"symfony",
"testing",
"tests"
],
"support": {
"issues": "https://github.com/dmaicher/doctrine-test-bundle/issues",
"source": "https://github.com/dmaicher/doctrine-test-bundle/tree/v8.3.1"
},
"time": "2025-08-05T17:55:02+00:00"
},
{ {
"name": "masterminds/html5", "name": "masterminds/html5",
"version": "2.10.0", "version": "2.10.0",

View File

@ -19,4 +19,5 @@ return [
Symfony\Bundle\MercureBundle\MercureBundle::class => ['all' => true], Symfony\Bundle\MercureBundle\MercureBundle::class => ['all' => true],
Knp\Bundle\TimeBundle\KnpTimeBundle::class => ['all' => true], Knp\Bundle\TimeBundle\KnpTimeBundle::class => ['all' => true],
Aws\Symfony\AwsBundle::class => ['all' => true], Aws\Symfony\AwsBundle::class => ['all' => true],
DAMA\DoctrineTestBundle\DAMADoctrineTestBundle::class => ['test' => true],
]; ];

View File

@ -0,0 +1,5 @@
when@test:
dama_doctrine_test:
enable_static_connection: true
enable_static_meta_data_cache: true
enable_static_query_cache: true

View File

@ -12,6 +12,7 @@ use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route; use Symfony\Component\Routing\Attribute\Route;
use Symfony\Component\HttpFoundation\Session\Flash\FlashBagInterface;
#[Route(path: '/application', name: 'application_')] #[Route(path: '/application', name: 'application_')]
@ -41,8 +42,8 @@ class ApplicationController extends AbstractController
$this->loggerService->logEntityNotFound('Application', [ $this->loggerService->logEntityNotFound('Application', [
'applicationId' => $id, 'applicationId' => $id,
'message' => "Application not found for editing." 'message' => "Application not found for editing."
], $actingUser); ], $actingUser->getId());
$this->addFlash('error', "L'application n'existe pas ou n'est pas reconnu."); $this->addFlash('danger', "L'application n'existe pas ou n'est pas reconnu.");
return $this->redirectToRoute('application_index'); return $this->redirectToRoute('application_index');
} }
$applicationData = [ $applicationData = [
@ -61,13 +62,12 @@ class ApplicationController extends AbstractController
$application->setDescription($data['description']); $application->setDescription($data['description']);
$application->setDescriptionSmall($data['descriptionSmall']); $application->setDescriptionSmall($data['descriptionSmall']);
$this->entityManager->persist($application); $this->entityManager->persist($application);
$this->actionService->createAction("Modification de l'application ", $actingUser->getId(), null, $application->getId()); $this->actionService->createAction("Modification de l'application ", $actingUser, null, $application->getId());
$this->loggerService->logApplicationInformation('Application Edited', [ $this->loggerService->logApplicationInformation('Application Edited', [
'applicationId' => $application->getId(), 'applicationId' => $application->getId(),
'applicationName' => $application->getName(), 'applicationName' => $application->getName(),
'message' => "Application edited successfully." 'message' => "Application edited successfully."
], $actingUser->getId()); ], $actingUser->getId());
$this->addFlash('success', "L'application a été mise à jour avec succès.");
}catch (\Exception $e){ }catch (\Exception $e){
$this->loggerService->logError('Application Edit Failed', [ $this->loggerService->logError('Application Edit Failed', [
'applicationId' => $application->getId(), 'applicationId' => $application->getId(),
@ -75,7 +75,6 @@ class ApplicationController extends AbstractController
'error' => $e->getMessage(), 'error' => $e->getMessage(),
'message' => "Failed to edit application." 'message' => "Failed to edit application."
], $actingUser); ], $actingUser);
$this->addFlash('error', "Une erreur est survenue lors de la mise à jour de l'application.");
} }
return $this->redirectToRoute('application_index'); return $this->redirectToRoute('application_index');
@ -91,6 +90,7 @@ class ApplicationController extends AbstractController
{ {
$this->denyAccessUnlessGranted('ROLE_SUPER_ADMIN'); $this->denyAccessUnlessGranted('ROLE_SUPER_ADMIN');
$actingUser = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier()); $actingUser = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier());
try{
$application = $this->entityManager->getRepository(Apps::class)->find($id); $application = $this->entityManager->getRepository(Apps::class)->find($id);
if (!$application) { if (!$application) {
$this->loggerService->logEntityNotFound('Application', [ $this->loggerService->logEntityNotFound('Application', [
@ -116,13 +116,25 @@ class ApplicationController extends AbstractController
'organizationId' => $organization->getId(), 'organizationId' => $organization->getId(),
'message' => "Application authorized for organization." 'message' => "Application authorized for organization."
], $actingUser->getId()); ], $actingUser->getId());
$this->entityManager->persist($application);
$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){
$this->loggerService->logError('Application Authorization Failed', [
'applicationId' => $id,
'error' => $e->getMessage(),
'message' => "Failed to authorize application.",
'acting_user_id' => $actingUser->getId()
]);
return new Response('Erreur lors de l\'autorisation de l\'application.', Response::HTTP_INTERNAL_SERVER_ERROR);
} }
#[Route(path: '/remove/{id}', name: 'remove', methods: ['POST'])]
public function remove(int $id, Request $request) }
#[Route(path: '/revoke/{id}', name: 'revoke', methods: ['POST'])]
public function revoke(int $id, Request $request)
{ {
$this->denyAccessUnlessGranted('ROLE_SUPER_ADMIN'); $this->denyAccessUnlessGranted('ROLE_SUPER_ADMIN');
$actingUser = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier()); $actingUser = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier());

View File

@ -59,6 +59,7 @@ class Apps
{ {
$this->userOrganizatonApps = new ArrayCollection(); $this->userOrganizatonApps = new ArrayCollection();
$this->organization = new ArrayCollection(); $this->organization = new ArrayCollection();
$this->setIsActive(true);
} }
public function getId(): ?int public function getId(): ?int

View File

@ -11,6 +11,18 @@
"config/packages/aws.yaml" "config/packages/aws.yaml"
] ]
}, },
"dama/doctrine-test-bundle": {
"version": "8.3",
"recipe": {
"repo": "github.com/symfony/recipes-contrib",
"branch": "main",
"version": "8.3",
"ref": "dfc51177476fb39d014ed89944cde53dc3326d23"
},
"files": [
"config/packages/dama_doctrine_test_bundle.yaml"
]
},
"doctrine/deprecations": { "doctrine/deprecations": {
"version": "1.1", "version": "1.1",
"recipe": { "recipe": {

View File

@ -14,7 +14,7 @@
{% if application.hasAccess %} {% if application.hasAccess %}
{% if is_granted("ROLE_SUPER_ADMIN") %} {% if is_granted("ROLE_SUPER_ADMIN") %}
<form method="POST" <form method="POST"
action="{{ path('application_remove', {'id': application.entity.id}) }}" action="{{ path('application_revoke', {'id': application.entity.id}) }}"
data-controller="application" data-controller="application"
data-application-application-value="{{ application.entity.name }}" data-application-application-value="{{ application.entity.name }}"
data-application-organization-value="{{ organization.name|capitalize }}" data-application-organization-value="{{ organization.name|capitalize }}"

View File

@ -8,10 +8,22 @@
<div class="w-100 h-100 p-5 m-auto"> <div class="w-100 h-100 p-5 m-auto">
<div class="row m-5"> <div class="row m-5">
<div class="container mt-5"> <div class="container mt-5">
{% for type, messages in app.flashes %}
{% for message in messages %}
<div class="alert alert-{{ type }}">
{{ message }}
</div>
{% endfor %}
{% endfor %}
<h1 class="mb-4">Bienvenue sur la suite Easy</h1> <h1 class="mb-4">Bienvenue sur la suite Easy</h1>
<p class="lead">Ici, vous pouvez trouver toutes nos applications à un seul endroit!</p> <p class="lead">Ici, vous pouvez trouver toutes nos applications à un seul endroit !</p>
</div> </div>
{% if applications is empty %}
<div class="alert alert-info w-100 text-center" role="alert">
Aucune application disponible pour le moment. Veuillez revenir plus tard.
</div>
{% endif %}
{% for application in applications %} {% for application in applications %}
<div class="col-6 mb-3"> <div class="col-6 mb-3">
{% include 'application/InformationCard.html.twig' with { {% include 'application/InformationCard.html.twig' with {

View File

@ -0,0 +1,301 @@
<?php
namespace App\Tests\Controller;
use App\Entity\Apps;
use App\Entity\Organizations;
use App\Service\ActionService;
use App\Service\LoggerService;
use App\Tests\Functional\AbstractFunctionalTest;
use PHPUnit\Framework\Attributes\Test;
class ApplicationControllerTest extends AbstractFunctionalTest
{
//region Index Tests
#[Test]
public function index_redirects_unauthenticated_user(): void
{
$this->client->request('GET', '/application/');
self::assertResponseRedirects('/login'); // Assuming your login route is /login
}
#[Test]
public function index_lists_applications_for_authenticated_user(): void
{
// 1. Arrange: Create User and Data
$user = $this->createUser('user@test.com');
$this->createApp('App One');
$this->createApp('App Two');
// 2. Act: Login and Request
$this->client->loginUser($user);
$this->client->request('GET', '/application/');
// 3. Assert
self::assertResponseIsSuccessful();
self::assertSelectorTextContains('body', 'App One');
self::assertSelectorTextContains('body', 'App Two');
}
#[Test]
public function index_no_application_found(): void
{
$user = $this->createUser('user@test.com');
$this->client->loginUser($user);
$this->client->request('GET', '/application/');
self::assertResponseIsSuccessful();
self::assertSelectorTextContains('body', 'Aucune application disponible');
}
//endregion
//region Edit Tests
#[Test]
public function edit_page_denies_access_to_regular_users(): void
{
$user = $this->createUser('regular@test.com');
$app = $this->createApp('Target App');
$this->client->loginUser($user);
$this->client->request('GET', '/application/edit/' . $app->getId());
self::assertResponseStatusCodeSame(403);
}
#[Test]
public function edit_page_denies_access_to_admin_users(): void
{
$user = $this->createUser('admin@test.com', ['ROLE_ADMIN']);
$app = $this->createApp('Target App');
$this->client->loginUser($user);
$this->client->request('GET', '/application/edit/' . $app->getId());
self::assertResponseStatusCodeSame(403);
}
#[Test]
public function edit_page_loads_for_super_admin(): void
{
$admin = $this->createUser('admin@test.com', ['ROLE_SUPER_ADMIN']);
$app = $this->createApp('Editable App');
$this->client->loginUser($admin);
$crawler = $this->client->request('GET', '/application/edit/' . $app->getId());
self::assertResponseIsSuccessful();
$this->assertCount(1, $crawler->filter('input[name="name"]'));
}
#[Test]
public function edit_submits_changes_successfully(): void
{
$admin = $this->createUser('admin@test.com', ['ROLE_SUPER_ADMIN']);
$app = $this->createApp('Old Name');
$this->client->loginUser($admin);
// Simulate POST request directly (mimicking form submission)
$this->client->request('POST', '/application/edit/' . $app->getId(), [
'name' => 'New Name',
'description' => 'Updated Description',
'descriptionSmall' => 'Updated Small',
]);
// Assert Redirection
self::assertResponseRedirects('/application/');
$this->client->followRedirect();
// Assert Database Update
$this->entityManager->clear(); // Clear identity map to force fresh fetch
$updatedApp = $this->entityManager->getRepository(Apps::class)->find($app->getId());
$this->assertEquals('New Name', $updatedApp->getName());
$this->assertEquals('Updated Description', $updatedApp->getDescription());
}
#[Test]
public function edit_handles_non_existent_id_get(): void
{
$admin = $this->createUser('admin@test.com', ['ROLE_SUPER_ADMIN']);
$this->client->loginUser($admin);
$this->client->request('GET', '/application/edit/999999');
self::assertResponseRedirects('/application/');
$this->client->followRedirect();
self::assertSelectorExists('.alert-danger');
self::assertSelectorTextContains('.alert-danger', "n'existe pas");
}
#[Test]
public function edit_handles_non_existent_id_post(): void
{
// Arrange
$admin = $this->createUser('superAdmin@test.com', ['ROLE_SUPER_ADMIN']);
$app = $this->createApp('App With Issue');
$this->client->loginUser($admin);
$this->client->request('POST', '/application/edit/' . 99999, [
'name' => 'New Name',
'description' => 'Updated Description',
'descriptionSmall' => 'Updated Small',
]);
self::assertResponseRedirects('/application/');
$this->client->followRedirect();
self::assertSelectorExists('.alert-danger');
self::assertSelectorTextContains('.alert-danger', "n'existe pas");
}
//endregion
//region Authorize Tests
#[Test]
public function authorize_adds_organization_successfully(): void
{
$admin = $this->createUser('admin@test.com', ['ROLE_SUPER_ADMIN']);
$app = $this->createApp('Auth App');
$org = $this->createOrganization('Test Org');
$this->client->loginUser($admin);
$this->client->request('POST', '/application/authorize/' . $app->getId(), [
'organizationId' => $org->getId()
]);
self::assertResponseStatusCodeSame(200);
// Clear Doctrine memory to force fetching fresh data from DB
$this->entityManager->clear();
$updatedApp = $this->entityManager->getRepository(Apps::class)->find($app->getId());
$exists = $updatedApp->getOrganization()->exists(function($key, $element) use ($org) {
return $element->getId() === $org->getId();
});
$this->assertTrue($exists, 'The application is not linked to the organization.');
}
#[Test]
public function authorize_fails_on_invalid_organization(): void
{
$admin = $this->createUser('sAdmin@test.com', ['ROLE_SUPER_ADMIN']);
$app = $this->createApp('App For Org Test');
$this->client->loginUser($admin);
$this->client->request('POST', '/application/authorize/' . $app->getId(), [
'organizationId' => 99999
]);
self::assertResponseStatusCodeSame(404);
}
#[Test]
public function authorize_fails_on_invalid_application(): void
{
$admin = $this->createUser('sAdmin@test.com', ['ROLE_SUPER_ADMIN']);
$this->client->loginUser($admin);
$this->client->request('POST', '/application/authorize/99999', [
'organizationId' => 1
]);
self::assertResponseStatusCodeSame(404);
}
//endregion
//region revoke Tests
#[Test]
public function revoke_denies_access_to_admins(): void
{
$user = $this->createUser('Admin@test.com', ['ROLE_ADMIN']);
$app = $this->createApp('App To Revoke');
$org = $this->createOrganization('Org To Revoke');
$this->client->loginUser($user);
$this->client->request('POST', '/application/revoke/'. $app->getId(), [
'organizationId' => $org->getId()
]);
self::assertResponseStatusCodeSame(403);
}
#[Test]
public function revoke_denies_access_to_user(): void
{
$user = $this->createUser('user@test.com');
$app = $this->createApp('App To Revoke');
$org = $this->createOrganization('Org To Revoke');
$this->client->loginUser($user);
$this->client->request('POST', '/application/revoke/'. $app->getId(), [
'organizationId' => $org->getId()
]);
self::assertResponseStatusCodeSame(403);
}
#[Test]
public function revoke_removes_organization_successfully(): void
{
$admin = $this->createUser('sAdmin@test.com', ['ROLE_SUPER_ADMIN']);
$app = $this->createApp('App To Revoke Org');
$org = $this->createOrganization('Org To Be Revoked');
// First, authorize the organization
$app->addOrganization($org);
$this->entityManager->persist($app);
$this->entityManager->flush();
$this->client->loginUser($admin);
$this->client->request('POST', '/application/revoke/'. $app->getId(), [
'organizationId' => $org->getId()
]);
self::assertResponseStatusCodeSame(200);
// Clear Doctrine memory to force fetching fresh data from DB
$this->entityManager->clear();
$updatedApp = $this->entityManager->getRepository(Apps::class)->find($app->getId());
$exists = $updatedApp->getOrganization()->exists(function($key, $element) use ($org) {
return $element === $org;
});
self::assertFalse($exists, 'The organization was removed from the application.');
}
#[Test]
public function revoke_fails_on_invalid_organization(): void
{
$admin = $this->createUser('sAdmin@test.com', ['ROLE_SUPER_ADMIN']);
$app = $this->createApp('App To Revoke Org');
$org = $this->createOrganization('Org To Be Revoked');
// First, authorize the organization
$app->addOrganization($org);
$this->entityManager->persist($app);
$this->entityManager->flush();
$this->client->loginUser($admin);
$this->client->request('POST', '/application/revoke/' . $app->
getId(), [
'organizationId' => 99999
]);
self::assertResponseStatusCodeSame(404);
}
#[Test]
public function revoke_fails_on_invalid_application(): void
{
$admin = $this->createUser('sAdmin@test.com', ['ROLE_SUPER_ADMIN']);
$org = $this->createOrganization('Org To Be Revoked');
// First, authorize the organization
$this->client->loginUser($admin);
$this->client->request('POST', '/application/revoke/' . 9999, [
'organizationId' => 99999
]);
self::assertResponseStatusCodeSame(404, "L'application n'existe pas.");
}
//endregion
}

View File

@ -0,0 +1,72 @@
<?php
namespace App\Tests\Functional;
use App\Entity\Apps;
use App\Entity\Organizations;
use App\Entity\User;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\KernelBrowser;
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
abstract class AbstractFunctionalTest extends WebTestCase
{
protected KernelBrowser $client;
protected EntityManagerInterface $entityManager;
protected function setUp(): void
{
$this->client = static::createClient();
// Access the container to get the EntityManager
$this->entityManager = static::getContainer()->get(EntityManagerInterface::class);
}
protected function createUser(string $email, array $roles = ['ROLE_USER']): User
{
$user = new User();
$user->setEmail($email);
$user->setRoles($roles);
$user->setName('Test');
$user->setSurname('User');
$user->setPassword('$2y$13$...'); // Dummy hash, logic typically bypassed by loginUser
$this->entityManager->persist($user);
$this->entityManager->flush();
return $user;
}
protected function createApp(string $name): Apps
{
// Based on your Entity, these fields are NOT nullable, so we must fill them
$app = new Apps();
$app->setName($name);
$app->setTitle($name . ' Title');
$app->setSubDomain(strtolower($name)); // Assuming valid subdomain logic
$app->setLogoUrl('https://example.com/logo.png');
// $app->setDescription() is nullable, so we can skip or set it
$this->entityManager->persist($app);
$this->entityManager->flush();
return $app;
}
protected function createOrganization(string $name): Organizations
{
// I am assuming the Organizations entity structure here based on context
$org = new Organizations();
$org->setName($name);
$org->setEmail('contact@' . strtolower($name) . '.com');
$org->setNumber(100 + rand(1, 900)); // Example number
$org->setAddress('123 ' . $name . ' St'); // Example address
$org->setLogoUrl('https://example.com/org_logo.png');
// Add other required fields if Organizations has non-nullable columns
$this->entityManager->persist($org);
$this->entityManager->flush();
return $org;
}
}