From 87a4e84c6a004e0dd7232626fd16f82ce90efb99 Mon Sep 17 00:00:00 2001 From: styve Lioumba Date: Thu, 27 Nov 2025 16:33:26 +0100 Subject: [PATCH] feat : maj du mot de passe --- src/app/app.component.spec.ts | 4 +- .../guard/authentication/auth.guard.spec.ts | 96 +++++++++++-------- .../core/services/theme/theme.service.spec.ts | 38 ++++++-- .../authentification/auth.repository.ts | 8 +- .../authentification/pb-auth.repository.ts | 14 ++- .../my-profile/my-profile.component.spec.ts | 17 ++++ ...file-update-project-form.component.spec.ts | 4 +- .../nav-bar/nav-bar.component.spec.ts | 4 +- .../user-password-form.component.html | 48 +++++++++- .../user-password-form.component.spec.ts | 72 +++++++++++++- .../user-password-form.component.ts | 70 ++++++++++---- .../features/login/login.component.spec.ts | 4 +- .../register/register.component.spec.ts | 4 +- .../update-user/update-user.component.html | 7 ++ .../update-user/update-user.component.ts | 3 +- .../authentification/fake-auth.repository.ts | 55 +++++++++++ .../infrastructure/authentification/.gitkeep} | 0 .../ui/authentification/.gitkeep} | 0 .../testing/usecase/authentification/.gitkeep | 0 .../authentification/get-user.usecase.spec.ts | 14 +++ .../authentification/login.usecase.spec.ts | 21 ++++ .../authentification/logout.usecase.spec.ts | 17 ++++ .../authentification/register.usecase.spec.ts | 21 ++++ .../send-request-password.usecase.spec.ts | 15 +++ .../send-verification-email.usecase.spec.ts | 14 +++ .../verify-auth.usecase.spec.ts | 11 +++ .../verify-email.usecase.spec.ts | 11 +++ src/app/ui/authentification/auth.facade.ts | 19 ++++ .../confirm-password-reset.usecase.ts | 9 ++ .../send-request-password-reset.usecase.ts | 9 ++ 30 files changed, 525 insertions(+), 84 deletions(-) create mode 100644 src/app/testing/domain/authentification/fake-auth.repository.ts rename src/app/{usecase/authentification/reset-password.usecase.ts => testing/infrastructure/authentification/.gitkeep} (100%) rename src/app/{usecase/authentification/send-password-reset.usecase.ts => testing/ui/authentification/.gitkeep} (100%) create mode 100644 src/app/testing/usecase/authentification/.gitkeep create mode 100644 src/app/testing/usecase/authentification/get-user.usecase.spec.ts create mode 100644 src/app/testing/usecase/authentification/login.usecase.spec.ts create mode 100644 src/app/testing/usecase/authentification/logout.usecase.spec.ts create mode 100644 src/app/testing/usecase/authentification/register.usecase.spec.ts create mode 100644 src/app/testing/usecase/authentification/send-request-password.usecase.spec.ts create mode 100644 src/app/testing/usecase/authentification/send-verification-email.usecase.spec.ts create mode 100644 src/app/testing/usecase/authentification/verify-auth.usecase.spec.ts create mode 100644 src/app/testing/usecase/authentification/verify-email.usecase.spec.ts create mode 100644 src/app/usecase/authentification/confirm-password-reset.usecase.ts create mode 100644 src/app/usecase/authentification/send-request-password-reset.usecase.ts diff --git a/src/app/app.component.spec.ts b/src/app/app.component.spec.ts index 8eb843a..3440c00 100644 --- a/src/app/app.component.spec.ts +++ b/src/app/app.component.spec.ts @@ -22,8 +22,8 @@ describe('AppComponent', () => { isAuthenticated: jest.fn(), isEmailVerified: jest.fn(), register: jest.fn(), - resetPassword: jest.fn(), - sendPasswordResetEmail: jest.fn(), + sendRequestPasswordReset: jest.fn(), + confirmPasswordReset: jest.fn(), }; mockProfileRepo = { diff --git a/src/app/core/guard/authentication/auth.guard.spec.ts b/src/app/core/guard/authentication/auth.guard.spec.ts index 6756e63..a40d13f 100644 --- a/src/app/core/guard/authentication/auth.guard.spec.ts +++ b/src/app/core/guard/authentication/auth.guard.spec.ts @@ -1,50 +1,44 @@ import { TestBed } from '@angular/core/testing'; import { authGuard } from './auth.guard'; -import { CanActivateFn, Router } from '@angular/router'; +import { CanActivateFn, Router, UrlTree } from '@angular/router'; import { AuthRepository } from '@app/domain/authentification/auth.repository'; import { ProfileRepository } from '@app/domain/profiles/profile.repository'; import { AUTH_REPOSITORY_TOKEN } from '@app/infrastructure/authentification/auth-repository.token'; import { PROFILE_REPOSITORY_TOKEN } from '@app/infrastructure/profiles/profile-repository.token'; +import { AuthFacade } from '@app/ui/authentification/auth.facade'; describe('authGuard', () => { - let mockRouter: Partial; - - let mockAuthRepository: jest.Mocked>; - let mockProfileRepo: jest.Mocked>; + // 1. Définition des variables pour les Mocks + let mockRouter: { parseUrl: jest.Mock }; + let mockAuthFacade: { + verifyEmail: jest.Mock; + verifyAuthenticatedUser: jest.Mock; + isAuthenticated: jest.Mock; + isEmailVerified: jest.Mock; + }; + // 2. Fonction helper pour exécuter le guard dans le contexte d'injection const executeGuard: CanActivateFn = (...guardParameters) => TestBed.runInInjectionContext(() => authGuard(...guardParameters)); beforeEach(() => { + // 3. Initialisation des Mocks mockRouter = { parseUrl: jest.fn(), }; - mockAuthRepository = { - get: jest.fn(), - login: jest.fn(), - sendVerificationEmail: jest.fn(), - logout: jest.fn(), - isAuthenticated: jest.fn(), - isEmailVerified: jest.fn(), - register: jest.fn(), - resetPassword: jest.fn(), - sendPasswordResetEmail: jest.fn(), - }; - - mockProfileRepo = { - create: jest.fn(), - list: jest.fn(), - update: jest.fn(), - getByUserId: jest.fn(), + mockAuthFacade = { + verifyEmail: jest.fn(), + verifyAuthenticatedUser: jest.fn(), + isAuthenticated: jest.fn(), // On changera la valeur de retour selon le test + isEmailVerified: jest.fn(), // On changera la valeur de retour selon le test }; TestBed.configureTestingModule({ providers: [ { provide: Router, useValue: mockRouter }, - { provide: AUTH_REPOSITORY_TOKEN, useValue: mockAuthRepository }, - { provide: PROFILE_REPOSITORY_TOKEN, useValue: mockProfileRepo }, + { provide: AuthFacade, useValue: mockAuthFacade }, // On fournit la Facade, pas le Repo ], }); }); @@ -53,26 +47,50 @@ describe('authGuard', () => { expect(executeGuard).toBeTruthy(); }); - /*it('should allow access if user is valid', () => { - const mockRoute = {} as any; - const mockState = {} as any; + // --- SCÉNARIO 1 : L'utilisateur a tout bon --- + it('should return true if user is authenticated AND email is verified', () => { + // Setup : Tout est OK + mockAuthFacade.isAuthenticated.mockReturnValue(true); + mockAuthFacade.isEmailVerified.mockReturnValue(true); - const result = TestBed.runInInjectionContext(() => executeGuard(mockRoute, mockState)); - expect(result).toEqual(true); + const result = executeGuard({} as any, {} as any); // On passe des fausses routes/state + + // Vérifications + expect(result).toBe(true); + // On vérifie aussi que le guard a bien lancé les vérifications + expect(mockAuthFacade.verifyEmail).toHaveBeenCalled(); + expect(mockAuthFacade.verifyAuthenticatedUser).toHaveBeenCalled(); }); - it('should redirect to /auth if user is not valid', () => { - mockFacade.isAuthenticated(); - mockFacade.isEmailVerified(); + // --- SCÉNARIO 2 : L'utilisateur n'est pas connecté --- + it('should redirect to /auth if user is NOT authenticated', () => { + // Setup : Pas connecté + mockAuthFacade.isAuthenticated.mockReturnValue(false); + mockAuthFacade.isEmailVerified.mockReturnValue(true); // Peu importe ici - const mockRoute = {} as any; - const mockState = {} as any; + // On prépare le router pour qu'il renvoie une fausse UrlTree + const dummyUrlTree = {} as UrlTree; + mockRouter.parseUrl.mockReturnValue(dummyUrlTree); - (mockRouter.parseUrl as jest.Mock).mockReturnValue('/auth'); + const result = executeGuard({} as any, {} as any); - const result = TestBed.runInInjectionContext(() => executeGuard(mockRoute, mockState)); - - expect(result).toEqual('/auth' as any); + // Le guard doit retourner la redirection (UrlTree) + expect(result).toBe(dummyUrlTree); expect(mockRouter.parseUrl).toHaveBeenCalledWith('/auth'); - });*/ + }); + + // --- SCÉNARIO 3 : Connecté mais email non vérifié --- + it('should redirect to /auth if user is authenticated but email is NOT verified', () => { + // Setup : Connecté mais mail pas bon + mockAuthFacade.isAuthenticated.mockReturnValue(true); + mockAuthFacade.isEmailVerified.mockReturnValue(false); + + const dummyUrlTree = {} as UrlTree; + mockRouter.parseUrl.mockReturnValue(dummyUrlTree); + + const result = executeGuard({} as any, {} as any); + + expect(result).toBe(dummyUrlTree); + expect(mockRouter.parseUrl).toHaveBeenCalledWith('/auth'); + }); }); diff --git a/src/app/core/services/theme/theme.service.spec.ts b/src/app/core/services/theme/theme.service.spec.ts index a2ff531..c5adc12 100644 --- a/src/app/core/services/theme/theme.service.spec.ts +++ b/src/app/core/services/theme/theme.service.spec.ts @@ -1,22 +1,13 @@ import { TestBed } from '@angular/core/testing'; import { ThemeService } from './theme.service'; -import { Router } from '@angular/router'; describe('ThemeService', () => { let service: ThemeService; - const routerSpy = { - navigate: jest.fn(), - navigateByUrl: jest.fn(), - }; - beforeEach(() => { TestBed.configureTestingModule({ - providers: [ - { provide: Router, useValue: routerSpy }, // <<— spy: neutralise la navigation - ], - imports: [], + providers: [ThemeService], }); service = TestBed.inject(ThemeService); }); @@ -24,4 +15,31 @@ describe('ThemeService', () => { it('should be created', () => { expect(service).toBeTruthy(); }); + + // Test de la valeur initiale + it('should have initial value "null"', () => { + // Avec les Signals, on accède à la valeur en exécutant la fonction : signal() + expect(service.darkModeSignal()).toBe('null'); + }); + + // Test du basculement vers Dark + it('should switch to "dark" when updateDarkMode is called and current is "null"', () => { + // Action + service.updateDarkMode(); + + // Vérification + expect(service.darkModeSignal()).toBe('dark'); + }); + + // Test du basculement inverse (Dark vers Null) + it('should switch back to "null" when updateDarkMode is called and current is "dark"', () => { + // 1. Préparation : On force l'état à 'dark' pour tester ce cas précis + service.darkModeSignal.set('dark'); + + // 2. Action + service.updateDarkMode(); + + // 3. Vérification + expect(service.darkModeSignal()).toBe('null'); + }); }); diff --git a/src/app/domain/authentification/auth.repository.ts b/src/app/domain/authentification/auth.repository.ts index b36c1a1..5e67c5a 100644 --- a/src/app/domain/authentification/auth.repository.ts +++ b/src/app/domain/authentification/auth.repository.ts @@ -18,8 +18,12 @@ export interface AuthRepository { get(): User | undefined; - sendPasswordResetEmail(email: string): Observable; - resetPassword(token: string, newPassword: string): Observable; + sendRequestPasswordReset(email: string): Observable; + confirmPasswordReset( + resetToken: string, + newPassword: string, + confirmPassword: string + ): Observable; sendVerificationEmail(email: string): Observable; } diff --git a/src/app/infrastructure/authentification/pb-auth.repository.ts b/src/app/infrastructure/authentification/pb-auth.repository.ts index 2b5b8ee..f1abafd 100644 --- a/src/app/infrastructure/authentification/pb-auth.repository.ts +++ b/src/app/infrastructure/authentification/pb-auth.repository.ts @@ -2,7 +2,7 @@ import { Injectable } from '@angular/core'; import { environment } from '@env/environment'; import PocketBase from 'pocketbase'; import { AuthRepository, AuthResponse } from '@app/domain/authentification/auth.repository'; -import { from, map, Observable, of } from 'rxjs'; +import { from, map, Observable } from 'rxjs'; import { User } from '@app/domain/users/user.model'; import { LoginDto } from '@app/domain/authentification/dto/login-dto'; import { RegisterDto } from '@app/domain/authentification/dto/register-dto'; @@ -46,11 +46,17 @@ export class PbAuthRepository implements AuthRepository { return from(this.pb.collection('users').create(registerDto)); } - resetPassword(token: string, newPassword: string): Observable { - return of(false); + confirmPasswordReset( + resetToken: string, + newPassword: string, + confirmPassword: string + ): Observable { + return from( + this.pb.collection('users').confirmPasswordReset(resetToken, newPassword, confirmPassword) + ); } - sendPasswordResetEmail(email: string): Observable { + sendRequestPasswordReset(email: string): Observable { return from(this.pb.collection('users').requestPasswordReset(email)); } diff --git a/src/app/routes/my-profile/my-profile.component.spec.ts b/src/app/routes/my-profile/my-profile.component.spec.ts index 72a48a5..7851222 100644 --- a/src/app/routes/my-profile/my-profile.component.spec.ts +++ b/src/app/routes/my-profile/my-profile.component.spec.ts @@ -9,6 +9,8 @@ import { Profile } from '@app/domain/profiles/profile.model'; import { ToastrService } from 'ngx-toastr'; import { USER_REPOSITORY_TOKEN } from '@app/infrastructure/users/user-repository.token'; import { UserRepository } from '@app/domain/users/user.repository'; +import { AuthRepository } from '@app/domain/authentification/auth.repository'; +import { AUTH_REPOSITORY_TOKEN } from '@app/infrastructure/authentification/auth-repository.token'; describe('MyProfileComponent', () => { let component: MyProfileComponent; @@ -17,6 +19,7 @@ describe('MyProfileComponent', () => { let mockProfileRepo: ProfileRepository; let mockToastrService: Partial; let mockUserRepo: Partial; + let mockAuthRepository: jest.Mocked>; beforeEach(async () => { mockProfileRepo = { @@ -37,11 +40,25 @@ describe('MyProfileComponent', () => { info: jest.fn(), error: jest.fn(), }; + + mockAuthRepository = { + get: jest.fn(), + login: jest.fn(), + sendVerificationEmail: jest.fn(), + logout: jest.fn(), + isAuthenticated: jest.fn(), + isEmailVerified: jest.fn(), + register: jest.fn(), + sendRequestPasswordReset: jest.fn(), + confirmPasswordReset: jest.fn(), + }; + await TestBed.configureTestingModule({ imports: [MyProfileComponent], providers: [ provideRouter([]), { provide: PROFILE_REPOSITORY_TOKEN, useValue: mockProfileRepo }, + { provide: AUTH_REPOSITORY_TOKEN, useValue: mockAuthRepository }, { provide: USER_REPOSITORY_TOKEN, useValue: mockUserRepo }, { provide: ToastrService, useValue: mockToastrService }, ], diff --git a/src/app/shared/components/my-profile-update-project-form/my-profile-update-project-form.component.spec.ts b/src/app/shared/components/my-profile-update-project-form/my-profile-update-project-form.component.spec.ts index 158092c..8a802ce 100644 --- a/src/app/shared/components/my-profile-update-project-form/my-profile-update-project-form.component.spec.ts +++ b/src/app/shared/components/my-profile-update-project-form/my-profile-update-project-form.component.spec.ts @@ -43,8 +43,8 @@ describe('MyProfileUpdateProjectFormComponent', () => { isAuthenticated: jest.fn(), isEmailVerified: jest.fn(), register: jest.fn(), - resetPassword: jest.fn(), - sendPasswordResetEmail: jest.fn(), + sendRequestPasswordReset: jest.fn(), + confirmPasswordReset: jest.fn(), }; mockProfileRepo = { diff --git a/src/app/shared/components/nav-bar/nav-bar.component.spec.ts b/src/app/shared/components/nav-bar/nav-bar.component.spec.ts index 6b305e8..9e0aefe 100644 --- a/src/app/shared/components/nav-bar/nav-bar.component.spec.ts +++ b/src/app/shared/components/nav-bar/nav-bar.component.spec.ts @@ -46,8 +46,8 @@ describe('NavBarComponent', () => { isAuthenticated: jest.fn(), isEmailVerified: jest.fn(), register: jest.fn(), - resetPassword: jest.fn(), - sendPasswordResetEmail: jest.fn(), + sendRequestPasswordReset: jest.fn(), + confirmPasswordReset: jest.fn(), }; mockProfileRepo = { diff --git a/src/app/shared/components/user-password-form/user-password-form.component.html b/src/app/shared/components/user-password-form/user-password-form.component.html index e0ca9a7..8896bb2 100644 --- a/src/app/shared/components/user-password-form/user-password-form.component.html +++ b/src/app/shared/components/user-password-form/user-password-form.component.html @@ -1 +1,47 @@ -

user-password-form works!

+@if (loading().action === ActionType.CREATE && loading().isLoading) { + +} @else { + +} + + +
+
+ + + +
+

Mise à jour du mot de passe

+
+ + +
diff --git a/src/app/shared/components/user-password-form/user-password-form.component.spec.ts b/src/app/shared/components/user-password-form/user-password-form.component.spec.ts index 8c95a6e..c597cc1 100644 --- a/src/app/shared/components/user-password-form/user-password-form.component.spec.ts +++ b/src/app/shared/components/user-password-form/user-password-form.component.spec.ts @@ -1,22 +1,92 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { UserPasswordFormComponent } from './user-password-form.component'; +import { ToastrService } from 'ngx-toastr'; +import { AuthFacade } from '@app/ui/authentification/auth.facade'; +import { By } from '@angular/platform-browser'; +import { ActionType } from '@app/domain/action-type.util'; +import { signal, WritableSignal } from '@angular/core'; describe('UserPasswordFormComponent', () => { let component: UserPasswordFormComponent; let fixture: ComponentFixture; + let mockToastrService: Partial; + + // On mocke la Facade car c'est ce que le composant utilise directement + let mockAuthFacade: { + sendRequestPasswordReset: jest.Mock; + loading: WritableSignal<{ isLoading: boolean; action: ActionType }>; + error: WritableSignal<{ hasError: boolean }>; + }; + beforeEach(async () => { + mockToastrService = { + warning: jest.fn(), + success: jest.fn(), + info: jest.fn(), + error: jest.fn(), + }; + + mockAuthFacade = { + sendRequestPasswordReset: jest.fn(), + loading: signal({ isLoading: false, action: ActionType.NONE }), + error: signal({ hasError: false }), + }; + await TestBed.configureTestingModule({ imports: [UserPasswordFormComponent], + providers: [ + { provide: ToastrService, useValue: mockToastrService }, + { provide: AuthFacade, useValue: mockAuthFacade }, // On fournit le mock à la place du vrai + ], }).compileComponents(); fixture = TestBed.createComponent(UserPasswordFormComponent); component = fixture.componentInstance; - fixture.detectChanges(); + // 3. Input obligatoire + component.user = { email: 'test@example.com' } as any; + + fixture.detectChanges(); // Déclenche le ngOnInit et le premier rendu }); it('should create', () => { expect(component).toBeTruthy(); }); + + // --- TEST DE L'UI (Interaction utilisateur) --- + it('should call sendRequestPasswordReset when the submit button is clicked', () => { + // Simulation du clic + const button = fixture.debugElement.query(By.css('button')); + button.nativeElement.click(); + + // Vérification + expect(mockAuthFacade.sendRequestPasswordReset).toHaveBeenCalledWith('test@example.com'); + }); + + // --- TEST DE L'UI (État de chargement) --- + it('should show loading spinner when loading action is CREATE', () => { + // Mise à jour du signal + mockAuthFacade.loading.set({ isLoading: true, action: ActionType.CREATE }); + fixture.detectChanges(); // Mise à jour du DOM + + const loadingComponent = fixture.debugElement.query(By.css('app-loading')); + expect(loadingComponent).toBeTruthy(); // Le composant de loading doit être là + }); + + // --- TEST DE LOGIQUE (Effect & Toast) --- + it('should show success toast when action finishes successfully', () => { + // 1. On simule la fin du chargement (isLoading: false) avec l'action CREATE + mockAuthFacade.loading.set({ isLoading: false, action: ActionType.CREATE }); + mockAuthFacade.error.set({ hasError: false }); + + // Nécessaire pour déclencher l'effect() + fixture.detectChanges(); + + expect(mockToastrService.success).toHaveBeenCalledWith( + expect.stringContaining('test@example.com'), + 'Mise à jour', + expect.anything() + ); + }); }); diff --git a/src/app/shared/components/user-password-form/user-password-form.component.ts b/src/app/shared/components/user-password-form/user-password-form.component.ts index 2935c47..db5c583 100644 --- a/src/app/shared/components/user-password-form/user-password-form.component.ts +++ b/src/app/shared/components/user-password-form/user-password-form.component.ts @@ -1,33 +1,71 @@ -import { Component, inject, Input, output } from '@angular/core'; -import { User } from '@app/domain/users/user.model'; -import { FormBuilder, FormControl, Validators } from '@angular/forms'; +import { Component, effect, inject, Input, output } from '@angular/core'; +import { ReactiveFormsModule } from '@angular/forms'; +import { UserViewModel } from '@app/ui/users/user.presenter.model'; +import { AuthFacade } from '@app/ui/authentification/auth.facade'; +import { ActionType } from '@app/domain/action-type.util'; +import { ToastrService } from 'ngx-toastr'; +import { LoadingComponent } from '@app/shared/components/loading/loading.component'; +import { NgTemplateOutlet } from '@angular/common'; @Component({ selector: 'app-user-password-form', standalone: true, - imports: [], + imports: [ReactiveFormsModule, LoadingComponent, NgTemplateOutlet], templateUrl: './user-password-form.component.html', styleUrl: './user-password-form.component.scss', }) export class UserPasswordFormComponent { - @Input({ required: true }) user: User | undefined = undefined; + private readonly toastrService = inject(ToastrService); + @Input({ required: true }) user: UserViewModel | undefined = undefined; onFormSubmitted = output(); - private fb = inject(FormBuilder); + private readonly authFacade = inject(AuthFacade); + protected readonly loading = this.authFacade.loading; + protected readonly error = this.authFacade.error; - protected userPasswordForm = this.fb.group({ - password: new FormControl('', [Validators.required]), - passwordConfirm: new FormControl('', [Validators.required]), - oldPassword: new FormControl('', [Validators.required]), - }); + constructor() { + let message = ''; - onUserPasswordFormSubmit() { - if (this.userPasswordForm.invalid) { + 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.user!.email}`; + this.customToast(ActionType.CREATE, message); + break; + } + } + }); + } + + onSubmit() { + this.authFacade.sendRequestPasswordReset(this.user!.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; } - const data = this.userPasswordForm.getRawValue(); - - this.onFormSubmitted.emit(data); + this.toastrService.success( + `${message}`, + `${action === ActionType.CREATE ? 'Mise à jour' : ''}`, + { + closeButton: true, + progressAnimation: 'decreasing', + progressBar: true, + } + ); } + + protected readonly ActionType = ActionType; } diff --git a/src/app/shared/features/login/login.component.spec.ts b/src/app/shared/features/login/login.component.spec.ts index 79a6415..20f370d 100644 --- a/src/app/shared/features/login/login.component.spec.ts +++ b/src/app/shared/features/login/login.component.spec.ts @@ -34,8 +34,8 @@ describe('LoginComponent', () => { isAuthenticated: jest.fn(), isEmailVerified: jest.fn(), register: jest.fn(), - resetPassword: jest.fn(), - sendPasswordResetEmail: jest.fn(), + sendRequestPasswordReset: jest.fn(), + confirmPasswordReset: jest.fn(), }; mockProfileRepo = { diff --git a/src/app/shared/features/register/register.component.spec.ts b/src/app/shared/features/register/register.component.spec.ts index 079160b..9e207e6 100644 --- a/src/app/shared/features/register/register.component.spec.ts +++ b/src/app/shared/features/register/register.component.spec.ts @@ -38,8 +38,8 @@ describe('RegisterComponent', () => { isAuthenticated: jest.fn(), isEmailVerified: jest.fn(), register: jest.fn(), - resetPassword: jest.fn(), - sendPasswordResetEmail: jest.fn(), + sendRequestPasswordReset: jest.fn(), + confirmPasswordReset: jest.fn(), }; await TestBed.configureTestingModule({ diff --git a/src/app/shared/features/update-user/update-user.component.html b/src/app/shared/features/update-user/update-user.component.html index 548d469..586ac1c 100644 --- a/src/app/shared/features/update-user/update-user.component.html +++ b/src/app/shared/features/update-user/update-user.component.html @@ -30,5 +30,12 @@ > + + +
+ +
} diff --git a/src/app/shared/features/update-user/update-user.component.ts b/src/app/shared/features/update-user/update-user.component.ts index 78fe44e..a117e17 100644 --- a/src/app/shared/features/update-user/update-user.component.ts +++ b/src/app/shared/features/update-user/update-user.component.ts @@ -2,11 +2,12 @@ import { Component, Input } from '@angular/core'; import { UserFormComponent } from '@app/shared/components/user-form/user-form.component'; import { UserAvatarFormComponent } from '@app/shared/components/user-avatar-form/user-avatar-form.component'; import { UserViewModel } from '@app/ui/users/user.presenter.model'; +import { UserPasswordFormComponent } from '@app/shared/components/user-password-form/user-password-form.component'; @Component({ selector: 'app-update-user', standalone: true, - imports: [UserFormComponent, UserAvatarFormComponent], + imports: [UserFormComponent, UserAvatarFormComponent, UserPasswordFormComponent], templateUrl: './update-user.component.html', styleUrl: './update-user.component.scss', }) diff --git a/src/app/testing/domain/authentification/fake-auth.repository.ts b/src/app/testing/domain/authentification/fake-auth.repository.ts new file mode 100644 index 0000000..344b579 --- /dev/null +++ b/src/app/testing/domain/authentification/fake-auth.repository.ts @@ -0,0 +1,55 @@ +import { AuthRepository, AuthResponse } from '@app/domain/authentification/auth.repository'; +import { Observable, of } from 'rxjs'; +import { User } from '@app/domain/users/user.model'; +import { LoginDto } from '@app/domain/authentification/dto/login-dto'; +import { RegisterDto } from '@app/domain/authentification/dto/register-dto'; +import { fakeUsers } from '@app/testing/user.mock'; + +export class FakeAuthRepository implements AuthRepository { + confirmPasswordReset( + resetToken: string, + newPassword: string, + confirmPassword: string + ): Observable { + return of(true); + } + + get(): User | undefined { + return fakeUsers[0]; + } + + isAuthenticated(): boolean { + return fakeUsers[0] !== undefined; + } + + isEmailVerified(): boolean { + return fakeUsers[0].verified; + } + + login(loginDto: LoginDto): Observable { + const user = fakeUsers.find((u) => u.email === loginDto.email); + return of({ isValid: true, record: user, token: 'fakeToken' } as AuthResponse); + } + + logout(): void { + fakeUsers.pop(); + } + + register(registerDto: RegisterDto): Observable { + const user = { + ...fakeUsers[0], + id: 'fakeId', + email: registerDto.email, + }; + fakeUsers.push(user); + return of(user); + } + + sendRequestPasswordReset(email: string): Observable { + return of(fakeUsers[0].email === email); + } + + sendVerificationEmail(email: string): Observable { + return of(fakeUsers[0].email === email); + } +} diff --git a/src/app/usecase/authentification/reset-password.usecase.ts b/src/app/testing/infrastructure/authentification/.gitkeep similarity index 100% rename from src/app/usecase/authentification/reset-password.usecase.ts rename to src/app/testing/infrastructure/authentification/.gitkeep diff --git a/src/app/usecase/authentification/send-password-reset.usecase.ts b/src/app/testing/ui/authentification/.gitkeep similarity index 100% rename from src/app/usecase/authentification/send-password-reset.usecase.ts rename to src/app/testing/ui/authentification/.gitkeep diff --git a/src/app/testing/usecase/authentification/.gitkeep b/src/app/testing/usecase/authentification/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/src/app/testing/usecase/authentification/get-user.usecase.spec.ts b/src/app/testing/usecase/authentification/get-user.usecase.spec.ts new file mode 100644 index 0000000..038e0b2 --- /dev/null +++ b/src/app/testing/usecase/authentification/get-user.usecase.spec.ts @@ -0,0 +1,14 @@ +import { FakeAuthRepository } from '@app/testing/domain/authentification/fake-auth.repository'; +import { GetCurrentUserUseCase } from '@app/usecase/authentification/get-current-user.usecase'; +import { fakeUsers } from '@app/testing/user.mock'; + +describe('GetUserUsecase', () => { + it("devrait retourner l'utilisateur connecté ", () => { + const repo = new FakeAuthRepository(); + const useCase = new GetCurrentUserUseCase(repo); + + const res = useCase.execute(); + expect(res).toBeTruthy(); + expect(res).toBe(fakeUsers[0]); + }); +}); diff --git a/src/app/testing/usecase/authentification/login.usecase.spec.ts b/src/app/testing/usecase/authentification/login.usecase.spec.ts new file mode 100644 index 0000000..35e58a0 --- /dev/null +++ b/src/app/testing/usecase/authentification/login.usecase.spec.ts @@ -0,0 +1,21 @@ +import { FakeAuthRepository } from '@app/testing/domain/authentification/fake-auth.repository'; +import { LoginUseCase } from '@app/usecase/authentification/login.usecase'; +import { LoginDto } from '@app/domain/authentification/dto/login-dto'; +import { firstValueFrom } from 'rxjs'; + +describe('LoginUsecase', () => { + it('devrait se connecter ', async () => { + const repo = new FakeAuthRepository(); + const useCase = new LoginUseCase(repo); + + const loginDto: LoginDto = { + email: 'foo@bar.com', + password: 'password1234', + }; + + const response = await firstValueFrom(useCase.execute(loginDto)); + + expect(response.isValid).toBe(true); + expect(response.token).toEqual('fakeToken'); + }); +}); diff --git a/src/app/testing/usecase/authentification/logout.usecase.spec.ts b/src/app/testing/usecase/authentification/logout.usecase.spec.ts new file mode 100644 index 0000000..3a69b33 --- /dev/null +++ b/src/app/testing/usecase/authentification/logout.usecase.spec.ts @@ -0,0 +1,17 @@ +import { FakeAuthRepository } from '@app/testing/domain/authentification/fake-auth.repository'; +import { LogoutUseCase } from '@app/usecase/authentification/logout.usecase'; +import { fakeUsers } from '@app/testing/user.mock'; + +describe('LogoutUsecase', () => { + it("devrait supprimer les infos de l'utilisateur en cache ", async () => { + const repo = new FakeAuthRepository(); + const useCase = new LogoutUseCase(repo); + + expect(fakeUsers[0]).toBeTruthy(); + + useCase.execute(); + + expect(fakeUsers[0]).toBeUndefined(); + expect(fakeUsers.length).toBe(0); + }); +}); diff --git a/src/app/testing/usecase/authentification/register.usecase.spec.ts b/src/app/testing/usecase/authentification/register.usecase.spec.ts new file mode 100644 index 0000000..ee0aa57 --- /dev/null +++ b/src/app/testing/usecase/authentification/register.usecase.spec.ts @@ -0,0 +1,21 @@ +import { FakeAuthRepository } from '@app/testing/domain/authentification/fake-auth.repository'; +import { RegisterUseCase } from '@app/usecase/authentification/register.usecase'; +import { RegisterDto } from '@app/domain/authentification/dto/register-dto'; +import { firstValueFrom } from 'rxjs'; + +describe('RegisterUsecase', () => { + it("devrait s'enregister", async () => { + const repo = new FakeAuthRepository(); + const useCase = new RegisterUseCase(repo); + + const registerDto: RegisterDto = { + email: 'bar@foo.com', + emailVisibility: false, + password: 'password1234', + passwordConfirm: 'password1234', + }; + const response = await firstValueFrom(useCase.execute(registerDto)); + + expect(response.email).toEqual('bar@foo.com'); + }); +}); diff --git a/src/app/testing/usecase/authentification/send-request-password.usecase.spec.ts b/src/app/testing/usecase/authentification/send-request-password.usecase.spec.ts new file mode 100644 index 0000000..881ad0f --- /dev/null +++ b/src/app/testing/usecase/authentification/send-request-password.usecase.spec.ts @@ -0,0 +1,15 @@ +import { FakeAuthRepository } from '@app/testing/domain/authentification/fake-auth.repository'; +import { SendRequestPasswordResetUsecase } from '@app/usecase/authentification/send-request-password-reset.usecase'; +import { firstValueFrom } from 'rxjs'; + +describe('SendRequestPasswordUsecase', () => { + it('devrait envoyer une demande de maj du mot de passe ', async () => { + const repo = new FakeAuthRepository(); + const useCase = new SendRequestPasswordResetUsecase(repo); + + const email = 'foo@bar.com'; + + const res = await firstValueFrom(useCase.execute(email)); + expect(res).toEqual(true); + }); +}); diff --git a/src/app/testing/usecase/authentification/send-verification-email.usecase.spec.ts b/src/app/testing/usecase/authentification/send-verification-email.usecase.spec.ts new file mode 100644 index 0000000..a12b78d --- /dev/null +++ b/src/app/testing/usecase/authentification/send-verification-email.usecase.spec.ts @@ -0,0 +1,14 @@ +import { FakeAuthRepository } from '@app/testing/domain/authentification/fake-auth.repository'; +import { SendVerificationEmailUsecase } from '@app/usecase/authentification/send-verification-email.usecase'; +import { firstValueFrom } from 'rxjs'; + +describe('SendVerificationEmailUsecase', () => { + it('devrait effectuer une verification de mail ', async () => { + const repo = new FakeAuthRepository(); + const useCase = new SendVerificationEmailUsecase(repo); + const email = 'foo@bar.com'; + + const response = await firstValueFrom(useCase.execute(email)); + expect(response).toBeTruthy(); + }); +}); diff --git a/src/app/testing/usecase/authentification/verify-auth.usecase.spec.ts b/src/app/testing/usecase/authentification/verify-auth.usecase.spec.ts new file mode 100644 index 0000000..fa926be --- /dev/null +++ b/src/app/testing/usecase/authentification/verify-auth.usecase.spec.ts @@ -0,0 +1,11 @@ +import { FakeAuthRepository } from '@app/testing/domain/authentification/fake-auth.repository'; +import { VerifyAuthenticatedUsecase } from '@app/usecase/authentification/verify-authenticated.usecase'; + +describe('VerifyAuthenticationUseCase', () => { + it("devrait retourner l'utilisateur authentifier ", () => { + const repo = new FakeAuthRepository(); + const useCase = new VerifyAuthenticatedUsecase(repo); + + expect(useCase.execute()).toBeTruthy(); + }); +}); diff --git a/src/app/testing/usecase/authentification/verify-email.usecase.spec.ts b/src/app/testing/usecase/authentification/verify-email.usecase.spec.ts new file mode 100644 index 0000000..1a7da32 --- /dev/null +++ b/src/app/testing/usecase/authentification/verify-email.usecase.spec.ts @@ -0,0 +1,11 @@ +import { FakeAuthRepository } from '@app/testing/domain/authentification/fake-auth.repository'; +import { VerifyEmailUseCase } from '@app/usecase/authentification/verify-email.usecase'; + +describe('VerifyEmailUseCase', () => { + it("devrait verifier le mail de l'utilisateur ", () => { + const repo = new FakeAuthRepository(); + const useCase = new VerifyEmailUseCase(repo); + + expect(useCase.execute()).toBeFalsy(); + }); +}); diff --git a/src/app/ui/authentification/auth.facade.ts b/src/app/ui/authentification/auth.facade.ts index 76f3973..acb10c7 100644 --- a/src/app/ui/authentification/auth.facade.ts +++ b/src/app/ui/authentification/auth.facade.ts @@ -16,6 +16,7 @@ import { LogoutUseCase } from '@app/usecase/authentification/logout.usecase'; import { VerifyAuthenticatedUsecase } from '@app/usecase/authentification/verify-authenticated.usecase'; import { VerifyEmailUseCase } from '@app/usecase/authentification/verify-email.usecase'; import { GetCurrentUserUseCase } from '@app/usecase/authentification/get-current-user.usecase'; +import { SendRequestPasswordResetUsecase } from '@app/usecase/authentification/send-request-password-reset.usecase'; @Injectable({ providedIn: 'root' }) export class AuthFacade { @@ -33,9 +34,14 @@ export class AuthFacade { private readonly verifyAuthenticatedUseCase = new VerifyAuthenticatedUsecase(this.authRepository); private readonly verifyEmailUseCase = new VerifyEmailUseCase(this.authRepository); + private readonly senRequestPasswordResetUseCase = new SendRequestPasswordResetUsecase( + this.authRepository + ); + readonly isAuthenticated = signal(false); readonly isEmailVerified = signal(false); readonly isVerificationEmailSent = signal(false); + readonly isRequestPasswordSent = signal(false); readonly user = signal(undefined); readonly authResponse = signal(undefined); @@ -94,6 +100,19 @@ export class AuthFacade { this.user.set(this.getUserUseCase.execute()); } + sendRequestPasswordReset(email: string) { + this.handleError(ActionType.CREATE, false, null, true); + this.senRequestPasswordResetUseCase.execute(email).subscribe({ + next: (res) => { + this.isRequestPasswordSent.set(res); + this.handleError(ActionType.CREATE, false, null, false); + }, + error: (err) => { + this.handleError(ActionType.CREATE, true, err.message, false); + }, + }); + } + private sendVerificationEmail(email: string) { this.handleError(ActionType.CREATE, false, null, true); this.sendVerificationEmailUseCase.execute(email).subscribe({ diff --git a/src/app/usecase/authentification/confirm-password-reset.usecase.ts b/src/app/usecase/authentification/confirm-password-reset.usecase.ts new file mode 100644 index 0000000..be286da --- /dev/null +++ b/src/app/usecase/authentification/confirm-password-reset.usecase.ts @@ -0,0 +1,9 @@ +import { AuthRepository } from '@app/domain/authentification/auth.repository'; + +export class ConfirmPasswordResetUsecase { + constructor(private readonly authRepo: AuthRepository) {} + + execute(resetToken: string, newPassword: string, confirmPassword: string) { + return this.authRepo.confirmPasswordReset(resetToken, newPassword, confirmPassword); + } +} diff --git a/src/app/usecase/authentification/send-request-password-reset.usecase.ts b/src/app/usecase/authentification/send-request-password-reset.usecase.ts new file mode 100644 index 0000000..9b60a9f --- /dev/null +++ b/src/app/usecase/authentification/send-request-password-reset.usecase.ts @@ -0,0 +1,9 @@ +import { AuthRepository } from '@app/domain/authentification/auth.repository'; + +export class SendRequestPasswordResetUsecase { + constructor(private readonly authRepo: AuthRepository) {} + + execute(email: string) { + return this.authRepo.sendRequestPasswordReset(email); + } +}