project et profile => clean archi et test vert

This commit is contained in:
styve Lioumba
2025-10-24 16:18:27 +02:00
parent 4c1787d784
commit 3654709250
48 changed files with 762 additions and 656 deletions

View File

@@ -3,7 +3,7 @@ import { Router } from '@angular/router';
import { detailResolver } from './detail.resolver';
import { User } from '@app/shared/models/user';
import { Profile } from '@app/shared/models/profile';
import { Profile } from '@app/domain/profiles/profile.model';
describe('detailResolver', () => {
let mockRoute: Partial<Router>;

View File

@@ -1,7 +1,7 @@
import { ResolveFn, Router } from '@angular/router';
import { inject } from '@angular/core';
import { User } from '@app/shared/models/user';
import { Profile } from '@app/shared/models/profile';
import { Profile } from '@app/domain/profiles/profile.model';
export const detailResolver: ResolveFn<{ user: User; profile: Profile }> = (route, state) => {
const paramValue = route.params['name'];

View File

@@ -1,5 +1,5 @@
import { ResolveFn } from '@angular/router';
import { Profile } from '@app/shared/models/profile';
import { Profile } from '@app/domain/profiles/profile.model';
export const listResolver: ResolveFn<Profile[]> = (route, state) => {
const queryValue = route.queryParams['search'];

View File

@@ -1,27 +0,0 @@
import { TestBed } from '@angular/core/testing';
import { ProfileService } from './profile.service';
import { Router } from '@angular/router';
describe('ProfileService', () => {
let service: ProfileService;
const routerSpy = {
navigate: jest.fn(),
navigateByUrl: jest.fn(),
};
beforeEach(() => {
TestBed.configureTestingModule({
providers: [
{ provide: Router, useValue: routerSpy }, // <<— spy: neutralise la navigation
],
imports: [],
});
service = TestBed.inject(ProfileService);
});
it('should be created', () => {
expect(service).toBeTruthy();
});
});

View File

@@ -1,35 +0,0 @@
import { Injectable } from '@angular/core';
import PocketBase from 'pocketbase';
import { environment } from '@env/environment';
import { Profile } from '@app/shared/models/profile';
import { from } from 'rxjs';
import { ProfileDto } from '@app/shared/models/profile-dto';
@Injectable({
providedIn: 'root',
})
export class ProfileService {
createProfile(profileDto: ProfileDto) {
const pb = new PocketBase(environment.baseUrl);
return from(pb.collection('profiles').create(profileDto));
}
get profiles() {
const pb = new PocketBase(environment.baseUrl);
return from(
pb.collection('profiles').getFullList<Profile>({
sort: 'profession',
})
);
}
getProfileByUserId(userId: string) {
const pb = new PocketBase(environment.baseUrl);
return from(pb.collection<Profile>('profiles').getFirstListItem(`utilisateur="${userId}"`));
}
updateProfile(id: string, data: Profile | any) {
const pb = new PocketBase(environment.baseUrl);
return from(pb.collection('profiles').update<Profile>(id, data));
}
}

View File

@@ -3,7 +3,7 @@ import { Observable } from 'rxjs';
export interface ProfileRepository {
list(params?: { search?: string; page?: number; pageSize?: number }): Observable<Profile[]>;
getByUserId(userId: string): Observable<Profile | null>;
getByUserId(userId: string): Observable<Profile>;
create(profile: Profile): Observable<Profile>;
update(id: string, profile: Partial<Profile>): Observable<Profile>;
update(profileId: string, profile: Partial<Profile>): Observable<Profile>;
}

View File

@@ -1,6 +1,6 @@
import { Observable } from 'rxjs';
import { Project } from '@app/shared/models/project';
import { CreateProjectDto } from '@app/domain/projects/dto/create-project.dto';
import { Project } from '@app/domain/projects/project.model';
export interface ProjectRepository {
create(projectDto: CreateProjectDto): Observable<Project>;

View File

@@ -14,7 +14,7 @@ export class PbProfileRepository implements ProfileRepository {
return from(this.pb.collection('profiles').getFullList<Profile>({ sort: 'profession' }));
}
getByUserId(userId: string): Observable<Profile | null> {
getByUserId(userId: string): Observable<Profile> {
return from(
this.pb.collection('profiles').getFirstListItem<Profile>(`utilisateur="${userId}"`)
);

View File

@@ -1,10 +1,10 @@
import { Injectable } from '@angular/core';
import { environment } from '@env/environment';
import { ProjectRepository } from '@app/domain/projects/project.repository';
import { Project } from '@app/shared/models/project';
import { from, Observable } from 'rxjs';
import PocketBase from 'pocketbase';
import { CreateProjectDto } from '@app/domain/projects/dto/create-project.dto';
import { Project } from '@app/domain/projects/project.model';
@Injectable({
providedIn: 'root',

View File

@@ -1,6 +1,7 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { HomeComponent } from './home.component';
import { provideRouter } from '@angular/router';
describe('HomeComponent', () => {
let component: HomeComponent;
@@ -9,6 +10,7 @@ describe('HomeComponent', () => {
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [HomeComponent],
providers: [provideRouter([])],
}).compileComponents();
fixture = TestBed.createComponent(HomeComponent);

View File

@@ -19,7 +19,7 @@
</svg>
</a>
@if (profile.estVerifier) {
@if (profile().estVerifier) {
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
@@ -100,8 +100,8 @@
</h2>
}
<div class="w-12 h-1 bg-indigo-500 rounded mt-2 mb-4"></div>
@if (profile.bio) {
<p class="text-base dark:text-white w-full">{{ profile.bio }}</p>
@if (profile().bio) {
<p class="text-base dark:text-white w-full">{{ profile().bio }}</p>
} @else {
<p class="text-base dark:text-white w-full">
Je suis sur la plateforme Trouve Ton Profile pour partager mon expertise et mes
@@ -110,17 +110,17 @@
</p>
}
@if (profile.secteur) {
@if (profile().secteur) {
<div class="space-y-2 flex flex-col my-4">
<p class="text-base dark:text-white">Secteur</p>
<app-chips [sectorId]="profile.secteur" />
<app-chips [sectorId]="profile().secteur" />
</div>
}
@if (profile.reseaux) {
@if (profile().reseaux) {
<div class="space-y-2 flex flex-col my-4">
<p class="text-base dark:text-white">Réseaux</p>
<app-reseaux [reseaux]="profile.reseaux" />
<app-reseaux [reseaux]="profile().reseaux" />
</div>
}
</div>
@@ -240,23 +240,23 @@
<div id="homeContent" class="tab-content max-w-2xl block mt-8">
@switch (menu().toLowerCase()) {
@case ('home'.toLowerCase()) {
<app-my-home-profile [profile]="profile" />
<app-my-home-profile [profile]="profile()" />
}
@case ('projects'.toLowerCase()) {
<app-my-profile-project-list
[projectIds]="profile.projets"
[projectIds]="profile().projets"
[userId]="user().id"
/>
<router-outlet />
}
@case ('update'.toLowerCase()) {
<app-my-profile-update-form [profile]="profile" />
<app-my-profile-update-form [profile]="profile()" />
}
@case ('cv'.toLowerCase()) {
<app-pdf-viewer [profile]="profile" />
<app-pdf-viewer [profile]="profile()" />
}
@default {
<app-my-home-profile [profile]="profile" />
<app-my-home-profile [profile]="profile()" />
}
}
</div>

View File

@@ -2,15 +2,30 @@ import { ComponentFixture, TestBed } from '@angular/core/testing';
import { MyProfileComponent } from './my-profile.component';
import { provideRouter } from '@angular/router';
import { ProfileRepository } from '@app/domain/profiles/profile.repository';
import { PROFILE_REPOSITORY_TOKEN } from '@app/infrastructure/profiles/profile-repository.token';
import { of } from 'rxjs';
import { Profile } from '@app/domain/profiles/profile.model';
describe('MyProfileComponent', () => {
let component: MyProfileComponent;
let fixture: ComponentFixture<MyProfileComponent>;
let mockProfileRepo: ProfileRepository;
beforeEach(async () => {
mockProfileRepo = {
create: jest.fn(),
list: jest.fn(),
update: jest.fn(),
getByUserId: jest.fn().mockReturnValue(of({} as Profile)),
};
await TestBed.configureTestingModule({
imports: [MyProfileComponent],
providers: [provideRouter([])],
providers: [
provideRouter([]),
{ provide: PROFILE_REPOSITORY_TOKEN, useValue: mockProfileRepo },
],
}).compileComponents();
fixture = TestBed.createComponent(MyProfileComponent);

View File

@@ -1,7 +1,7 @@
import { Component, computed, inject, OnInit, signal } from '@angular/core';
import { ActivatedRoute, RouterLink, RouterOutlet } from '@angular/router';
import { User } from '@app/shared/models/user';
import { AsyncPipe, JsonPipe, Location, NgClass, UpperCasePipe } from '@angular/common';
import { Location, NgClass } from '@angular/common';
import { UntilDestroy } from '@ngneat/until-destroy';
import { SafeUrl } from '@angular/platform-browser';
import { QRCodeModule } from 'angularx-qrcode';
@@ -12,22 +12,18 @@ import { UpdateUserComponent } from '@app/shared/features/update-user/update-use
import { MyProfileProjectListComponent } from '@app/shared/components/my-profile-project-list/my-profile-project-list.component';
import { MyHomeProfileComponent } from '@app/shared/components/my-home-profile/my-home-profile.component';
import { MyProfileUpdateFormComponent } from '@app/shared/components/my-profile-update-form/my-profile-update-form.component';
import { ProfileService } from '@app/core/services/profile/profile.service';
import { Profile } from '@app/shared/models/profile';
import { PdfViewerComponent } from '@app/shared/features/pdf-viewer/pdf-viewer.component';
import { ProfileFacade } from '@app/ui/profiles/profile.facade';
@Component({
selector: 'app-my-profile',
standalone: true,
imports: [
JsonPipe,
RouterLink,
AsyncPipe,
QRCodeModule,
ChipsComponent,
ReseauxComponent,
UpdateUserComponent,
UpperCasePipe,
MyProfileProjectListComponent,
RouterOutlet,
MyHomeProfileComponent,
@@ -40,8 +36,6 @@ import { PdfViewerComponent } from '@app/shared/features/pdf-viewer/pdf-viewer.c
})
@UntilDestroy()
export class MyProfileComponent implements OnInit {
private profileService = inject(ProfileService);
protected readonly environment = environment;
protected menu = signal<string>('home');
@@ -58,8 +52,6 @@ export class MyProfileComponent implements OnInit {
return {} as User;
});
protected profile: Profile = {} as Profile;
protected isEditMode = signal<boolean>(false);
onChangeURL(url: SafeUrl) {
@@ -70,13 +62,13 @@ export class MyProfileComponent implements OnInit {
this.isEditMode.set(!$event);
}
private readonly profileFacade = new ProfileFacade();
protected profile = this.profileFacade.profile;
protected readonly loading = this.profileFacade.loading;
protected readonly error = this.profileFacade.error;
ngOnInit(): void {
this.myProfileQrCode = `${this.myProfileQrCode}/profiles/${this.user().id}`;
this.profileService.getProfileByUserId(this.user().id).subscribe({
next: (value: Profile) => {
this.profile = value;
},
});
this.profileFacade.loadOne(this.user().id);
}
}

View File

@@ -3,12 +3,12 @@ import { ActivatedRoute, RouterLink } from '@angular/router';
import { QRCodeModule } from 'angularx-qrcode';
import { UpperCasePipe } from '@angular/common';
import { User } from '@app/shared/models/user';
import { Profile } from '@app/shared/models/profile';
import { ChipsComponent } from '@app/shared/components/chips/chips.component';
import { ReseauxComponent } from '@app/shared/components/reseaux/reseaux.component';
import { UntilDestroy } from '@ngneat/until-destroy';
import { ProjectListComponent } from '@app/shared/components/project-list/project-list.component';
import { environment } from '@env/environment';
import { Profile } from '@app/domain/profiles/profile.model';
@Component({
selector: 'app-profile-detail',

View File

@@ -1,16 +1,16 @@
import { Component, Input } from '@angular/core';
import { AsyncPipe, JsonPipe, UpperCasePipe } from '@angular/common';
import { Profile } from '@app/shared/models/profile';
import { UpperCasePipe } from '@angular/common';
import { UntilDestroy } from '@ngneat/until-destroy';
import { ProfileViewModel } from '@app/ui/profiles/profile.presenter.model';
@Component({
selector: 'app-my-home-profile',
standalone: true,
imports: [UpperCasePipe, AsyncPipe, JsonPipe],
imports: [UpperCasePipe],
templateUrl: './my-home-profile.component.html',
styleUrl: './my-home-profile.component.scss',
})
@UntilDestroy()
export class MyHomeProfileComponent {
@Input({ required: true }) profile: Profile | undefined = undefined;
@Input({ required: true }) profile: ProfileViewModel | undefined = undefined;
}

View File

@@ -2,19 +2,22 @@ import { ComponentFixture, TestBed } from '@angular/core/testing';
import { MyProfileUpdateCvFormComponent } from './my-profile-update-cv-form.component';
import { ToastrService } from 'ngx-toastr';
import { ProfileService } from '@app/core/services/profile/profile.service';
import { AuthService } from '@app/core/services/authentication/auth.service';
import { signal } from '@angular/core';
import { Auth } from '@app/shared/models/auth';
import { provideRouter } from '@angular/router';
import { ProfileRepository } from '@app/domain/profiles/profile.repository';
import { of } from 'rxjs';
import { Profile } from '@app/domain/profiles/profile.model';
import { PROFILE_REPOSITORY_TOKEN } from '@app/infrastructure/profiles/profile-repository.token';
describe('MyProfileUpdateCvFormComponent', () => {
let component: MyProfileUpdateCvFormComponent;
let fixture: ComponentFixture<MyProfileUpdateCvFormComponent>;
let mockToastrService: Partial<ToastrService>;
let mockProfileService: Partial<ProfileService>;
let mockAuthService: Partial<AuthService>;
let mockProfileRepo: ProfileRepository;
beforeEach(async () => {
mockToastrService = {
@@ -23,10 +26,11 @@ describe('MyProfileUpdateCvFormComponent', () => {
warning: jest.fn(),
};
mockProfileService = {
updateProfile: jest.fn().mockReturnValue({
subscribe: jest.fn(),
}),
mockProfileRepo = {
create: jest.fn(),
list: jest.fn(),
update: jest.fn().mockReturnValue(of({} as Profile)),
getByUserId: jest.fn().mockReturnValue(of({} as Profile)),
};
mockAuthService = {
@@ -38,8 +42,8 @@ describe('MyProfileUpdateCvFormComponent', () => {
imports: [MyProfileUpdateCvFormComponent],
providers: [
provideRouter([]),
{ provide: PROFILE_REPOSITORY_TOKEN, useValue: mockProfileRepo },
{ provide: ToastrService, useValue: mockToastrService },
{ provide: ProfileService, useValue: mockProfileService },
{ provide: AuthService, useValue: mockAuthService },
],
}).compileComponents();
@@ -47,6 +51,8 @@ describe('MyProfileUpdateCvFormComponent', () => {
fixture = TestBed.createComponent(MyProfileUpdateCvFormComponent);
component = fixture.componentInstance;
fixture.detectChanges();
await fixture.whenStable();
});
it('should create', () => {

View File

@@ -1,9 +1,11 @@
import { Component, inject, Input, output } from '@angular/core';
import { Component, effect, inject, Input } from '@angular/core';
import { AuthService } from '@app/core/services/authentication/auth.service';
import { Profile } from '@app/shared/models/profile';
import { ProfileService } from '@app/core/services/profile/profile.service';
import { NgClass } from '@angular/common';
import { ToastrService } from 'ngx-toastr';
import { ProfileViewModel } from '@app/ui/profiles/profile.presenter.model';
import { ProfileFacade } from '@app/ui/profiles/profile.facade';
import { ActionType } from '@app/domain/action-type.util';
import { Profile } from '@app/domain/profiles/profile.model';
@Component({
selector: 'app-my-profile-update-cv-form',
@@ -13,29 +15,40 @@ import { ToastrService } from 'ngx-toastr';
styleUrl: './my-profile-update-cv-form.component.scss',
})
export class MyProfileUpdateCvFormComponent {
@Input({ required: true }) profile: Profile | undefined = undefined;
@Input({ required: true }) profile: ProfileViewModel | undefined = undefined;
private readonly profileService = inject(ProfileService);
private readonly toastrService = inject(ToastrService);
private readonly authService = inject(AuthService);
file: File | null = null; // Variable to store file
private readonly profileFacade = new ProfileFacade();
protected readonly loading = this.profileFacade.loading;
protected readonly error = this.profileFacade.error;
constructor() {
effect(() => {
switch (this.loading().action) {
case ActionType.UPDATE:
if (!this.loading() && !this.error().hasError) {
this.authService.updateUser();
this.toastrService.success(` Votre CV a bien été modifier !`, `Mise à jour`, {
closeButton: true,
progressAnimation: 'decreasing',
progressBar: true,
});
}
}
});
}
onSubmit() {
if (this.file != null) {
const formData = new FormData();
formData.append('cv', this.file); // "avatar" est le nom du champ dans PocketBase
this.profileService.updateProfile(this.profile?.id!, formData).subscribe((value) => {
this.authService.updateUser();
this.toastrService.success(` Votre CV a bien été modifier !`, `Mise à jour`, {
closeButton: true,
progressAnimation: 'decreasing',
progressBar: true,
});
});
this.profileFacade.update(this.profile?.id!, formData as Partial<Profile>);
}
}

View File

@@ -4,12 +4,17 @@ import { MyProfileUpdateFormComponent } from './my-profile-update-form.component
import { provideRouter } from '@angular/router';
import { ToastrService } from 'ngx-toastr';
import { FormBuilder } from '@angular/forms';
import { PROFILE_REPOSITORY_TOKEN } from '@app/infrastructure/profiles/profile-repository.token';
import { ProfileRepository } from '@app/domain/profiles/profile.repository';
import { of } from 'rxjs';
import { Profile } from '@app/domain/profiles/profile.model';
describe('MyProfileUpdateFormComponent', () => {
let component: MyProfileUpdateFormComponent;
let fixture: ComponentFixture<MyProfileUpdateFormComponent>;
let mockToastrService: Partial<ToastrService>;
let mockProfileRepo: ProfileRepository;
const mockProfileData = {
profession: '',
@@ -26,18 +31,28 @@ describe('MyProfileUpdateFormComponent', () => {
error: jest.fn(),
};
mockProfileRepo = {
create: jest.fn(),
list: jest.fn(),
update: jest.fn().mockReturnValue(of({} as Profile)),
getByUserId: jest.fn().mockReturnValue(of({} as Profile)),
};
await TestBed.configureTestingModule({
imports: [MyProfileUpdateFormComponent],
providers: [
FormBuilder,
provideRouter([]),
{ provide: ToastrService, useValue: mockToastrService },
{ provide: PROFILE_REPOSITORY_TOKEN, useValue: mockProfileRepo },
],
}).compileComponents();
fixture = TestBed.createComponent(MyProfileUpdateFormComponent);
component = fixture.componentInstance;
fixture.detectChanges();
await fixture.whenStable();
});
it('should create', () => {

View File

@@ -1,4 +1,4 @@
import { Component, inject, Input, OnInit, signal } from '@angular/core';
import { Component, effect, inject, Input, OnInit, signal } from '@angular/core';
import {
FormBuilder,
FormControl,
@@ -7,14 +7,16 @@ import {
Validators,
} from '@angular/forms';
import { UntilDestroy } from '@ngneat/until-destroy';
import { Profile } from '@app/shared/models/profile';
import { NgClass } from '@angular/common';
import { SectorService } from '@app/core/services/sector/sector.service';
import { Sector } from '@app/shared/models/sector';
import { ProfileService } from '@app/core/services/profile/profile.service';
import { AuthService } from '@app/core/services/authentication/auth.service';
import { MyProfileUpdateCvFormComponent } from '@app/shared/components/my-profile-update-cv-form/my-profile-update-cv-form.component';
import { ToastrService } from 'ngx-toastr';
import { ProfileViewModel } from '@app/ui/profiles/profile.presenter.model';
import { ProfileFacade } from '@app/ui/profiles/profile.facade';
import { ActionType } from '@app/domain/action-type.util';
import { Profile } from '@app/domain/profiles/profile.model';
@Component({
selector: 'app-my-profile-update-form',
@@ -27,14 +29,38 @@ import { ToastrService } from 'ngx-toastr';
export class MyProfileUpdateFormComponent implements OnInit {
private readonly toastrService = inject(ToastrService);
@Input({ required: true }) profile: Profile = {} as Profile;
@Input({ required: true }) profile: ProfileViewModel = {} as ProfileViewModel;
private readonly formBuilder = inject(FormBuilder);
protected readonly sectorService = inject(SectorService);
protected readonly profileService = inject(ProfileService);
protected readonly authService = inject(AuthService);
profileForm!: FormGroup;
protected sectors = signal<Sector[]>([]);
private readonly profileFacade = new ProfileFacade();
protected readonly loading = this.profileFacade.loading;
protected readonly error = this.profileFacade.error;
constructor() {
effect(() => {
switch (this.loading().action) {
case ActionType.UPDATE:
if (!this.loading() && !this.error().hasError) {
this.authService.updateUser();
this.toastrService.success(
` Vos informations personnelles ont bien été modifier !`,
`Mise à jour`,
{
closeButton: true,
progressAnimation: 'decreasing',
progressBar: true,
}
);
}
}
});
}
ngOnInit(): void {
this.profileForm = this.formBuilder.group({
profession: new FormControl(
@@ -90,26 +116,6 @@ export class MyProfileUpdateFormComponent implements OnInit {
reseaux: this.profileForm.getRawValue().reseaux,
} as Profile;
this.profileService.updateProfile(this.profile.id, data).subscribe({
next: (value) => {
this.authService.updateUser();
this.toastrService.success(
` Vos informations personnelles ont bien été modifier !`,
`Mise à jour`,
{
closeButton: true,
progressAnimation: 'decreasing',
progressBar: true,
}
);
},
error: (error) => {
this.toastrService.error(
'Une erreur est survenue lors de la mise à jour de votre profil',
'Erreur'
);
},
});
this.profileFacade.update(this.profile.id, data);
}
}

View File

@@ -64,9 +64,9 @@ export class MyProfileUpdateProjectFormComponent implements OnInit, OnChanges {
if (this.project() !== undefined) {
this.projectForm.setValue({
nom: this.project()!.nom,
description: this.project()!.description,
lien: this.project()!.lien,
nom: this.project().nom ?? '',
description: this.project().description ?? '',
lien: this.project().lien ?? '',
});
}
}

View File

@@ -1,16 +1,16 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { PdfViewerComponent } from './pdf-viewer.component';
import { Profile } from '@app/shared/models/profile';
import { ProfileViewModel } from '@app/ui/profiles/profile.presenter.model';
describe('PdfViewerComponent', () => {
let component: PdfViewerComponent;
let fixture: ComponentFixture<PdfViewerComponent>;
const mockProfile: Profile = {
const mockProfile: ProfileViewModel = {
id: '123',
cv: 'cvfilename.pdf',
} as Profile;
} as ProfileViewModel;
beforeEach(async () => {
await TestBed.configureTestingModule({

View File

@@ -1,7 +1,7 @@
import { Component, computed, Input } from '@angular/core';
import { PdfViewerModule } from 'ng2-pdf-viewer';
import { Profile } from '@app/shared/models/profile';
import { environment } from '@env/environment';
import { ProfileViewModel } from '@app/ui/profiles/profile.presenter.model';
@Component({
selector: 'app-pdf-viewer',
@@ -11,7 +11,7 @@ import { environment } from '@env/environment';
styleUrl: './pdf-viewer.component.scss',
})
export class PdfViewerComponent {
@Input({ required: true }) profile: Profile | undefined = undefined;
@Input({ required: true }) profile: ProfileViewModel | undefined = undefined;
protected readonly environment = environment;
protected readonly cv_link = computed(() => {
return `${environment.baseUrl}/api/files/profiles/${this.profile!.id}/${this.profile!.cv}`;

View File

@@ -3,8 +3,9 @@ import { ComponentFixture, TestBed } from '@angular/core/testing';
import { RegisterComponent } from './register.component';
import { ToastrService } from 'ngx-toastr';
import { AuthService } from '@app/core/services/authentication/auth.service';
import { ProfileService } from '@app/core/services/profile/profile.service';
import { provideRouter } from '@angular/router';
import { ProfileRepository } from '@app/domain/profiles/profile.repository';
import { PROFILE_REPOSITORY_TOKEN } from '@app/infrastructure/profiles/profile-repository.token';
describe('RegisterComponent', () => {
let component: RegisterComponent;
@@ -12,19 +13,22 @@ describe('RegisterComponent', () => {
let mockToastrService: Partial<ToastrService>;
let mockAuthService: Partial<AuthService>;
let mockProfileService: Partial<ProfileService>;
let mockProfileRepo: ProfileRepository;
beforeEach(async () => {
mockProfileRepo = {
create: jest.fn(),
list: jest.fn(),
update: jest.fn(),
getByUserId: jest.fn(),
};
mockToastrService = {
success: jest.fn(),
error: jest.fn(),
warning: jest.fn(),
};
mockProfileService = {
createProfile: jest.fn().mockResolvedValue(true),
};
mockAuthService = {};
await TestBed.configureTestingModule({
@@ -33,7 +37,7 @@ describe('RegisterComponent', () => {
provideRouter([]),
{ provide: ToastrService, useValue: mockToastrService },
{ provide: AuthService, useValue: mockAuthService },
{ provide: ProfileService, useValue: mockProfileService },
{ provide: PROFILE_REPOSITORY_TOKEN, useValue: mockProfileRepo },
],
}).compileComponents();

View File

@@ -1,13 +1,14 @@
import { ChangeDetectionStrategy, Component, inject, output, signal } from '@angular/core';
import { ChangeDetectionStrategy, Component, effect, inject, output, signal } from '@angular/core';
import { Router, RouterLink } from '@angular/router';
import { FormBuilder, FormControl, ReactiveFormsModule, Validators } from '@angular/forms';
import { AuthService } from '@app/core/services/authentication/auth.service';
import { RegisterDto } from '@app/shared/models/register-dto';
import { UntilDestroy } from '@ngneat/until-destroy';
import { ToastrService } from 'ngx-toastr';
import { ProfileService } from '@app/core/services/profile/profile.service';
import { ProfileDto } from '@app/shared/models/profile-dto';
import { ProgressBarModule } from 'primeng/progressbar';
import { ProfileFacade } from '@app/ui/profiles/profile.facade';
import { ActionType } from '@app/domain/action-type.util';
import { ProfileDTO } from '@app/domain/profiles/dto/create-profile.dto';
@Component({
selector: 'app-register',
@@ -20,7 +21,6 @@ import { ProgressBarModule } from 'primeng/progressbar';
@UntilDestroy()
export class RegisterComponent {
private readonly authService = inject(AuthService);
private readonly profileService = inject(ProfileService);
private readonly toastrService = inject(ToastrService);
private readonly formBuilder = inject(FormBuilder);
@@ -34,6 +34,26 @@ export class RegisterComponent {
formSubmitted = output<any>();
protected isLoading = signal<boolean>(false);
private readonly profileFacade = new ProfileFacade();
protected readonly loading = this.profileFacade.loading;
protected readonly error = this.profileFacade.error;
constructor() {
effect(() => {
switch (this.loading().action) {
case ActionType.CREATE:
if (!this.loading().isLoading) {
if (!this.error().hasError) {
this.router.navigate(['/auth']).then(() => {
this.sendVerificationEmail();
});
}
}
break;
}
});
}
onSubmit() {
if (this.registerForm.invalid) {
this.isLoading.set(false);
@@ -63,13 +83,13 @@ export class RegisterComponent {
register(registerDto: RegisterDto) {
this.authService.register(registerDto).then((res) => {
if (res) {
this.createProfile(res.id, registerDto.email);
this.createProfile(res.id);
}
});
}
createProfile(userId: string, email: string) {
const profileDto: ProfileDto = {
createProfile(userId: string) {
const profileDto: ProfileDTO = {
profession: 'Profession non renseignée',
utilisateur: userId,
reseaux: {
@@ -83,28 +103,11 @@ export class RegisterComponent {
},
};
this.profileService.createProfile(profileDto).subscribe({
next: (profile) => {
if (profile)
this.router.navigate(['/auth']).then(() => {
this.sendVerificationEmail(email);
});
},
error: (err) => {
this.toastrService.error(
`Une erreur est survenue lors de la création de votre profil.`,
`Erreur`,
{
closeButton: true,
progressAnimation: 'decreasing',
progressBar: true,
}
);
},
});
this.profileFacade.create(profileDto);
}
sendVerificationEmail(email: string) {
sendVerificationEmail() {
const email = this.registerForm.getRawValue().email!;
this.authService.verifyEmail(email).then((isVerified: boolean) => {
this.isLoading.set(false);
this.registerForm.enable();

View File

@@ -1,5 +0,0 @@
export interface ProfileDto {
profession: string;
utilisateur: string;
reseaux: any;
}

View File

@@ -1,14 +0,0 @@
export interface Profile {
id: string;
created: string;
updated: string;
profession: string;
utilisateur: string;
estVerifier: boolean;
secteur: string;
reseaux: JSON;
bio: string;
cv: string;
projets: string[];
apropos: string;
}

View File

@@ -1,6 +0,0 @@
export interface ProjectDto {
nom: string;
description: string;
lien: string;
utilisateur: string;
}

View File

@@ -1,10 +0,0 @@
export interface Project {
id: string;
created: string;
updated: string;
nom: string;
lien: string;
description: string;
fichier: string[];
utilisateur: string;
}

View File

@@ -8,8 +8,8 @@ export class FakeProfileRepository implements ProfileRepository {
return of(mockProfiles);
}
getByUserId(userId: string): Observable<Profile | null> {
const profile = mockProfiles.find((p) => p.utilisateur === userId) ?? null;
getByUserId(userId: string): Observable<Profile> {
const profile = mockProfiles.find((p) => p.utilisateur === userId) ?? ({} as Profile);
return of(profile);
}

View File

@@ -1,8 +1,8 @@
import { ProjectRepository } from '@app/domain/projects/project.repository';
import { Project } from '@app/shared/models/project';
import { Observable, of } from 'rxjs';
import { CreateProjectDto } from '@app/domain/projects/dto/create-project.dto';
import { fakeProjects } from '@app/testing/project.mock';
import { Project } from '@app/domain/projects/project.model';
export class FakeProjectRepository implements ProjectRepository {
private projects: Project[] = [...fakeProjects];

View File

@@ -2,6 +2,9 @@ import { ProfileFacade } from '@app/ui/profiles/profile.facade';
import { TestBed } from '@angular/core/testing';
import { PROFILE_REPOSITORY_TOKEN } from '@app/infrastructure/profiles/profile-repository.token';
import { FakeProfileRepository } from '@app/testing/domain/profiles/fake-profile.repository';
import { mockProfiles } from '@app/testing/profile.mock';
import { Profile } from '@app/domain/profiles/profile.model';
import { ProfileDTO } from '@app/domain/profiles/dto/create-profile.dto';
describe('ProfileFacade', () => {
let facade: ProfileFacade;
@@ -27,4 +30,52 @@ describe('ProfileFacade', () => {
done();
}, 0);
});
it("doit charger le profile d'un utilisateur ", () => {
facade.loadOne('1');
const expectedProfile = mockProfiles[0];
// attendre un peu le .subscribe
setTimeout(() => {
expect(facade.profile()).toBe(expectedProfile);
expect(facade.loading().isLoading).toBe(false);
expect(facade.error().hasError).toBe(false);
}, 0);
});
it('doit mettre à jour un profile ', () => {
expect(mockProfiles[0].profession).toBe('Développeur Web');
const profileUpdated: Profile = {
...mockProfiles[0],
profession: 'Devops',
};
facade.update('1', profileUpdated);
// attendre un peu le .subscribe
setTimeout(() => {
expect(facade.profile().profession).toBe('Devops');
expect(facade.loading().isLoading).toBe(false);
expect(facade.error().hasError).toBe(false);
}, 0);
});
it('doit creer un nouveau profile ', () => {
const profile: ProfileDTO = {
utilisateur: 'john doe',
reseaux: {},
profession: 'Journaliste',
};
facade.create(profile);
// attendre un peu le .subscribe
setTimeout(() => {
expect(mockProfiles.find((profile) => profile.id === facade.profile().id)).toBeDefined();
expect(facade.profile().profession).toBe('Journaliste');
expect(facade.loading().isLoading).toBe(false);
expect(facade.error().hasError).toBe(false);
}, 0);
});
});

View File

@@ -19,6 +19,10 @@ describe('ProfilePresenter', () => {
utilisateur: 'user_abc',
createdAtFormatted: new Date(profile.created).toLocaleDateString(),
avatarUrl: '',
apropos: 'Développeur Angular & Node.js',
bio: 'Passionné de code.',
cv: 'cv.pdf',
projets: ['p1', 'p2'],
});
});

View File

@@ -0,0 +1,26 @@
import { FakeProfileRepository } from '@app/testing/domain/profiles/fake-profile.repository';
import { mockProfiles } from '@app/testing/profile.mock';
import { CreateProfileUseCase } from '@app/usecase/profiles/create-profile.usecase';
import { ProfileDTO } from '@app/domain/profiles/dto/create-profile.dto';
describe('CreateProfileUseCase', () => {
it('doit creer nouveau un profile', () => {
const repo = new FakeProfileRepository();
const useCase = new CreateProfileUseCase(repo);
const profile: ProfileDTO = {
utilisateur: 'john doe',
profession: 'Designer',
reseaux: {},
};
useCase.execute(profile).subscribe({
next: (profile) => {
expect(mockProfiles).toContain(profile);
expect(mockProfiles.find((current_profile) => profile.id === current_profile.id)).toBe(
profile.id
);
},
});
});
});

View File

@@ -0,0 +1,16 @@
import { FakeProfileRepository } from '@app/testing/domain/profiles/fake-profile.repository';
import { mockProfiles } from '@app/testing/profile.mock';
import { GetProfileUseCase } from '@app/usecase/profiles/get-profile.usecase';
describe('GetProfileUseCase', () => {
it('doit retourner un profile', () => {
const repo = new FakeProfileRepository();
const useCase = new GetProfileUseCase(repo);
useCase.execute('1').subscribe({
next: (profile) => {
expect(profile).toEqual(mockProfiles[0]);
},
});
});
});

View File

@@ -0,0 +1,23 @@
import { FakeProfileRepository } from '@app/testing/domain/profiles/fake-profile.repository';
import { mockProfiles } from '@app/testing/profile.mock';
import { UpdateProfileUseCase } from '@app/usecase/profiles/update-profile.usecase';
describe('UpdateProfileUseCase', () => {
it('doit retourner un profile modifier', () => {
const repo = new FakeProfileRepository();
const useCase = new UpdateProfileUseCase(repo);
const profile = mockProfiles[0];
expect(profile.profession).toBe('Développeur Web');
const profileUpdate = {
...profile,
profession: 'Développeur fullstack',
};
useCase.execute('1', profileUpdate).subscribe({
next: (profile) => {
expect(profile.profession).toEqual('Développeur fullstack');
},
});
});
});

View File

@@ -1,21 +1,30 @@
import { ListProfilesUseCase } from '@app/usecase/profiles/list-profiles.usecase';
import { inject, signal } from '@angular/core';
import { inject, Injectable, signal } from '@angular/core';
import { PROFILE_REPOSITORY_TOKEN } from '@app/infrastructure/profiles/profile-repository.token';
import { Profile } from '@app/domain/profiles/profile.model';
import { Injectable } from '@angular/core';
import { ProfilePresenter } from '@app/ui/profiles/profile.presenter';
import { ProfileViewModel } from '@app/ui/profiles/profile.presenter.model';
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 { CreateProfileUseCase } from '@app/usecase/profiles/create-profile.usecase';
import { UpdateProfileUseCase } from '@app/usecase/profiles/update-profile.usecase';
import { GetProfileUseCase } from '@app/usecase/profiles/get-profile.usecase';
import { ProfileDTO } from '@app/domain/profiles/dto/create-profile.dto';
@Injectable({
providedIn: 'root',
})
export class ProfileFacade {
private profileRepository = inject(PROFILE_REPOSITORY_TOKEN);
private listUseCase = new ListProfilesUseCase(this.profileRepository);
private createUseCase = new CreateProfileUseCase(this.profileRepository);
private updateUseCase = new UpdateProfileUseCase(this.profileRepository);
private getUseCase = new GetProfileUseCase(this.profileRepository);
readonly profiles = signal<ProfileViewModel[]>([]);
readonly profile = signal<ProfileViewModel>({} as ProfileViewModel);
readonly loading = signal<LoaderAction>({ isLoading: false, action: ActionType.NONE });
readonly error = signal<ErrorResponse>({
action: ActionType.NONE,
@@ -37,6 +46,48 @@ export class ProfileFacade {
});
}
loadOne(userId: string) {
this.handleError(ActionType.READ, false, null, true);
this.getUseCase.execute(userId).subscribe({
next: (profile: Profile) => {
this.profile.set(ProfilePresenter.toViewModel(profile));
this.handleError(ActionType.READ, false, null, false);
},
error: (err) => {
this.handleError(ActionType.READ, false, err, false);
},
});
}
create(profileDto: ProfileDTO) {
this.handleError(ActionType.CREATE, false, null, true);
this.createUseCase.execute(profileDto).subscribe({
next: (profile: Profile) => {
this.profile.set(ProfilePresenter.toViewModel(profile));
this.handleError(ActionType.CREATE, false, null, false);
},
error: (err) => {
this.handleError(ActionType.CREATE, false, err, false);
},
});
}
update(profileId: string, profile: Partial<Profile>) {
this.handleError(ActionType.UPDATE, false, null, true);
this.updateUseCase.execute(profileId, profile).subscribe({
next: (profile: Profile) => {
this.profile.set(ProfilePresenter.toViewModel(profile));
this.handleError(ActionType.UPDATE, false, null, false);
},
error: (err) => {
this.handleError(ActionType.UPDATE, false, err, false);
},
});
}
private handleError(
action: ActionType = ActionType.NONE,
hasError: boolean,

View File

@@ -9,4 +9,8 @@ export interface ProfileViewModel {
profession: string;
secteur: string;
reseaux: any;
apropos: string;
bio: string;
cv: string;
projets: string[];
}

View File

@@ -14,6 +14,10 @@ export class ProfilePresenter {
profession: profile.profession,
secteur: profile.secteur,
reseaux: profile.reseaux,
apropos: profile.apropos,
projets: profile.projets,
cv: profile.cv,
bio: profile.bio,
};
}

View File

@@ -0,0 +1,12 @@
import { ProfileRepository } from '@app/domain/profiles/profile.repository';
import { Profile } from '@app/domain/profiles/profile.model';
import { Observable } from 'rxjs';
import { ProfileDTO } from '@app/domain/profiles/dto/create-profile.dto';
export class CreateProfileUseCase {
constructor(private readonly repo: ProfileRepository) {}
execute(profileDto: ProfileDTO): Observable<Profile> {
return this.repo.create(profileDto as Profile);
}
}

View File

@@ -0,0 +1,11 @@
import { ProfileRepository } from '@app/domain/profiles/profile.repository';
import { Observable } from 'rxjs';
import { Profile } from '@app/domain/profiles/profile.model';
export class GetProfileUseCase {
constructor(private readonly repo: ProfileRepository) {}
execute(userId: string): Observable<Profile> {
return this.repo.getByUserId(userId);
}
}

View File

@@ -0,0 +1,11 @@
import { ProfileRepository } from '@app/domain/profiles/profile.repository';
import { Profile } from '@app/domain/profiles/profile.model';
import { Observable } from 'rxjs';
export class UpdateProfileUseCase {
constructor(private readonly repo: ProfileRepository) {}
execute(profileId: string, profile: Partial<Profile>): Observable<Profile> {
return this.repo.update(profileId, profile as Profile);
}
}

View File

@@ -22,3 +22,33 @@ Object.defineProperty(document.body.style, 'transform', {
};
},
});
// Simule la fonction fetch pour éviter les erreurs dans les tests Jest
global.fetch = jest.fn(() =>
Promise.resolve({
ok: true,
json: () => Promise.resolve({}),
blob: () => Promise.resolve(new Blob()),
})
) as jest.Mock;
// 🟡 Ignore le warning pdfjs-dist
const originalWarn = console.warn;
console.warn = (...args) => {
if (
typeof args[0] === 'string' &&
args[0].includes('ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING_FLAG')
) {
return;
}
originalWarn(...args);
};
// 🔴 Ignore le log angularx-qrcode après les tests
const originalError = console.error;
console.error = (...args) => {
if (typeof args[0] === 'string' && args[0].includes('[angularx-qrcode]')) {
return;
}
originalError(...args);
};