feat : maj du mot de passe

This commit is contained in:
styve Lioumba
2025-11-27 16:33:26 +01:00
parent c21f018054
commit 87a4e84c6a
30 changed files with 525 additions and 84 deletions

View File

@@ -22,8 +22,8 @@ describe('AppComponent', () => {
isAuthenticated: jest.fn(), isAuthenticated: jest.fn(),
isEmailVerified: jest.fn(), isEmailVerified: jest.fn(),
register: jest.fn(), register: jest.fn(),
resetPassword: jest.fn(), sendRequestPasswordReset: jest.fn(),
sendPasswordResetEmail: jest.fn(), confirmPasswordReset: jest.fn(),
}; };
mockProfileRepo = { mockProfileRepo = {

View File

@@ -1,50 +1,44 @@
import { TestBed } from '@angular/core/testing'; import { TestBed } from '@angular/core/testing';
import { authGuard } from './auth.guard'; 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 { AuthRepository } from '@app/domain/authentification/auth.repository';
import { ProfileRepository } from '@app/domain/profiles/profile.repository'; import { ProfileRepository } from '@app/domain/profiles/profile.repository';
import { AUTH_REPOSITORY_TOKEN } from '@app/infrastructure/authentification/auth-repository.token'; import { AUTH_REPOSITORY_TOKEN } from '@app/infrastructure/authentification/auth-repository.token';
import { PROFILE_REPOSITORY_TOKEN } from '@app/infrastructure/profiles/profile-repository.token'; import { PROFILE_REPOSITORY_TOKEN } from '@app/infrastructure/profiles/profile-repository.token';
import { AuthFacade } from '@app/ui/authentification/auth.facade';
describe('authGuard', () => { describe('authGuard', () => {
let mockRouter: Partial<Router>; // 1. Définition des variables pour les Mocks
let mockRouter: { parseUrl: jest.Mock };
let mockAuthRepository: jest.Mocked<Partial<AuthRepository>>; let mockAuthFacade: {
let mockProfileRepo: jest.Mocked<Partial<ProfileRepository>>; 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) => const executeGuard: CanActivateFn = (...guardParameters) =>
TestBed.runInInjectionContext(() => authGuard(...guardParameters)); TestBed.runInInjectionContext(() => authGuard(...guardParameters));
beforeEach(() => { beforeEach(() => {
// 3. Initialisation des Mocks
mockRouter = { mockRouter = {
parseUrl: jest.fn(), parseUrl: jest.fn(),
}; };
mockAuthRepository = { mockAuthFacade = {
get: jest.fn(), verifyEmail: jest.fn(),
login: jest.fn(), verifyAuthenticatedUser: jest.fn(),
sendVerificationEmail: jest.fn(), isAuthenticated: jest.fn(), // On changera la valeur de retour selon le test
logout: jest.fn(), isEmailVerified: jest.fn(), // On changera la valeur de retour selon le test
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(),
}; };
TestBed.configureTestingModule({ TestBed.configureTestingModule({
providers: [ providers: [
{ provide: Router, useValue: mockRouter }, { provide: Router, useValue: mockRouter },
{ provide: AUTH_REPOSITORY_TOKEN, useValue: mockAuthRepository }, { provide: AuthFacade, useValue: mockAuthFacade }, // On fournit la Facade, pas le Repo
{ provide: PROFILE_REPOSITORY_TOKEN, useValue: mockProfileRepo },
], ],
}); });
}); });
@@ -53,26 +47,50 @@ describe('authGuard', () => {
expect(executeGuard).toBeTruthy(); expect(executeGuard).toBeTruthy();
}); });
/*it('should allow access if user is valid', () => { // --- SCÉNARIO 1 : L'utilisateur a tout bon ---
const mockRoute = {} as any; it('should return true if user is authenticated AND email is verified', () => {
const mockState = {} as any; // Setup : Tout est OK
mockAuthFacade.isAuthenticated.mockReturnValue(true);
mockAuthFacade.isEmailVerified.mockReturnValue(true);
const result = TestBed.runInInjectionContext(() => executeGuard(mockRoute, mockState)); const result = executeGuard({} as any, {} as any); // On passe des fausses routes/state
expect(result).toEqual(true);
// 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', () => { // --- SCÉNARIO 2 : L'utilisateur n'est pas connecté ---
mockFacade.isAuthenticated(); it('should redirect to /auth if user is NOT authenticated', () => {
mockFacade.isEmailVerified(); // Setup : Pas connecté
mockAuthFacade.isAuthenticated.mockReturnValue(false);
mockAuthFacade.isEmailVerified.mockReturnValue(true); // Peu importe ici
const mockRoute = {} as any; // On prépare le router pour qu'il renvoie une fausse UrlTree
const mockState = {} as any; 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)); // Le guard doit retourner la redirection (UrlTree)
expect(result).toBe(dummyUrlTree);
expect(result).toEqual('/auth' as any);
expect(mockRouter.parseUrl).toHaveBeenCalledWith('/auth'); 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');
});
}); });

View File

@@ -1,22 +1,13 @@
import { TestBed } from '@angular/core/testing'; import { TestBed } from '@angular/core/testing';
import { ThemeService } from './theme.service'; import { ThemeService } from './theme.service';
import { Router } from '@angular/router';
describe('ThemeService', () => { describe('ThemeService', () => {
let service: ThemeService; let service: ThemeService;
const routerSpy = {
navigate: jest.fn(),
navigateByUrl: jest.fn(),
};
beforeEach(() => { beforeEach(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
providers: [ providers: [ThemeService],
{ provide: Router, useValue: routerSpy }, // <<— spy: neutralise la navigation
],
imports: [],
}); });
service = TestBed.inject(ThemeService); service = TestBed.inject(ThemeService);
}); });
@@ -24,4 +15,31 @@ describe('ThemeService', () => {
it('should be created', () => { it('should be created', () => {
expect(service).toBeTruthy(); 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');
});
}); });

View File

@@ -18,8 +18,12 @@ export interface AuthRepository {
get(): User | undefined; get(): User | undefined;
sendPasswordResetEmail(email: string): Observable<boolean>; sendRequestPasswordReset(email: string): Observable<boolean>;
resetPassword(token: string, newPassword: string): Observable<boolean>; confirmPasswordReset(
resetToken: string,
newPassword: string,
confirmPassword: string
): Observable<boolean>;
sendVerificationEmail(email: string): Observable<boolean>; sendVerificationEmail(email: string): Observable<boolean>;
} }

View File

@@ -2,7 +2,7 @@ import { Injectable } from '@angular/core';
import { environment } from '@env/environment'; import { environment } from '@env/environment';
import PocketBase from 'pocketbase'; import PocketBase from 'pocketbase';
import { AuthRepository, AuthResponse } from '@app/domain/authentification/auth.repository'; 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 { User } from '@app/domain/users/user.model';
import { LoginDto } from '@app/domain/authentification/dto/login-dto'; import { LoginDto } from '@app/domain/authentification/dto/login-dto';
import { RegisterDto } from '@app/domain/authentification/dto/register-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<User>(registerDto)); return from(this.pb.collection('users').create<User>(registerDto));
} }
resetPassword(token: string, newPassword: string): Observable<boolean> { confirmPasswordReset(
return of(false); resetToken: string,
newPassword: string,
confirmPassword: string
): Observable<boolean> {
return from(
this.pb.collection('users').confirmPasswordReset(resetToken, newPassword, confirmPassword)
);
} }
sendPasswordResetEmail(email: string): Observable<boolean> { sendRequestPasswordReset(email: string): Observable<boolean> {
return from(this.pb.collection('users').requestPasswordReset(email)); return from(this.pb.collection('users').requestPasswordReset(email));
} }

View File

@@ -9,6 +9,8 @@ import { Profile } from '@app/domain/profiles/profile.model';
import { ToastrService } from 'ngx-toastr'; import { ToastrService } from 'ngx-toastr';
import { USER_REPOSITORY_TOKEN } from '@app/infrastructure/users/user-repository.token'; import { USER_REPOSITORY_TOKEN } from '@app/infrastructure/users/user-repository.token';
import { UserRepository } from '@app/domain/users/user.repository'; 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', () => { describe('MyProfileComponent', () => {
let component: MyProfileComponent; let component: MyProfileComponent;
@@ -17,6 +19,7 @@ describe('MyProfileComponent', () => {
let mockProfileRepo: ProfileRepository; let mockProfileRepo: ProfileRepository;
let mockToastrService: Partial<ToastrService>; let mockToastrService: Partial<ToastrService>;
let mockUserRepo: Partial<UserRepository>; let mockUserRepo: Partial<UserRepository>;
let mockAuthRepository: jest.Mocked<Partial<AuthRepository>>;
beforeEach(async () => { beforeEach(async () => {
mockProfileRepo = { mockProfileRepo = {
@@ -37,11 +40,25 @@ describe('MyProfileComponent', () => {
info: jest.fn(), info: jest.fn(),
error: 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({ await TestBed.configureTestingModule({
imports: [MyProfileComponent], imports: [MyProfileComponent],
providers: [ providers: [
provideRouter([]), provideRouter([]),
{ provide: PROFILE_REPOSITORY_TOKEN, useValue: mockProfileRepo }, { provide: PROFILE_REPOSITORY_TOKEN, useValue: mockProfileRepo },
{ provide: AUTH_REPOSITORY_TOKEN, useValue: mockAuthRepository },
{ provide: USER_REPOSITORY_TOKEN, useValue: mockUserRepo }, { provide: USER_REPOSITORY_TOKEN, useValue: mockUserRepo },
{ provide: ToastrService, useValue: mockToastrService }, { provide: ToastrService, useValue: mockToastrService },
], ],

View File

@@ -43,8 +43,8 @@ describe('MyProfileUpdateProjectFormComponent', () => {
isAuthenticated: jest.fn(), isAuthenticated: jest.fn(),
isEmailVerified: jest.fn(), isEmailVerified: jest.fn(),
register: jest.fn(), register: jest.fn(),
resetPassword: jest.fn(), sendRequestPasswordReset: jest.fn(),
sendPasswordResetEmail: jest.fn(), confirmPasswordReset: jest.fn(),
}; };
mockProfileRepo = { mockProfileRepo = {

View File

@@ -46,8 +46,8 @@ describe('NavBarComponent', () => {
isAuthenticated: jest.fn(), isAuthenticated: jest.fn(),
isEmailVerified: jest.fn(), isEmailVerified: jest.fn(),
register: jest.fn(), register: jest.fn(),
resetPassword: jest.fn(), sendRequestPasswordReset: jest.fn(),
sendPasswordResetEmail: jest.fn(), confirmPasswordReset: jest.fn(),
}; };
mockProfileRepo = { mockProfileRepo = {

View File

@@ -1 +1,47 @@
<p>user-password-form works!</p> @if (loading().action === ActionType.CREATE && loading().isLoading) {
<app-loading message="Mise à jour encours..." />
} @else {
<ng-container *ngTemplateOutlet="form" />
}
<ng-template #form>
<!-- Titre -->
<div class="flex items-center gap-3 pb-4 border-b border-gray-200 dark:border-gray-700">
<div
class="w-10 h-10 bg-indigo-100 dark:bg-indigo-900 rounded-lg flex items-center justify-center"
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="w-6 h-6 text-indigo-600 dark:text-indigo-400"
viewBox="0 0 20 20"
fill="currentColor"
>
<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>
</div>
<h3 class="text-xl font-bold text-gray-900 dark:text-white">Mise à jour du mot de passe</h3>
</div>
<!-- Bouton de soumission -->
<button
type="submit"
(click)="onSubmit()"
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"
>
<span class="flex items-center justify-center gap-2">
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-5 w-5"
viewBox="0 0 20 20"
fill="currentColor"
>
<path
d="M7.707 10.293a1 1 0 10-1.414 1.414l3 3a1 1 0 001.414 0l3-3a1 1 0 00-1.414-1.414L11 11.586V6h5a2 2 0 012 2v7a2 2 0 01-2 2H4a2 2 0 01-2-2V8a2 2 0 012-2h5v5.586l-1.293-1.293zM9 4a1 1 0 012 0v2H9V4z"
/>
</svg>
Réinitialiser le mot de passe
</span>
</button>
</ng-template>

View File

@@ -1,22 +1,92 @@
import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ComponentFixture, TestBed } from '@angular/core/testing';
import { UserPasswordFormComponent } from './user-password-form.component'; 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', () => { describe('UserPasswordFormComponent', () => {
let component: UserPasswordFormComponent; let component: UserPasswordFormComponent;
let fixture: ComponentFixture<UserPasswordFormComponent>; let fixture: ComponentFixture<UserPasswordFormComponent>;
let mockToastrService: Partial<ToastrService>;
// 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 () => { 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({ await TestBed.configureTestingModule({
imports: [UserPasswordFormComponent], imports: [UserPasswordFormComponent],
providers: [
{ provide: ToastrService, useValue: mockToastrService },
{ provide: AuthFacade, useValue: mockAuthFacade }, // On fournit le mock à la place du vrai
],
}).compileComponents(); }).compileComponents();
fixture = TestBed.createComponent(UserPasswordFormComponent); fixture = TestBed.createComponent(UserPasswordFormComponent);
component = fixture.componentInstance; 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', () => { it('should create', () => {
expect(component).toBeTruthy(); 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()
);
});
}); });

View File

@@ -1,33 +1,71 @@
import { Component, inject, Input, output } from '@angular/core'; import { Component, effect, inject, Input, output } from '@angular/core';
import { User } from '@app/domain/users/user.model'; import { ReactiveFormsModule } from '@angular/forms';
import { FormBuilder, FormControl, Validators } 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({ @Component({
selector: 'app-user-password-form', selector: 'app-user-password-form',
standalone: true, standalone: true,
imports: [], imports: [ReactiveFormsModule, LoadingComponent, NgTemplateOutlet],
templateUrl: './user-password-form.component.html', templateUrl: './user-password-form.component.html',
styleUrl: './user-password-form.component.scss', styleUrl: './user-password-form.component.scss',
}) })
export class UserPasswordFormComponent { export class UserPasswordFormComponent {
@Input({ required: true }) user: User | undefined = undefined; private readonly toastrService = inject(ToastrService);
@Input({ required: true }) user: UserViewModel | undefined = undefined;
onFormSubmitted = output<any>(); onFormSubmitted = output<any>();
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({ constructor() {
password: new FormControl('', [Validators.required]), let message = '';
passwordConfirm: new FormControl('', [Validators.required]),
oldPassword: new FormControl('', [Validators.required]), 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;
}
}
}); });
}
onUserPasswordFormSubmit() { onSubmit() {
if (this.userPasswordForm.invalid) { 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; return;
} }
const data = this.userPasswordForm.getRawValue(); this.toastrService.success(
`${message}`,
this.onFormSubmitted.emit(data); `${action === ActionType.CREATE ? 'Mise à jour' : ''}`,
{
closeButton: true,
progressAnimation: 'decreasing',
progressBar: true,
} }
);
}
protected readonly ActionType = ActionType;
} }

View File

@@ -34,8 +34,8 @@ describe('LoginComponent', () => {
isAuthenticated: jest.fn(), isAuthenticated: jest.fn(),
isEmailVerified: jest.fn(), isEmailVerified: jest.fn(),
register: jest.fn(), register: jest.fn(),
resetPassword: jest.fn(), sendRequestPasswordReset: jest.fn(),
sendPasswordResetEmail: jest.fn(), confirmPasswordReset: jest.fn(),
}; };
mockProfileRepo = { mockProfileRepo = {

View File

@@ -38,8 +38,8 @@ describe('RegisterComponent', () => {
isAuthenticated: jest.fn(), isAuthenticated: jest.fn(),
isEmailVerified: jest.fn(), isEmailVerified: jest.fn(),
register: jest.fn(), register: jest.fn(),
resetPassword: jest.fn(), sendRequestPasswordReset: jest.fn(),
sendPasswordResetEmail: jest.fn(), confirmPasswordReset: jest.fn(),
}; };
await TestBed.configureTestingModule({ await TestBed.configureTestingModule({

View File

@@ -30,5 +30,12 @@
> >
<app-user-form [userId]="user!.id" /> <app-user-form [userId]="user!.id" />
</div> </div>
<!-- Section Mot de passe -->
<div
class="bg-white dark:bg-gray-800 rounded-xl shadow-lg p-6 animate-fade-in animation-delay-100"
>
<app-user-password-form [user]="user" />
</div>
</div> </div>
} }

View File

@@ -2,11 +2,12 @@ import { Component, Input } from '@angular/core';
import { UserFormComponent } from '@app/shared/components/user-form/user-form.component'; 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 { UserAvatarFormComponent } from '@app/shared/components/user-avatar-form/user-avatar-form.component';
import { UserViewModel } from '@app/ui/users/user.presenter.model'; import { UserViewModel } from '@app/ui/users/user.presenter.model';
import { UserPasswordFormComponent } from '@app/shared/components/user-password-form/user-password-form.component';
@Component({ @Component({
selector: 'app-update-user', selector: 'app-update-user',
standalone: true, standalone: true,
imports: [UserFormComponent, UserAvatarFormComponent], imports: [UserFormComponent, UserAvatarFormComponent, UserPasswordFormComponent],
templateUrl: './update-user.component.html', templateUrl: './update-user.component.html',
styleUrl: './update-user.component.scss', styleUrl: './update-user.component.scss',
}) })

View File

@@ -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<boolean> {
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<AuthResponse> {
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<User> {
const user = {
...fakeUsers[0],
id: 'fakeId',
email: registerDto.email,
};
fakeUsers.push(user);
return of(user);
}
sendRequestPasswordReset(email: string): Observable<boolean> {
return of(fakeUsers[0].email === email);
}
sendVerificationEmail(email: string): Observable<boolean> {
return of(fakeUsers[0].email === email);
}
}

View File

@@ -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]);
});
});

View File

@@ -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');
});
});

View File

@@ -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);
});
});

View File

@@ -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');
});
});

View File

@@ -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);
});
});

View File

@@ -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();
});
});

View File

@@ -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();
});
});

View File

@@ -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();
});
});

View File

@@ -16,6 +16,7 @@ import { LogoutUseCase } from '@app/usecase/authentification/logout.usecase';
import { VerifyAuthenticatedUsecase } from '@app/usecase/authentification/verify-authenticated.usecase'; import { VerifyAuthenticatedUsecase } from '@app/usecase/authentification/verify-authenticated.usecase';
import { VerifyEmailUseCase } from '@app/usecase/authentification/verify-email.usecase'; import { VerifyEmailUseCase } from '@app/usecase/authentification/verify-email.usecase';
import { GetCurrentUserUseCase } from '@app/usecase/authentification/get-current-user.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' }) @Injectable({ providedIn: 'root' })
export class AuthFacade { export class AuthFacade {
@@ -33,9 +34,14 @@ export class AuthFacade {
private readonly verifyAuthenticatedUseCase = new VerifyAuthenticatedUsecase(this.authRepository); private readonly verifyAuthenticatedUseCase = new VerifyAuthenticatedUsecase(this.authRepository);
private readonly verifyEmailUseCase = new VerifyEmailUseCase(this.authRepository); private readonly verifyEmailUseCase = new VerifyEmailUseCase(this.authRepository);
private readonly senRequestPasswordResetUseCase = new SendRequestPasswordResetUsecase(
this.authRepository
);
readonly isAuthenticated = signal<boolean>(false); readonly isAuthenticated = signal<boolean>(false);
readonly isEmailVerified = signal<boolean>(false); readonly isEmailVerified = signal<boolean>(false);
readonly isVerificationEmailSent = signal<boolean>(false); readonly isVerificationEmailSent = signal<boolean>(false);
readonly isRequestPasswordSent = signal<boolean>(false);
readonly user = signal<User | undefined>(undefined); readonly user = signal<User | undefined>(undefined);
readonly authResponse = signal<AuthResponse | undefined>(undefined); readonly authResponse = signal<AuthResponse | undefined>(undefined);
@@ -94,6 +100,19 @@ export class AuthFacade {
this.user.set(this.getUserUseCase.execute()); 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) { private sendVerificationEmail(email: string) {
this.handleError(ActionType.CREATE, false, null, true); this.handleError(ActionType.CREATE, false, null, true);
this.sendVerificationEmailUseCase.execute(email).subscribe({ this.sendVerificationEmailUseCase.execute(email).subscribe({

View File

@@ -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);
}
}

View File

@@ -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);
}
}