feat: Implémente la fonctionnalité de réinitialisation du mot de passe

Ce commit introduit la fonctionnalité de réinitialisation du mot de passe pour les utilisateurs.

Les modifications apportées sont les suivantes :

- Ajout de l'entité `AccountResetPasswordRequest` pour gérer les requêtes de réinitialisation de mot de passe.
- Ajout du repository `AccountResetPasswordRequestRepository` pour interagir avec l'entité `AccountResetPasswordRequest`.
- Ajout du formulaire `RequestPasswordRequestType` pour permettre aux utilisateurs de demander une réinitialisation de mot de passe.
- Ajout de l'événement `ResetPasswordEvent` pour déclencher le processus de réinitialisation du mot de passe.
- Ajout de la route `/forgot-password` dans le `HomeController` pour gérer la demande de réinitialisation.
- Création des templates twig `admin/forgot-password.twig` et `admin/base.twig` et `form_tailwind.twig` pour la gestion de l'affichage du formulaire et de la base de l'interface admin.
- Modification des templates twig `admin/login.twig` pour ajouter un lien vers la page de réinitialisation de mot de passe.
- Mise à jour du fichier `assets/app.scss` pour inclure des styles CSS personnalisés.
- Ajout de tests unitaires pour l'entité, le repository et le formulaire.
- Ajout de la configuration twig pour prendre en charge les formulaires avec tailwind
- Ajout des règles d'exclusions sonar dans `sonar-project.properties`
This commit is contained in:
Serreau Jovann
2025-07-18 11:25:13 +02:00
parent 617eae9f24
commit 6d7a9552f6
15 changed files with 586 additions and 83 deletions

View File

@@ -1,2 +1,25 @@
@import "tailwindcss";
@import url('https://fonts.googleapis.com/css2?family=DynaPuff:wght@400..700&display=swap');
@import url('https://fonts.googleapis.com/css2?family=Intel+One+Mono:ital,wght@0,300..700;1,300..700&display=swap');
h1,h2,h3,h4,h5,h6,
label,span,input,{
font-family: 'Intel One Mono', monospace;
}
h1 {
font-size: 2rem;
}
h2 {
font-size: 1.5rem;
}
label {
font-size: 1rem;
}
input {
background: oklch(21% 0.034 264.665);
color: white;
&::placeholder {
opacity: 0.5;
}
}

View File

@@ -1,6 +1,7 @@
twig:
file_name_pattern: '*.twig'
form_themes:
- 'form_tailwind.twig'
when@test:
twig:
strict_variables: true

View File

@@ -25,7 +25,7 @@ sonar.coverage.exclusions= \
src/Service/**/*.php, \
src/Command/*.php
sonar.issue.ignore.multicriteria=e1,e2,e3,e4,e5,e6
sonar.issue.ignore.multicriteria=e1,e2,e3,e4,e5,e6,e7,e8
sonar.issue.ignore.multicriteria.e1.ruleKey=php:S103
sonar.issue.ignore.multicriteria.e1.resourceKey=**/*.php
@@ -43,3 +43,9 @@ sonar.issue.ignore.multicriteria.e5.resourceKey=**/*.php
sonar.issue.ignore.multicriteria.e6.ruleKey=javascript:S1128
sonar.issue.ignore.multicriteria.e6.resourceKey=**/*.js
sonar.issue.ignore.multicriteria.e7.ruleKey=php:S117
sonar.issue.ignore.multicriteria.e7.resourceKey=**/*.php
sonar.issue.ignore.multicriteria.e8.ruleKey=php:S116
sonar.issue.ignore.multicriteria.e8.resourceKey=**/*.php

View File

@@ -3,9 +3,14 @@
namespace App\Controller;
use App\Attribute\Mainframe;
use App\Entity\AccountResetPasswordRequest;
use App\Form\RequestPasswordRequestType;
use App\Service\Mailer\Mailer;
use App\Service\ResetPassword\Event\ResetPasswordEvent;
use Psr\EventDispatcher\EventDispatcherInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
use Symfony\Component\Security\Http\Authentication\AuthenticationUtils;
@@ -24,4 +29,19 @@ class HomeController extends AbstractController
'error' => $authenticationUtils->getLastAuthenticationError(),
]);
}
#[Route(path: '/forgot-password',name: 'app_forgotpassword',methods: ['GET', 'POST'])]
public function forgotPassword(EventDispatcherInterface $eventDispatcher,Request $request): Response
{
$requestPasswordResetEvent = new ResetPasswordEvent();
$form = $this->createForm(RequestPasswordRequestType::class,$requestPasswordResetEvent);
$form->handleRequest($request);
if($form->isSubmitted() && $form->isValid()) {
$eventDispatcher->dispatch($requestPasswordResetEvent);
$this->addFlash('success', 'Si votre compte existe, vous recevrez un email pour réinitialiser votre mot de passe.');
return $this->redirectToRoute('app_forgotpassword');
}
return $this->render('admin/forgot-password.twig', [
'form' => $form->createView(),
]);
}
}

View File

@@ -0,0 +1,82 @@
<?php
namespace App\Entity;
use App\Repository\AccountResetPasswordRequestRepository;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity(repositoryClass: AccountResetPasswordRequestRepository::class)]
class AccountResetPasswordRequest
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;
#[ORM\ManyToOne]
private ?Account $Account = null;
#[ORM\Column(length: 255)]
private ?string $token = null;
#[ORM\Column]
private ?\DateTimeImmutable $requestedAt = null;
#[ORM\Column]
private ?\DateTimeImmutable $expiresAt = null;
public function getId(): ?int
{
return $this->id;
}
public function getAccount(): ?Account
{
return $this->Account;
}
public function setAccount(?Account $Account): static
{
$this->Account = $Account;
return $this;
}
/**
* @return string|null
*/
public function getToken(): ?string
{
return $this->token;
}
public function setToken(?string $token): static
{
$this->token = $token;
return $this;
}
public function getRequestedAt(): ?\DateTimeImmutable
{
return $this->requestedAt;
}
public function setRequestedAt(\DateTimeImmutable $requestedAt): static
{
$this->requestedAt = $requestedAt;
return $this;
}
public function getExpiresAt(): ?\DateTimeImmutable
{
return $this->expiresAt;
}
public function setExpiresAt(\DateTimeImmutable $expiresAt): static
{
$this->expiresAt = $expiresAt;
return $this;
}
}

View File

@@ -0,0 +1,25 @@
<?php
namespace App\Form;
use App\Service\ResetPassword\Event\ResetPasswordEvent;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\EmailType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class RequestPasswordRequestType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('email',EmailType::class,[
'required' => true,
'label' => 'Email de votre compte',
]);
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefault('data_class',ResetPasswordEvent::class);
}
}

View File

@@ -0,0 +1,18 @@
<?php
namespace App\Repository;
use App\Entity\AccountResetPasswordRequest;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
/**
* @extends ServiceEntityRepository<AccountResetPasswordRequest>
*/
class AccountResetPasswordRequestRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, AccountResetPasswordRequest::class);
}
}

View File

@@ -0,0 +1,24 @@
<?php
namespace App\Service\ResetPassword\Event;
class ResetPasswordEvent
{
private string $email;
/**
* @param string $email
*/
public function setEmail(string $email): void
{
$this->email = $email;
}
/**
* @return string
*/
public function getEmail(): string
{
return $this->email;
}
}

25
templates/admin/base.twig Normal file
View File

@@ -0,0 +1,25 @@
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Mainframe - {% block title %}{% endblock %}</title>
<meta name="robots" content="noindex, nofollow">
<link rel="icon" type="image/png" href="{{ asset('favicon/favicon-96x96.png') }}" sizes="96x96" />
<link rel="icon" type="image/svg+xml" href="{{ asset('favicon/favicon.svg') }}" />
<link rel="shortcut icon" href="{{ asset('favicon/favicon.ico') }}" />
<link rel="apple-touch-icon" sizes="180x180" href="{{ asset('favicon/apple-touch-icon.png') }}" />
<meta name="apple-mobile-web-app-title" content="Mainframe" />
<link rel="manifest" href="{{ asset('favicon/site.webmanifest') }}" />
{{ vite_asset('app.js',[]) }}
</head>
{# Changement ici : fond noir, texte blanc #}
<body class="bg-gray-900 font-sans antialiased text-gray-100">
<div class="min-h-screen flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8">
{% block content %}
{% endblock %}
</div>
</body>
</html>

View File

@@ -0,0 +1,39 @@
{% extends 'admin/base.twig' %}
{% block title %}Mot de passe perdu{% endblock %}
{% block content %}
<div class="max-w-md w-full space-y-8 p-10 bg-gray-800 rounded-xl shadow-xl z-10">
<div class="text-center">
<img class="mx-auto h-12 w-auto" src="{{ asset('assets/logo.png') | imagine_filter('webp') }}" alt="Logo Mainframe">
{# Changement ici : texte du titre blanc #}
<h2 class="mt-6 text-3xl font-extrabold text-white">
Mot de de passe perdu
</h2>
</div>
{% for label, messages in app.flashes %}
{% for message in messages %}
<div class="w-full px-4 py-3 rounded-md text-sm font-medium
{% if label == 'success' %}
text-green-100 bg-green-600
{% elseif label == 'error' %}
text-red-100 bg-red-600
{% else %}
text-gray-100 bg-gray-600
{% endif %}">
{{ message }}
</div>
{% endfor %}
{% endfor %}
{{ form_start(form) }}
{{ form_row(form.email) }}
<button
type="submit"
class="w-full px-4 py-2 bg-indigo-600 hover:bg-indigo-700 text-white font-medium rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-indigo-400 dark:bg-indigo-500 dark:hover:bg-indigo-600"
>
Valider
</button>
{{ form_end(form) }}
</div>
{% endblock %}

View File

@@ -1,95 +1,75 @@
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Mainframe - Connexion</title>
{% extends 'admin/base.twig' %}
{% block title %}Connexion{% endblock %}
{% block content %}
<div class="max-w-md w-full space-y-8 p-10 bg-gray-800 rounded-xl shadow-xl z-10">
<div class="text-center">
<img class="mx-auto h-12 w-auto" src="{{ asset('assets/logo.png') | imagine_filter('webp') }}" alt="Logo Mainframe">
<meta name="robots" content="noindex, nofollow">
{# Changement ici : texte du titre blanc #}
<h2 class="mt-6 text-3xl font-extrabold text-white">
Connectez-vous à votre compte
</h2>
</div>
<link rel="icon" type="image/png" href="{{ asset('favicon/favicon-96x96.png') }}" sizes="96x96" />
<link rel="icon" type="image/svg+xml" href="{{ asset('favicon/favicon.svg') }}" />
<link rel="shortcut icon" href="{{ asset('favicon/favicon.ico') }}" />
<link rel="apple-touch-icon" sizes="180x180" href="{{ asset('favicon/apple-touch-icon.png') }}" />
<meta name="apple-mobile-web-app-title" content="Mainframe" />
<link rel="manifest" href="{{ asset('favicon/site.webmanifest') }}" />
{{ vite_asset('app.js',[]) }}
</head>
{# Changement ici : fond noir, texte blanc #}
<body class="bg-gray-900 font-sans antialiased text-gray-100">
<div class="min-h-screen flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8">
{# Changement ici : fond du formulaire gris foncé, ombre plus subtile #}
<div class="max-w-md w-full space-y-8 p-10 bg-gray-800 rounded-xl shadow-xl z-10">
<div class="text-center">
<img class="mx-auto h-12 w-auto" src="{{ asset('assets/logo.png') | imagine_filter('webp') }}" alt="Logo Mainframe">
{% if error %}
{# Changement ici : couleurs de l'alerte pour le thème sombre #}
<div class="bg-red-900 border border-red-700 text-red-200 px-4 py-3 rounded relative" role="alert">
<strong class="font-bold">Erreur de connexion !</strong>
<span class="block sm:inline">{{ error.messageKey|trans(error.messageData, 'security') }}</span>
</div>
{% endif %}
{# Changement ici : texte du titre blanc #}
<h2 class="mt-6 text-3xl font-extrabold text-white">
Connectez-vous à votre compte
</h2>
<form data-turbo="false" class="mt-8 space-y-6" action="{{ path('app_login') }}" method="POST">
<input type="hidden" name="_csrf_token" value="{{ csrf_token('authenticate') }}">
<div class="rounded-md shadow-sm -space-y-px">
<div>
<label for="username" class="sr-only">Utilisateur</label>
{# Changement ici : champs de saisie pour le thème sombre #}
<input id="username" name="_username" type="text" autocomplete="text" required
class="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-600 placeholder-gray-400 text-gray-100 bg-gray-700 rounded-t-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm"
placeholder="Utilisateur" value="{{ last_username }}">
</div>
<div>
<label for="password" class="sr-only">Mot de passe</label>
{# Changement ici : champs de saisie pour le thème sombre #}
<input id="password" name="_password" type="password" autocomplete="current-password" required
class="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-600 placeholder-gray-400 text-gray-100 bg-gray-700 rounded-b-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm"
placeholder="Mot de passe">
</div>
</div>
{% if error %}
{# Changement ici : couleurs de l'alerte pour le thème sombre #}
<div class="bg-red-900 border border-red-700 text-red-200 px-4 py-3 rounded relative" role="alert">
<strong class="font-bold">Erreur de connexion !</strong>
<span class="block sm:inline">{{ error.messageKey|trans(error.messageData, 'security') }}</span>
</div>
{% endif %}
<form data-turbo="false" class="mt-8 space-y-6" action="{{ path('app_login') }}" method="POST">
<input type="hidden" name="_csrf_token" value="{{ csrf_token('authenticate') }}">
<div class="rounded-md shadow-sm -space-y-px">
<div>
<label for="username" class="sr-only">Utilisateur</label>
{# Changement ici : champs de saisie pour le thème sombre #}
<input id="username" name="_username" type="text" autocomplete="text" required
class="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-600 placeholder-gray-400 text-gray-100 bg-gray-700 rounded-t-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm"
placeholder="Utilisateur" value="{{ last_username }}">
</div>
<div>
<label for="password" class="sr-only">Mot de passe</label>
{# Changement ici : champs de saisie pour le thème sombre #}
<input id="password" name="_password" type="password" autocomplete="current-password" required
class="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-600 placeholder-gray-400 text-gray-100 bg-gray-700 rounded-b-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm"
placeholder="Mot de passe">
</div>
<div class="flex items-center justify-between">
<div class="flex items-center">
{# Changement ici : case à cocher pour le thème sombre #}
<input id="remember_me" name="_remember_me" type="checkbox"
class="h-4 w-4 text-indigo-500 focus:ring-indigo-500 border-gray-600 rounded bg-gray-700">
{# Changement ici : texte du label blanc #}
<label for="remember_me" class="ml-2 block text-sm text-gray-100">
Se souvenir de moi
</label>
</div>
<div class="flex items-center justify-between">
<div class="flex items-center">
{# Changement ici : case à cocher pour le thème sombre #}
<input id="remember_me" name="_remember_me" type="checkbox"
class="h-4 w-4 text-indigo-500 focus:ring-indigo-500 border-gray-600 rounded bg-gray-700">
{# Changement ici : texte du label blanc #}
<label for="remember_me" class="ml-2 block text-sm text-gray-100">
Se souvenir de moi
</label>
</div>
<div class="text-sm">
{# Changement ici : couleur du lien plus claire #}
<a href="#" class="font-medium text-indigo-400 hover:text-indigo-300">
Mot de passe oublié ?
</a>
</div>
<div class="text-sm">
{# Changement ici : couleur du lien plus claire #}
<a href="{{ path('app_forgotpassword') }}" class="font-medium text-indigo-400 hover:text-indigo-300">
Mot de passe oublié ?
</a>
</div>
</div>
<div>
{# Le bouton reste indigo pour un bon contraste et une visibilité #}
<button type="submit"
class="group relative w-full flex justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
<div>
{# Le bouton reste indigo pour un bon contraste et une visibilité #}
<button type="submit"
class="group relative w-full flex justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
<span class="absolute left-0 inset-y-0 flex items-center pl-3">
<svg class="h-5 w-5 text-indigo-500 group-hover:text-indigo-400" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
<path fill-rule="evenodd" d="M5 9V7a5 5 0 0110 0v2a2 2 0 012 2v5a2 2 0 01-2 2H5a2 2 0 01-2-2v-5a2 2 0 012-2zm8-2v2H7V7a3 3 0 016 0z" clip-rule="evenodd" />
</svg>
</span>
Se connecter
</button>
</div>
</form>
</div>
Se connecter
</button>
</div>
</form>
</div>
</body>
</html>
{% endblock %}

View File

@@ -0,0 +1,136 @@
{% use 'form_div_layout.html.twig' %}
{# ---------- FORM START / END ---------- #}
{% block form_start -%}
{{ parent() }}
{%- endblock %}
{% block form_end -%}
{{ parent() }}
{%- endblock %}
{# ---------- ROW ---------- #}
{% block form_row %}
<div class="mb-5">
{{ form_label(form) }}
<div class="mt-1">
{{ form_widget(form) }}
</div>
{% if not compound and not form.vars.valid %}
<p class="text-sm text-red-500 mt-1">{{ form_errors(form) }}</p>
{% else %}
{{ form_errors(form) }}
{% endif %}
</div>
{% endblock %}
{# ---------- LABEL ---------- #}
{% block form_label %}
{% if label is not same as(false) %}
<label for="{{ id }}" class="block text-sm font-medium text-gray-200 dark:text-gray-300">
{{ label|trans({}, translation_domain) }}
{% if required %}
<span class="text-red-400">*</span>
{% endif %}
</label>
{% endif %}
{% endblock %}
{# ---------- ERRORS ---------- #}
{% block form_errors %}
{% if errors|length > 0 %}
<ul class="mt-1 text-sm text-red-500">
{% for error in errors %}
<li>{{ error.message }}</li>
{% endfor %}
</ul>
{% endif %}
{% endblock %}
{# ---------- WIDGET DISPATCH ---------- #}
{% block form_widget %}
{% if compound %}
{{ block('form_widget_compound') }}
{% else %}
{{ block('form_widget_simple') }}
{% endif %}
{% endblock %}
{# ---------- SIMPLE INPUTS (text, email, number...) ---------- #}
{% block form_widget_simple %}
{% set type = type|default('text') %}
<input
type="{{ type }}"
{{ block('widget_attributes') }}
value="{{ value }}"
class="form-input mt-1 block w-full px-3 py-2 bg-gray-800 border border-gray-700 text-white rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm"
/>
{% endblock %}
{# ---------- TEXTAREA ---------- #}
{% block textarea_widget %}
<textarea
{{ block('widget_attributes') }}
class="form-textarea mt-1 block w-full px-3 py-2 bg-gray-800 border border-gray-700 text-white rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm"
>{{ value }}</textarea>
{% endblock %}
{# ---------- SELECT ---------- #}
{% block choice_widget_collapsed %}
<select
{{ block('widget_attributes') }}
class="form-select mt-1 block w-full px-3 py-2 bg-gray-800 border border-gray-700 text-white rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm"
>
{% if placeholder is not none %}
<option value="" {% if required and value is empty %}selected{% endif %}>
{{ placeholder != '' ? (placeholder|trans({}, translation_domain)) : '' }}
</option>
{% endif %}
{% for group_label, choice in choices %}
{% if choice is iterable %}
<optgroup label="{{ group_label|trans({}, translation_domain) }}">
{% for nested_choice in choice %}
<option value="{{ nested_choice.value }}" {% if nested_choice is selectedchoice(value) %}selected{% endif %}>
{{ nested_choice.label|trans({}, translation_domain) }}
</option>
{% endfor %}
</optgroup>
{% else %}
<option value="{{ choice.value }}" {% if choice is selectedchoice(value) %}selected{% endif %}>
{{ choice.label|trans({}, translation_domain) }}
</option>
{% endif %}
{% endfor %}
</select>
{% endblock %}
{# ---------- CHECKBOX ---------- #}
{% block checkbox_widget %}
<div class="flex items-center space-x-2">
<input type="checkbox"
{{ block('widget_attributes') }}
{% if value not in ['', null] %} value="{{ value }}"{% endif %}
{% if checked %}checked="checked"{% endif %}
class="form-checkbox h-5 w-5 text-indigo-500 bg-gray-800 border-gray-700 rounded focus:ring-indigo-500">
</div>
{% endblock %}
{# ---------- RADIO ---------- #}
{% block radio_widget %}
<input type="radio"
{{ block('widget_attributes') }}
value="{{ value }}"
{% if checked %}checked="checked"{% endif %}
class="form-radio h-5 w-5 text-indigo-500 bg-gray-800 border-gray-700 focus:ring-indigo-500">
{% endblock %}
{# ---------- FILE ---------- #}
{% block file_widget %}
<input type="file"
{{ block('widget_attributes') }}
class="block w-full text-sm text-gray-300 file:mr-4 file:py-2 file:px-4
file:rounded-md file:border-0
file:text-sm file:font-semibold
file:bg-indigo-600 file:text-white
hover:file:bg-indigo-700
bg-gray-800 border border-gray-700 rounded-md">
{% endblock %}

View File

@@ -0,0 +1,37 @@
<?php
namespace App\Tests\Entity;
use App\Entity\Account;
use App\Entity\AccountResetPasswordRequest;
use PHPUnit\Framework\TestCase;
use ReflectionClass;
class AccountResetPasswordRequestTest extends TestCase
{
public function testEntitySettersAndGetters(): void
{
$account = new Account(); // Ou crée un mock si la classe est complexe
$token = 'reset-token-example';
$now = new \DateTimeImmutable();
$expires = $now->modify('+1 hour');
$resetRequest = new AccountResetPasswordRequest();
$resetRequest->setAccount($account);
$resetRequest->setToken($token);
$resetRequest->setRequestedAt($now);
$resetRequest->setExpiresAt($expires);
$refClass = new ReflectionClass($resetRequest);
$idProp = $refClass->getProperty('id');
$idProp->setAccessible(true);
$idProp->setValue($resetRequest, 123);
$this->assertSame($account, $resetRequest->getAccount());
$this->assertSame($token, $resetRequest->getToken());
$this->assertSame($now, $resetRequest->getRequestedAt());
$this->assertSame($expires, $resetRequest->getExpiresAt());
$this->assertSame(123, $resetRequest->getId());
}
}

View File

@@ -0,0 +1,51 @@
<?php
namespace App\Tests\Form;
use App\Form\RequestPasswordRequestType;
use App\Service\ResetPassword\Event\ResetPasswordEvent;
use Symfony\Component\Form\Test\TypeTestCase;
class RequestPasswordRequestTypeTest extends TypeTestCase
{
public function testSubmitValidData(): void
{
$formData = [
'email' => 'user@example.com',
];
$model = new ResetPasswordEvent(); // l'objet lié au formulaire
$form = $this->factory->create(RequestPasswordRequestType::class, $model);
// Soumission du formulaire avec des données valides
$form->submit($formData);
$this->assertTrue($form->isSynchronized());
$this->assertTrue($form->isValid());
$expected = new ResetPasswordEvent();
$expected->setEmail('user@example.com'); // Assure-toi que la méthode setEmail existe
$this->assertEquals($expected, $model);
// Vérification de la création des champs dans le formulaire
$view = $form->createView();
$children = $view->children;
$this->assertArrayHasKey('email', $children);
}
public function testSubmitInvalidEmail(): void
{
$formData = [
'email' => 'not-an-email',
];
$model = new ResetPasswordEvent();
$form = $this->factory->create(RequestPasswordRequestType::class, $model);
$form->submit($formData);
$this->assertTrue($form->isValid());
}
}

View File

@@ -0,0 +1,36 @@
<?php
namespace App\Tests\Repository;
use App\Entity\AccountResetPasswordRequest;
use App\Repository\AccountResetPasswordRequestRepository;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
use Doctrine\ORM\EntityManagerInterface;
class AccountResetPasswordRequestRepositoryTest extends KernelTestCase
{
private ?EntityManagerInterface $entityManager;
private ?AccountResetPasswordRequestRepository $accountResetPasswordRequestRepository;
protected function setUp(): void
{
self::bootKernel();
$this->entityManager = self::getContainer()->get('doctrine')->getManager();
$this->accountResetPasswordRequestRepository = $this->entityManager->getRepository(AccountResetPasswordRequest::class);
}
public function testRepositoryExistsAndIsCorrectInstance(): void
{
$this->assertInstanceOf(AccountResetPasswordRequestRepository::class, $this->accountResetPasswordRequestRepository);
}
protected function tearDown(): void
{
parent::tearDown();
$this->entityManager->close();
$this->entityManager = null; // Avoid memory leaks
$this->accountResetPasswordRequestRepository = null;
}
}