refacto et TU user

This commit is contained in:
styve Lioumba
2025-11-17 17:59:45 +01:00
parent 778cb95724
commit 9669b2b5b4
11 changed files with 377 additions and 185 deletions

View File

@@ -1,6 +1,11 @@
@if (user) {
<h3 class="font-ubuntu font-bold text-xl uppercase dark:text-white mb-4">Ma photo de profil</h3>
@if (loading().action === ActionType.UPDATE && loading().isLoading && onSubmitted()) {
<app-loading message="Mise à jour de la photo de profile..." />
} @else {
<ng-container *ngTemplateOutlet="content" />
}
<ng-template #content>
@if (user) {
<div
class="w-40 h-40 rounded-full inline-flex items-center justify-center bg-gray-200 text-gray-400"
>
@@ -27,12 +32,12 @@
/>
}
</div>
}
}
<label
<label
for="uploadFile1"
class="flex bg-gray-800 hover:bg-gray-700 text-white text-base px-5 py-3 outline-none rounded w-max cursor-pointer mx-auto font-[sans-serif]"
>
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="w-4 h-4 mr-2 fill-white inline"
@@ -55,9 +60,9 @@
accept="image/*"
(change)="onPictureChange($event)"
/>
</label>
</label>
@if (file != null || imagePreviewUrl != null) {
@if (file != null || imagePreviewUrl != null) {
<button
type="button"
[ngClass]="{ 'bg-purple-600': file != null || imagePreviewUrl != null }"
@@ -66,4 +71,5 @@
>
Mettre à jour ma photo de profile
</button>
}
}
</ng-template>

View File

@@ -1,17 +1,17 @@
import { Component, effect, inject, Input, output } from '@angular/core';
import { Component, effect, inject, Input, output, signal } from '@angular/core';
import { User } from '@app/domain/users/user.model';
import { ReactiveFormsModule } from '@angular/forms';
import { AuthService } from '@app/core/services/authentication/auth.service';
import { environment } from '@env/environment';
import { NgClass } from '@angular/common';
import { NgClass, NgTemplateOutlet } from '@angular/common';
import { ToastrService } from 'ngx-toastr';
import { UserFacade } from '@app/ui/users/user.facade';
import { ActionType } from '@app/domain/action-type.util';
import { LoadingComponent } from '@app/shared/components/loading/loading.component';
@Component({
selector: 'app-user-avatar-form',
standalone: true,
imports: [ReactiveFormsModule, NgClass],
imports: [ReactiveFormsModule, NgClass, NgTemplateOutlet, LoadingComponent],
templateUrl: './user-avatar-form.component.html',
styleUrl: './user-avatar-form.component.scss',
})
@@ -23,10 +23,9 @@ export class UserAvatarFormComponent {
onFormSubmitted = output<any>();
private authService = inject(AuthService);
file: File | null = null; // Variable to store file
imagePreviewUrl: string | null = null; // URL for image preview
protected onSubmitted = signal<boolean>(false);
protected readonly loading = this.facade.loading;
protected readonly error = this.facade.error;
@@ -35,12 +34,12 @@ export class UserAvatarFormComponent {
let message = '';
effect(() => {
if (!this.loading().isLoading) {
if (!this.loading().isLoading && this.onSubmitted()) {
switch (this.loading().action) {
case ActionType.UPDATE:
this.authService.updateUser();
message = `Votre photo de profile a bien été modifier !`;
this.customToast(ActionType.UPDATE, message);
this.onSubmitted.set(false);
break;
}
}
@@ -55,6 +54,7 @@ export class UserAvatarFormComponent {
this.facade.update(this.user?.id!, formData as Partial<User>);
this.onFormSubmitted.emit('');
this.onSubmitted.set(true);
}
}
@@ -98,4 +98,6 @@ export class UserAvatarFormComponent {
}
);
}
protected readonly ActionType = ActionType;
}

View File

@@ -1,8 +1,15 @@
<form
@if (loading().action === ActionType.UPDATE && loading().isLoading && onSubmitted()) {
<app-loading message="Mise à jour encours..." />
} @else {
<ng-container *ngTemplateOutlet="forms" />
}
<ng-template #forms>
<form
[formGroup]="userForm"
(ngSubmit)="onUserFormSubmit()"
class="w-full space-y-6 animate-fade-in"
>
>
<!-- Titre -->
<div class="flex items-center gap-3 pb-4 border-b border-gray-200 dark:border-gray-700">
<div
@@ -104,4 +111,5 @@
Modifier mon identité
</span>
</button>
</form>
</form>
</ng-template>

View File

@@ -1,4 +1,4 @@
import { Component, effect, inject, Input, OnInit, output } from '@angular/core';
import { Component, effect, inject, Input, OnInit, output, signal } from '@angular/core';
import {
FormBuilder,
FormControl,
@@ -7,16 +7,17 @@ import {
Validators,
} from '@angular/forms';
import { User } from '@app/domain/users/user.model';
import { AuthService } from '@app/core/services/authentication/auth.service';
import { UntilDestroy } from '@ngneat/until-destroy';
import { ToastrService } from 'ngx-toastr';
import { UserFacade } from '@app/ui/users/user.facade';
import { ActionType } from '@app/domain/action-type.util';
import { LoadingComponent } from '@app/shared/components/loading/loading.component';
import { NgTemplateOutlet } from '@angular/common';
@Component({
selector: 'app-user-form',
standalone: true,
imports: [ReactiveFormsModule],
imports: [ReactiveFormsModule, LoadingComponent, NgTemplateOutlet],
templateUrl: './user-form.component.html',
styleUrl: './user-form.component.scss',
})
@@ -24,28 +25,28 @@ import { ActionType } from '@app/domain/action-type.util';
export class UserFormComponent implements OnInit {
private readonly toastrService = inject(ToastrService);
private readonly facade = inject(UserFacade);
protected readonly ActionType = ActionType;
@Input({ required: true }) user: User | undefined = undefined;
onFormSubmitted = output<any>();
private authService = inject(AuthService);
private fb = inject(FormBuilder);
protected userForm!: FormGroup;
protected readonly loading = this.facade.loading;
protected readonly error = this.facade.error;
protected onSubmitted = signal<boolean>(false);
constructor() {
let message = '';
effect(() => {
if (!this.loading().isLoading) {
if (!this.loading().isLoading && this.onSubmitted()) {
switch (this.loading().action) {
case ActionType.UPDATE:
this.authService.updateUser();
message = `Vos informations personnelles ont bien été modifier !`;
this.customToast(ActionType.UPDATE, message);
this.onSubmitted.set(false);
break;
}
}
@@ -74,6 +75,7 @@ export class UserFormComponent implements OnInit {
this.facade.update(this.user?.id!, data);
this.onFormSubmitted.emit(data);
this.onSubmitted.set(true);
}
private customToast(action: ActionType, message: string): void {

View File

@@ -0,0 +1,16 @@
import { UserRepository } from '@app/domain/users/user.repository';
import { Observable, of } from 'rxjs';
import { User } from '@app/domain/users/user.model';
import { fakeUsers } from '@app/testing/user.mock';
export class FakeUserRepository implements UserRepository {
getUserById(userId: string): Observable<User> {
const user = fakeUsers.find((u) => u.id === userId) ?? ({} as User);
return of(user);
}
update(userId: string, user: Partial<User> | User): Observable<User> {
const existingUser = fakeUsers.find((u) => u.id === userId) ?? fakeUsers[0];
return of({ ...existingUser, ...user });
}
}

View File

@@ -0,0 +1,58 @@
import { FakeUserRepository } from '@app/testing/domain/users/fake-user.repository';
import { PbUserRepository } from '@app/infrastructure/users/pb-user.repository';
import { fakeUsers } from '@app/testing/user.mock';
import PocketBase from 'pocketbase';
jest.mock('pocketbase');
describe('UserRepository', () => {
let userRepo: FakeUserRepository;
let mockCollection: any;
let mockPocketBase: any;
beforeEach(() => {
mockPocketBase = {
collection: jest.fn().mockReturnValue({
update: jest.fn(),
getOne: jest.fn(),
}),
};
// 👇 On remplace la classe importée par notre version mockée
(PocketBase as jest.Mock).mockImplementation(() => mockPocketBase);
// on récupère une "collection" simulée
mockCollection = mockPocketBase.collection('users');
// on instancie le repository à tester
userRepo = new PbUserRepository();
});
afterEach(() => {
jest.clearAllMocks();
});
// TODO: revoir la logique de test
it('devrait appeler pb.collection("users").getFirstListItem() ', () => {
const userId = 'user_001';
mockCollection.getOne.mockResolvedValue(fakeUsers.find((u) => u.id === userId));
userRepo.getUserById(userId).subscribe((result) => {
expect(mockCollection.getOne).toHaveBeenCalledWith(userId);
expect(result).toEqual(fakeUsers[0]);
});
});
it('devrait appeler pb.collection("users").update() avec ID et data ', () => {
const userId = 'user_001';
const data = { email: 'bar@foo.com' };
const updatedUser = { ...fakeUsers[0], email: data.email };
mockCollection.update.mockResolvedValue(updatedUser);
userRepo.update(userId, data).subscribe((result) => {
expect(mockCollection.update).toHaveBeenCalledWith(userId, data);
expect(result.email).toBe(data.email);
});
});
});

View File

@@ -0,0 +1,37 @@
import { UserFacade } from '@app/ui/users/user.facade';
import { TestBed } from '@angular/core/testing';
import { USER_REPOSITORY_TOKEN } from '@app/infrastructure/users/user-repository.token';
import { FakeUserRepository } from '@app/testing/domain/users/fake-user.repository';
describe('UserFacade', () => {
let facade: UserFacade;
beforeEach(() => {
TestBed.configureTestingModule({
providers: [UserFacade, { provide: USER_REPOSITORY_TOKEN, useClass: FakeUserRepository }],
});
facade = TestBed.inject(UserFacade);
});
it('devrait charger un user par son id ', () => {
const userId = 'user_001';
facade.loadOne(userId);
setTimeout(() => {
const user = facade.user();
expect(user?.id).toBe(userId);
expect(facade.error().hasError).toBe(false);
}, 0);
});
it('devrait mettre a jour un user existant ', () => {
const userId = 'user_001';
const newData = { email: 'bar@foo.com' };
facade.update(userId, newData);
setTimeout(() => {
const updatedUser = facade.user();
expect(updatedUser?.email).toBe(newData.email);
expect(facade.error().hasError).toBe(false);
}, 0);
});
});

View File

@@ -0,0 +1,19 @@
import { FakeUserRepository } from '@app/testing/domain/users/fake-user.repository';
import { GetUserUseCase } from '@app/usecase/users/get-user.usecase';
describe('GetUserUseCase', () => {
let useCase: GetUserUseCase;
let repo: FakeUserRepository;
beforeEach(() => {
repo = new FakeUserRepository();
useCase = new GetUserUseCase(repo);
});
it("devrait retourné un user en fonction de l'id ", () => {
const userId = 'user_001';
useCase.execute(userId).subscribe((user) => {
expect(user.id).toBe(userId);
});
});
});

View File

@@ -0,0 +1,22 @@
import { UpdateUserUseCase } from '@app/usecase/users/update-user.usecase';
import { FakeUserRepository } from '@app/testing/domain/users/fake-user.repository';
describe('UpdateUserUsecase', () => {
let useCase: UpdateUserUseCase;
let repo: FakeUserRepository;
beforeEach(() => {
repo = new FakeUserRepository();
useCase = new UpdateUserUseCase(repo);
});
it("devrait modifier un user en fonction de l'id ", () => {
const userId = 'user_001';
const newData = { email: 'bar@foo.com' };
useCase.execute(userId, newData).subscribe((updated) => {
expect(updated.email).toBe(newData.email);
expect(updated.updated).toBeDefined();
});
});
});

View File

@@ -0,0 +1,15 @@
import { User } from '@app/domain/users/user.model';
export const fakeUsers: User[] = [
{
id: 'user_001',
created: '2025-01-01T10:00:00Z',
updated: '2025-01-05T14:00:00Z',
name: 'foo',
avatar: 'foo.png',
email: 'foo@bar.com',
username: 'foo',
emailVisibility: false,
verified: false,
},
];

View File

@@ -6,6 +6,8 @@ import { LoaderAction } from '@app/domain/loader-action.util';
import { ActionType } from '@app/domain/action-type.util';
import { ErrorResponse } from '@app/domain/error-response.util';
import { UserViewModel } from '@app/ui/users/user.presenter.model';
import { UserPresenter } from '@app/ui/users/user.presenter';
import { AuthService } from '@app/core/services/authentication/auth.service';
@Injectable({
providedIn: 'root',
@@ -13,6 +15,8 @@ import { UserViewModel } from '@app/ui/users/user.presenter.model';
export class UserFacade {
private readonly userRepository = inject(USER_REPOSITORY_TOKEN);
private readonly authService = inject(AuthService);
private readonly getUseCase = new GetUserUseCase(this.userRepository);
private readonly updateUseCase = new UpdateUserUseCase(this.userRepository);
@@ -25,11 +29,13 @@ export class UserFacade {
message: null,
});
private readonly userPresenter = new UserPresenter();
loadOne(userId: string) {
this.handleError(ActionType.READ, false, null, true);
this.getUseCase.execute(userId).subscribe({
next: (user) => {
this.user.set(user);
this.user.set(this.userPresenter.toViewModel(user));
this.handleError(ActionType.READ, false, null, false);
},
error: (err) => {
@@ -42,7 +48,8 @@ export class UserFacade {
this.handleError(ActionType.UPDATE, false, null, true);
this.updateUseCase.execute(userId, user).subscribe({
next: (user) => {
this.user.set(user);
this.user.set(this.userPresenter.toViewModel(user));
this.authService.updateUser();
this.handleError(ActionType.UPDATE, false, null, false);
},
error: (err) => {