From d72c6e60a8be0fd4d046bd7ca670ffaa6a90ddbd Mon Sep 17 00:00:00 2001 From: styve Lioumba Date: Thu, 27 Nov 2025 22:09:14 +0100 Subject: [PATCH] =?UTF-8?q?feat=20:=20mot=20de=20passe=20oubli=C3=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../authentification-routing.module.ts | 2 + .../forgot-password.component.html | 58 +++++++++ .../forgot-password.component.scss | 0 .../forgot-password.component.spec.ts | 119 ++++++++++++++++++ .../forgot-password.component.ts | 80 ++++++++++++ .../features/login/login.component.html | 2 +- 6 files changed, 260 insertions(+), 1 deletion(-) create mode 100644 src/app/shared/features/forgot-password/forgot-password.component.html create mode 100644 src/app/shared/features/forgot-password/forgot-password.component.scss create mode 100644 src/app/shared/features/forgot-password/forgot-password.component.spec.ts create mode 100644 src/app/shared/features/forgot-password/forgot-password.component.ts diff --git a/src/app/routes/authentification/authentification-routing.module.ts b/src/app/routes/authentification/authentification-routing.module.ts index 45b9c03..607e3f2 100644 --- a/src/app/routes/authentification/authentification-routing.module.ts +++ b/src/app/routes/authentification/authentification-routing.module.ts @@ -3,6 +3,7 @@ import { RouterModule, Routes } from '@angular/router'; import { RegisterComponent } from '@app/shared/features/register/register.component'; import { AuthComponent } from '@app/routes/authentification/auth/auth.component'; import { LoginComponent } from '@app/shared/features/login/login.component'; +import { ForgotPasswordComponent } from '@app/shared/features/forgot-password/forgot-password.component'; const routes: Routes = [ { @@ -11,6 +12,7 @@ const routes: Routes = [ children: [ { path: '', component: LoginComponent }, { path: 'register', component: RegisterComponent }, + { path: 'forgot-password', component: ForgotPasswordComponent }, ], }, { diff --git a/src/app/shared/features/forgot-password/forgot-password.component.html b/src/app/shared/features/forgot-password/forgot-password.component.html new file mode 100644 index 0000000..ce81e26 --- /dev/null +++ b/src/app/shared/features/forgot-password/forgot-password.component.html @@ -0,0 +1,58 @@ +
+ +
+ +
+
+ + + + +
+ +
+ @if (fpForm.get('email')?.invalid && fpForm.get('email')?.touched) { +

Veuillez entrer une adresse email valide

+ } +
+ + + + + +
+

+ Vous n'avez pas de compte ? + + Créez-en un ici + +

+
+
diff --git a/src/app/shared/features/forgot-password/forgot-password.component.scss b/src/app/shared/features/forgot-password/forgot-password.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/src/app/shared/features/forgot-password/forgot-password.component.spec.ts b/src/app/shared/features/forgot-password/forgot-password.component.spec.ts new file mode 100644 index 0000000..879f103 --- /dev/null +++ b/src/app/shared/features/forgot-password/forgot-password.component.spec.ts @@ -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; + + let mockToastrService: Partial; + 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(); + }); +}); diff --git a/src/app/shared/features/forgot-password/forgot-password.component.ts b/src/app/shared/features/forgot-password/forgot-password.component.ts new file mode 100644 index 0000000..0079804 --- /dev/null +++ b/src/app/shared/features/forgot-password/forgot-password.component.ts @@ -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, + } + ); + } +} diff --git a/src/app/shared/features/login/login.component.html b/src/app/shared/features/login/login.component.html index 1988cd7..ada382f 100644 --- a/src/app/shared/features/login/login.component.html +++ b/src/app/shared/features/login/login.component.html @@ -106,7 +106,7 @@