Merge pull request 'feat : maj du mot de passe' (#22) from ttp-16 into main
Reviewed-on: #22 Reviewed-by: technostrea <contact@technostrea.fr>
This commit is contained in:
@@ -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 = {
|
||||
|
||||
@@ -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<Router>;
|
||||
|
||||
let mockAuthRepository: jest.Mocked<Partial<AuthRepository>>;
|
||||
let mockProfileRepo: jest.Mocked<Partial<ProfileRepository>>;
|
||||
// 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');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -18,8 +18,12 @@ export interface AuthRepository {
|
||||
|
||||
get(): User | undefined;
|
||||
|
||||
sendPasswordResetEmail(email: string): Observable<boolean>;
|
||||
resetPassword(token: string, newPassword: string): Observable<boolean>;
|
||||
sendRequestPasswordReset(email: string): Observable<boolean>;
|
||||
confirmPasswordReset(
|
||||
resetToken: string,
|
||||
newPassword: string,
|
||||
confirmPassword: string
|
||||
): Observable<boolean>;
|
||||
|
||||
sendVerificationEmail(email: string): Observable<boolean>;
|
||||
}
|
||||
|
||||
@@ -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<User>(registerDto));
|
||||
}
|
||||
|
||||
resetPassword(token: string, newPassword: string): Observable<boolean> {
|
||||
return of(false);
|
||||
confirmPasswordReset(
|
||||
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));
|
||||
}
|
||||
|
||||
|
||||
@@ -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<ToastrService>;
|
||||
let mockUserRepo: Partial<UserRepository>;
|
||||
let mockAuthRepository: jest.Mocked<Partial<AuthRepository>>;
|
||||
|
||||
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 },
|
||||
],
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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<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 () => {
|
||||
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()
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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<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({
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -30,5 +30,12 @@
|
||||
>
|
||||
<app-user-form [userId]="user!.id" />
|
||||
</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>
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
})
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
0
src/app/testing/usecase/authentification/.gitkeep
Normal file
0
src/app/testing/usecase/authentification/.gitkeep
Normal 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]);
|
||||
});
|
||||
});
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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<boolean>(false);
|
||||
readonly isEmailVerified = signal<boolean>(false);
|
||||
readonly isVerificationEmailSent = signal<boolean>(false);
|
||||
readonly isRequestPasswordSent = signal<boolean>(false);
|
||||
|
||||
readonly user = signal<User | undefined>(undefined);
|
||||
readonly authResponse = signal<AuthResponse | undefined>(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({
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user