feat : mot de passe oublié
This commit is contained in:
@@ -0,0 +1,58 @@
|
||||
<form [formGroup]="fpForm" (ngSubmit)="onSubmit()" class="space-y-4">
|
||||
<!-- Champ Email -->
|
||||
<div class="space-y-2">
|
||||
<label for="email" class="block text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
Adresse email
|
||||
</label>
|
||||
<div class="relative">
|
||||
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-5 w-5 text-gray-400"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path d="M2.003 5.884L10 9.882l7.997-3.998A2 2 0 0016 4H4a2 2 0 00-1.997 1.884z" />
|
||||
<path d="M18 8.118l-8 4-8-4V14a2 2 0 002 2h12a2 2 0 002-2V8.118z" />
|
||||
</svg>
|
||||
</div>
|
||||
<input
|
||||
formControlName="email"
|
||||
type="email"
|
||||
id="email"
|
||||
name="email"
|
||||
placeholder="votre@email.com"
|
||||
class="w-full pl-10 pr-4 py-3 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white placeholder-gray-400 focus:ring-2 focus:ring-indigo-500 focus:border-transparent transition-all"
|
||||
/>
|
||||
</div>
|
||||
@if (fpForm.get('email')?.invalid && fpForm.get('email')?.touched) {
|
||||
<p class="text-xs text-red-500 mt-1">Veuillez entrer une adresse email valide</p>
|
||||
}
|
||||
</div>
|
||||
|
||||
<!-- Bouton de connexion -->
|
||||
<button
|
||||
type="submit"
|
||||
[disabled]="fpForm.invalid || loading().isLoading"
|
||||
class="w-full py-3 px-4 bg-gradient-to-r from-indigo-600 to-purple-600 hover:from-indigo-700 hover:to-purple-700 text-white font-medium rounded-lg shadow-lg hover:shadow-xl transform hover:-translate-y-0.5 transition-all duration-200 disabled:opacity-50 disabled:cursor-not-allowed disabled:transform-none"
|
||||
>
|
||||
@if (loading().isLoading) {
|
||||
<app-btn-loading message="Demande en cours..." />
|
||||
} @else {
|
||||
Soumettre
|
||||
}
|
||||
</button>
|
||||
|
||||
<!-- Lien vers l'inscription -->
|
||||
<div class="text-center">
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400">
|
||||
Vous n'avez pas de compte ?
|
||||
<a
|
||||
[routerLink]="['register']"
|
||||
class="text-indigo-600 hover:text-indigo-700 dark:text-indigo-400 dark:hover:text-indigo-300 font-semibold transition-colors"
|
||||
>
|
||||
Créez-en un ici
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</form>
|
||||
@@ -0,0 +1,119 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { ForgotPasswordComponent } from './forgot-password.component';
|
||||
import { ToastrService } from 'ngx-toastr';
|
||||
import { signal, WritableSignal } from '@angular/core';
|
||||
import { ActionType } from '@app/domain/action-type.util';
|
||||
import { AuthFacade } from '@app/ui/authentification/auth.facade';
|
||||
import { ActivatedRoute, provideRouter, Router } from '@angular/router';
|
||||
import { Subject } from 'rxjs';
|
||||
|
||||
describe('ForgotPasswordComponent', () => {
|
||||
let component: ForgotPasswordComponent;
|
||||
let fixture: ComponentFixture<ForgotPasswordComponent>;
|
||||
|
||||
let mockToastrService: Partial<ToastrService>;
|
||||
let mockRouter: any;
|
||||
let mockActivatedRoute: any;
|
||||
let mockAuthFacade: {
|
||||
sendRequestPasswordReset: jest.Mock;
|
||||
isRequestPasswordSent: jest.Mock; // <--- AJOUT
|
||||
loading: WritableSignal<{ isLoading: boolean; action: ActionType }>;
|
||||
error: WritableSignal<{ hasError: boolean }>;
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
mockToastrService = {
|
||||
success: jest.fn(),
|
||||
error: jest.fn(),
|
||||
warning: jest.fn(),
|
||||
info: jest.fn(),
|
||||
};
|
||||
|
||||
// 1. CORRECTION DU MOCK ROUTER
|
||||
mockRouter = {
|
||||
navigate: jest.fn().mockResolvedValue(true),
|
||||
createUrlTree: jest.fn(),
|
||||
serializeUrl: jest.fn(),
|
||||
routerState: { root: '' },
|
||||
events: new Subject(), // <--- AJOUT CRUCIAL : RouterLink s'abonne à ceci
|
||||
};
|
||||
|
||||
mockActivatedRoute = {
|
||||
snapshot: { paramMap: { get: () => null } },
|
||||
};
|
||||
|
||||
mockAuthFacade = {
|
||||
sendRequestPasswordReset: jest.fn(),
|
||||
isRequestPasswordSent: jest.fn().mockReturnValue(true),
|
||||
loading: signal({ isLoading: false, action: ActionType.NONE }),
|
||||
error: signal({ hasError: false }),
|
||||
};
|
||||
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [ForgotPasswordComponent],
|
||||
providers: [
|
||||
// provideRouter([]) est inutile si on écrase Router juste en dessous,
|
||||
// mais on peut le laisser par sécurité si d'autres deps en ont besoin.
|
||||
provideRouter([]),
|
||||
{ provide: ToastrService, useValue: mockToastrService },
|
||||
{ provide: AuthFacade, useValue: mockAuthFacade },
|
||||
{ provide: Router, useValue: mockRouter }, // On remplace le vrai routeur par notre mock
|
||||
{ provide: ActivatedRoute, useValue: mockActivatedRoute },
|
||||
],
|
||||
}).compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(ForgotPasswordComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should NOT call sendRequestPasswordReset if form is invalid', () => {
|
||||
component.fpForm.setValue({ email: 'bad-email' });
|
||||
component.onSubmit();
|
||||
expect(component.fpForm.invalid).toBe(true);
|
||||
expect(mockAuthFacade.sendRequestPasswordReset).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should call sendRequestPasswordReset if form is valid', () => {
|
||||
const validEmail = 'test@example.com';
|
||||
component.fpForm.setValue({ email: validEmail });
|
||||
component.onSubmit();
|
||||
expect(mockAuthFacade.sendRequestPasswordReset).toHaveBeenCalledWith(validEmail);
|
||||
});
|
||||
|
||||
it('should show success toast and navigate to /auth when action finishes successfully', () => {
|
||||
component.fpForm.setValue({ email: 'success@test.com' });
|
||||
// On s'assure que la condition isRequestPasswordSent() renvoie bien TRUE
|
||||
mockAuthFacade.isRequestPasswordSent.mockReturnValue(true);
|
||||
|
||||
mockAuthFacade.error.set({ hasError: false });
|
||||
mockAuthFacade.loading.set({ isLoading: false, action: ActionType.CREATE });
|
||||
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(mockToastrService.success).toHaveBeenCalledWith(
|
||||
expect.stringContaining('success@test.com'),
|
||||
expect.anything(),
|
||||
expect.anything()
|
||||
);
|
||||
expect(mockRouter.navigate).toHaveBeenCalledWith(['/auth']);
|
||||
});
|
||||
|
||||
it('should show error toast when action finishes with error', () => {
|
||||
component.fpForm.setValue({ email: 'error@test.com' });
|
||||
|
||||
mockAuthFacade.error.set({ hasError: true });
|
||||
mockAuthFacade.loading.set({ isLoading: false, action: ActionType.CREATE });
|
||||
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(mockToastrService.error).toHaveBeenCalled();
|
||||
expect(mockRouter.navigate).not.toHaveBeenCalled();
|
||||
expect(mockToastrService.success).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,80 @@
|
||||
import { Component, effect, inject } from '@angular/core';
|
||||
import { BtnLoadingComponent } from '@app/shared/components/btn-loading/btn-loading.component';
|
||||
import { FormBuilder, FormsModule, ReactiveFormsModule, Validators } from '@angular/forms';
|
||||
import { Router, RouterLink } from '@angular/router';
|
||||
import { AuthFacade } from '@app/ui/authentification/auth.facade';
|
||||
import { ToastrService } from 'ngx-toastr';
|
||||
import { ActionType } from '@app/domain/action-type.util';
|
||||
|
||||
@Component({
|
||||
selector: 'app-forgot-password',
|
||||
standalone: true,
|
||||
imports: [BtnLoadingComponent, FormsModule, ReactiveFormsModule, RouterLink],
|
||||
templateUrl: './forgot-password.component.html',
|
||||
styleUrl: './forgot-password.component.scss',
|
||||
})
|
||||
export class ForgotPasswordComponent {
|
||||
private readonly fb = inject(FormBuilder);
|
||||
private readonly facade = inject(AuthFacade);
|
||||
private readonly toastrService = inject(ToastrService);
|
||||
protected readonly router = inject(Router);
|
||||
|
||||
fpForm = this.fb.group({
|
||||
email: ['', [Validators.required, Validators.email]],
|
||||
});
|
||||
|
||||
protected readonly loading = this.facade.loading;
|
||||
protected readonly error = this.facade.error;
|
||||
|
||||
constructor() {
|
||||
let message = '';
|
||||
|
||||
effect(() => {
|
||||
if (!this.loading().isLoading) {
|
||||
switch (this.loading().action) {
|
||||
case ActionType.CREATE:
|
||||
message = `Un mail de réinitialisation vous a été envoyé à cette adresse mail : ${this.fpForm.getRawValue().email!}`;
|
||||
this.customToast(ActionType.CREATE, message);
|
||||
if (!this.error().hasError && this.facade.isRequestPasswordSent()) {
|
||||
this.router.navigate(['/auth']);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
onSubmit() {
|
||||
if (this.fpForm.invalid) {
|
||||
this.fpForm.setErrors({ invalidForm: true });
|
||||
return;
|
||||
}
|
||||
this.facade.sendRequestPasswordReset(this.fpForm.getRawValue().email!);
|
||||
}
|
||||
|
||||
private customToast(action: ActionType, message: string): void {
|
||||
if (this.error().hasError) {
|
||||
this.toastrService.error(
|
||||
`Une erreur s'est produite, veuillez réessayer ulterieurement`,
|
||||
`Erreur`,
|
||||
{
|
||||
closeButton: true,
|
||||
progressAnimation: 'decreasing',
|
||||
progressBar: true,
|
||||
}
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
this.toastrService.success(
|
||||
`${message}`,
|
||||
`${action === ActionType.CREATE ? 'Mise à jour' : ''}`,
|
||||
{
|
||||
closeButton: true,
|
||||
progressAnimation: 'decreasing',
|
||||
progressBar: true,
|
||||
disableTimeOut: true,
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -106,7 +106,7 @@
|
||||
<!-- Mot de passe oublié -->
|
||||
<div class="flex justify-end">
|
||||
<a
|
||||
[routerLink]="['/forgot-password']"
|
||||
[routerLink]="['forgot-password']"
|
||||
class="text-sm text-indigo-600 hover:text-indigo-700 dark:text-indigo-400 dark:hover:text-indigo-300 font-medium transition-colors"
|
||||
>
|
||||
Mot de passe oublié ?
|
||||
|
||||
Reference in New Issue
Block a user