sector => clean archi et test vert

This commit is contained in:
styve Lioumba
2025-10-24 19:25:23 +02:00
parent 3654709250
commit b6241ff911
38 changed files with 453 additions and 313 deletions

View File

@@ -15,6 +15,8 @@ import { PROFILE_REPOSITORY_TOKEN } from '@app/infrastructure/profiles/profile-r
import { PbProfileRepository } from '@app/infrastructure/profiles/pb-profile.repository';
import { PROJECT_REPOSITORY_TOKEN } from '@app/infrastructure/projects/project-repository.token';
import { PbProjectRepository } from '@app/infrastructure/projects/pb-project.repository';
import { SECTOR_REPOSITORY_TOKEN } from '@app/infrastructure/sectors/sector-repository.token';
import { PbSectorRepository } from '@app/infrastructure/sectors/pb-sector.repository';
export const appConfig: ApplicationConfig = {
providers: [
@@ -31,6 +33,7 @@ export const appConfig: ApplicationConfig = {
provideHttpClient(withFetch()),
{ provide: PROFILE_REPOSITORY_TOKEN, useExisting: PbProfileRepository },
{ provide: PROJECT_REPOSITORY_TOKEN, useExisting: PbProjectRepository },
{ provide: SECTOR_REPOSITORY_TOKEN, useExisting: PbSectorRepository },
provideToastr({
timeOut: 10000,
positionClass: 'toast-top-right',

View File

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

View File

@@ -1,24 +0,0 @@
import { Injectable } from '@angular/core';
import PocketBase from 'pocketbase';
import { environment } from '@env/environment.development';
import { from } from 'rxjs';
import { Sector } from '@app/shared/models/sector';
@Injectable({
providedIn: 'root',
})
export class SectorService {
get sectors() {
const pb = new PocketBase(environment.baseUrl);
return from(
pb.collection<Sector>('secteur').getFullList({
sort: 'nom',
})
);
}
getSectorById(id: string) {
const pb = new PocketBase(environment.baseUrl);
return from(pb.collection<Sector>('secteur').getOne<Sector>(id));
}
}

View File

@@ -0,0 +1,6 @@
export interface CreateSectorDto {
id: string;
created: string;
updated: string;
nom: string;
}

View File

@@ -0,0 +1,7 @@
import { Observable } from 'rxjs';
import { Sector } from '@app/domain/sectors/sector.model';
export interface SectorRepository {
list(): Observable<Sector[]>;
getOne(sectorId: string): Observable<Sector>;
}

View File

@@ -0,0 +1,25 @@
import { SectorRepository } from '@app/domain/sectors/sector.repository';
import { from, Observable } from 'rxjs';
import { environment } from '@env/environment';
import { Sector } from '@app/domain/sectors/sector.model';
import PocketBase from 'pocketbase';
import { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root',
})
export class PbSectorRepository implements SectorRepository {
private pb = new PocketBase(environment.baseUrl);
getOne(sectorId: string): Observable<Sector> {
return from(this.pb.collection<Sector>('secteur').getOne<Sector>(sectorId));
}
list(): Observable<Sector[]> {
return from(
this.pb.collection<Sector>('secteur').getFullList({
sort: 'nom',
})
);
}
}

View File

@@ -0,0 +1,4 @@
import { InjectionToken } from '@angular/core';
import { SectorRepository } from '@app/domain/sectors/sector.repository';
export const SECTOR_REPOSITORY_TOKEN = new InjectionToken<SectorRepository>('SectorRepository');

View File

@@ -2,17 +2,19 @@ import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ProfileDetailComponent } from './profile-detail.component';
import { provideRouter } from '@angular/router';
import { AuthService } from '@app/core/services/authentication/auth.service';
import { ToastrService } from 'ngx-toastr';
import { PROJECT_REPOSITORY_TOKEN } from '@app/infrastructure/projects/project-repository.token';
import { ProjectRepository } from '@app/domain/projects/project.repository';
import { of } from 'rxjs';
import { Project } from '@app/domain/projects/project.model';
import { Sector } from '@app/domain/sectors/sector.model';
import { SectorRepository } from '@app/domain/sectors/sector.repository';
import { SECTOR_REPOSITORY_TOKEN } from '@app/infrastructure/sectors/sector-repository.token';
describe('ProfileDetailComponent', () => {
let component: ProfileDetailComponent;
let fixture: ComponentFixture<ProfileDetailComponent>;
let mockProjectRepository: jest.Mocked<ProjectRepository>;
let mockSectorRepo: SectorRepository;
beforeEach(async () => {
mockProjectRepository = {
@@ -22,11 +24,17 @@ describe('ProfileDetailComponent', () => {
update: jest.fn().mockReturnValue(of({} as Project)),
};
mockSectorRepo = {
list: jest.fn(),
getOne: jest.fn().mockReturnValue(of({} as Sector)),
};
await TestBed.configureTestingModule({
imports: [ProfileDetailComponent],
providers: [
provideRouter([]),
{ provide: PROJECT_REPOSITORY_TOKEN, useValue: mockProjectRepository },
{ provide: SECTOR_REPOSITORY_TOKEN, useValue: mockSectorRepo },
],
}).compileComponents();

View File

@@ -1,6 +1,6 @@
@if (sector != undefined) {
@if (sector() != undefined) {
<div class="flex flex-wrap space-x-2 items-center space-y-1">
@for (chip of sector.nom.split('-'); track chip) {
@for (chip of sector().noms; track chip) {
<small
class="rounded-full bg-indigo-400 hover:bg-indigo-700 text-xs text-white py-1.5 px-3 font-semibold"
>{{ chip | titlecase }}</small

View File

@@ -1,14 +1,30 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ChipsComponent } from './chips.component';
import { provideRouter } from '@angular/router';
import { SECTOR_REPOSITORY_TOKEN } from '@app/infrastructure/sectors/sector-repository.token';
import { of } from 'rxjs';
import { Sector } from '@app/domain/sectors/sector.model';
import { SectorRepository } from '@app/domain/sectors/sector.repository';
describe('ChipsComponent', () => {
let component: ChipsComponent;
let fixture: ComponentFixture<ChipsComponent>;
let mockSectorRepo: SectorRepository;
beforeEach(async () => {
mockSectorRepo = {
list: jest.fn(),
getOne: jest.fn().mockReturnValue(of({} as Sector)),
};
await TestBed.configureTestingModule({
imports: [ChipsComponent],
providers: [
provideRouter([]),
{ provide: SECTOR_REPOSITORY_TOKEN, useValue: mockSectorRepo },
],
}).compileComponents();
fixture = TestBed.createComponent(ChipsComponent);

View File

@@ -1,8 +1,7 @@
import { Component, inject, Input, OnInit } from '@angular/core';
import { Sector } from '@app/shared/models/sector';
import { Component, Input, OnInit } from '@angular/core';
import { TitleCasePipe } from '@angular/common';
import { SectorService } from '@app/core/services/sector/sector.service';
import { UntilDestroy } from '@ngneat/until-destroy';
import { SectorFacade } from '@app/ui/sectors/sector.facade';
@Component({
selector: 'app-chips',
@@ -15,12 +14,12 @@ import { UntilDestroy } from '@ngneat/until-destroy';
export class ChipsComponent implements OnInit {
@Input({ required: true }) sectorId: string | null = null;
protected sectorService = inject(SectorService);
protected sector: Sector | undefined = undefined;
private readonly sectorFacade = new SectorFacade();
protected sector = this.sectorFacade.sector;
ngOnInit(): void {
if (this.sectorId)
this.sectorService.getSectorById(this.sectorId).subscribe((value) => (this.sector = value));
if (this.sectorId) {
this.sectorFacade.loadOne(this.sectorId);
}
}
}

View File

@@ -1,65 +0,0 @@
@if (user != undefined) {
<a
[routerLink]="[user.username ? user.username : user.id]"
[state]="{ user, profile }"
class="cursor-pointer"
>
<div
class="items-center bg-gray-50 rounded-lg shadow sm:flex dark:bg-gray-800 dark:border-gray-700 cursor-pointer"
>
<div class="sm:w-max w-full flex items-center justify-center">
@if (user.avatar) {
<img
class="max-w-xl rounded-lg max-h-64 object-cover sm:rounded-none sm:rounded-l-lg"
src="{{ environment.baseUrl }}/api/files/users/{{ user.id }}/{{ user.avatar }}"
alt="{{ user.username }}"
loading="lazy"
/>
} @else {
<img
class="max-w-xl rounded-lg max-h-64 sm:rounded-none sm:rounded-l-lg"
src="https://api.dicebear.com/9.x/adventurer/svg?seed={{ user.username }}"
alt="{{ user.username }}"
loading="lazy"
/>
}
</div>
<div class="p-5 flex flex-col items-center space-y-2">
@if (profile.estVerifier) {
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="currentColor"
class="size-6 text-purple-800"
>
<path
fill-rule="evenodd"
d="M8.603 3.799A4.49 4.49 0 0 1 12 2.25c1.357 0 2.573.6 3.397 1.549a4.49 4.49 0 0 1 3.498 1.307 4.491 4.491 0 0 1 1.307 3.497A4.49 4.49 0 0 1 21.75 12a4.49 4.49 0 0 1-1.549 3.397 4.491 4.491 0 0 1-1.307 3.497 4.491 4.491 0 0 1-3.497 1.307A4.49 4.49 0 0 1 12 21.75a4.49 4.49 0 0 1-3.397-1.549 4.49 4.49 0 0 1-3.498-1.306 4.491 4.491 0 0 1-1.307-3.498A4.49 4.49 0 0 1 2.25 12c0-1.357.6-2.573 1.549-3.397a4.49 4.49 0 0 1 1.307-3.497 4.49 4.49 0 0 1 3.497-1.307Zm7.007 6.387a.75.75 0 1 0-1.22-.872l-3.236 4.53L9.53 12.22a.75.75 0 0 0-1.06 1.06l2.25 2.25a.75.75 0 0 0 1.14-.094l3.75-5.25Z"
clip-rule="evenodd"
/>
</svg>
}
@if (user.name) {
<h3 class="text-xl font-bold tracking-tight text-gray-900 dark:text-white">
{{ user.name }}
</h3>
} @else if (user.username) {
<h3 class="text-xl font-bold tracking-tight text-gray-900 dark:text-white">
{{ user.username }}
</h3>
} @else {
<h3 class="text-xl font-bold tracking-tight text-gray-900 dark:text-white">
Non mentionné
</h3>
}
<span class="text-gray-500 dark:text-gray-400">{{ profile.profession }}</span>
<app-chips [sectorId]="profile.secteur" />
<app-reseaux [reseaux]="profile.reseaux" />
</div>
</div>
</a>
}

View File

@@ -1,22 +0,0 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { HorizentalProfileItemComponent } from './horizental-profile-item.component';
describe('HorizentalProfileItemComponent', () => {
let component: HorizentalProfileItemComponent;
let fixture: ComponentFixture<HorizentalProfileItemComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [HorizentalProfileItemComponent],
}).compileComponents();
fixture = TestBed.createComponent(HorizentalProfileItemComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -1,31 +0,0 @@
import { Component, inject, Input, OnInit } from '@angular/core';
import { Router, RouterLink } from '@angular/router';
import { UserService } from '@app/core/services/user/user.service';
import { User } from '@app/shared/models/user';
import { ChipsComponent } from '@app/shared/components/chips/chips.component';
import { ReseauxComponent } from '@app/shared/components/reseaux/reseaux.component';
import { environment } from '@env/environment';
import { ProfileViewModel } from '@app/ui/profiles/profile.presenter.model';
@Component({
selector: 'app-horizental-profile-item',
standalone: true,
imports: [RouterLink, ChipsComponent, ReseauxComponent],
templateUrl: './horizental-profile-item.component.html',
styleUrl: './horizental-profile-item.component.scss',
})
export class HorizentalProfileItemComponent implements OnInit {
@Input({ required: true }) profile: ProfileViewModel = {} as ProfileViewModel;
protected router = inject(Router);
protected userService = inject(UserService);
protected user: User | undefined = undefined;
ngOnInit(): void {
this.userService
.getUserById(this.profile.utilisateur)
.subscribe((value) => (this.user = value));
}
protected readonly environment = environment;
}

View File

@@ -1,11 +0,0 @@
<section class="bg-white dark:bg-gray-900">
<div class="py-8 px-4 mx-auto max-w-screen-xl text-center lg:py-16 lg:px-6">
<div class="grid gap-8 mb-6 lg:mb-16 md:grid-cols-2">
@for (profile of profiles; track profile.id) {
<app-horizental-profile-item [profile]="profile" />
} @empty {
<p>Aucun profile trouvée</p>
}
</div>
</div>
</section>

View File

@@ -1,22 +0,0 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { HorizentalProfileListComponent } from './horizental-profile-list.component';
describe('HorizentalProfileListComponent', () => {
let component: HorizentalProfileListComponent;
let fixture: ComponentFixture<HorizentalProfileListComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [HorizentalProfileListComponent],
}).compileComponents();
fixture = TestBed.createComponent(HorizentalProfileListComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -1,14 +0,0 @@
import { Component, Input } from '@angular/core';
import { HorizentalProfileItemComponent } from '@app/shared/components/horizental-profile-item/horizental-profile-item.component';
import { ProfileViewModel } from '@app/ui/profiles/profile.presenter.model';
@Component({
selector: 'app-horizental-profile-list',
standalone: true,
imports: [HorizentalProfileItemComponent],
templateUrl: './horizental-profile-list.component.html',
styleUrl: './horizental-profile-list.component.scss',
})
export class HorizentalProfileListComponent {
@Input({ required: true }) profiles: ProfileViewModel[] = [];
}

View File

@@ -8,6 +8,9 @@ import { PROFILE_REPOSITORY_TOKEN } from '@app/infrastructure/profiles/profile-r
import { ProfileRepository } from '@app/domain/profiles/profile.repository';
import { of } from 'rxjs';
import { Profile } from '@app/domain/profiles/profile.model';
import { SectorRepository } from '@app/domain/sectors/sector.repository';
import { Sector } from '@app/domain/sectors/sector.model';
import { SECTOR_REPOSITORY_TOKEN } from '@app/infrastructure/sectors/sector-repository.token';
describe('MyProfileUpdateFormComponent', () => {
let component: MyProfileUpdateFormComponent;
@@ -15,6 +18,7 @@ describe('MyProfileUpdateFormComponent', () => {
let mockToastrService: Partial<ToastrService>;
let mockProfileRepo: ProfileRepository;
let mockSectorRepo: SectorRepository;
const mockProfileData = {
profession: '',
@@ -38,6 +42,11 @@ describe('MyProfileUpdateFormComponent', () => {
getByUserId: jest.fn().mockReturnValue(of({} as Profile)),
};
mockSectorRepo = {
list: jest.fn().mockReturnValue(of([])),
getOne: jest.fn().mockReturnValue(of({} as Sector)),
};
await TestBed.configureTestingModule({
imports: [MyProfileUpdateFormComponent],
providers: [
@@ -45,6 +54,7 @@ describe('MyProfileUpdateFormComponent', () => {
provideRouter([]),
{ provide: ToastrService, useValue: mockToastrService },
{ provide: PROFILE_REPOSITORY_TOKEN, useValue: mockProfileRepo },
{ provide: SECTOR_REPOSITORY_TOKEN, useValue: mockSectorRepo },
],
}).compileComponents();

View File

@@ -1,4 +1,4 @@
import { Component, effect, inject, Input, OnInit, signal } from '@angular/core';
import { Component, effect, inject, Input, OnInit } from '@angular/core';
import {
FormBuilder,
FormControl,
@@ -8,8 +8,6 @@ import {
} from '@angular/forms';
import { UntilDestroy } from '@ngneat/until-destroy';
import { NgClass } from '@angular/common';
import { SectorService } from '@app/core/services/sector/sector.service';
import { Sector } from '@app/shared/models/sector';
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';
@@ -17,6 +15,7 @@ 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';
import { SectorFacade } from '@app/ui/sectors/sector.facade';
@Component({
selector: 'app-my-profile-update-form',
@@ -31,15 +30,19 @@ export class MyProfileUpdateFormComponent implements OnInit {
@Input({ required: true }) profile: ProfileViewModel = {} as ProfileViewModel;
private readonly formBuilder = inject(FormBuilder);
protected readonly sectorService = inject(SectorService);
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;
private readonly sectorFacade = new SectorFacade();
protected sectors = this.sectorFacade.sectors;
protected sector = this.sectorFacade.sector;
protected readonly sectorLoading = this.sectorFacade.loading;
protected readonly sectorError = this.sectorFacade.error;
constructor() {
effect(() => {
switch (this.loading().action) {
@@ -58,6 +61,12 @@ export class MyProfileUpdateFormComponent implements OnInit {
);
}
}
switch (this.sectorLoading().action) {
case ActionType.READ:
if (!this.sectorLoading() && !this.sectorError().hasError) {
this.profileForm.get('secteur')!.setValue(this.sector().id);
}
}
});
}
@@ -95,12 +104,10 @@ export class MyProfileUpdateFormComponent implements OnInit {
});
if (this.profile.secteur) {
this.sectorService
.getSectorById(this.profile.secteur)
.subscribe((value) => this.profileForm.get('secteur')!.setValue(value.id));
this.sectorFacade.loadOne(this.profile.secteur);
}
this.sectorService.sectors.subscribe((value) => this.sectors.set(value));
this.sectorFacade.load();
}
onSubmit() {

View File

@@ -1,21 +1,16 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { VerticalProfileListComponent } from './vertical-profile-list.component';
import { Router } from '@angular/router';
import { provideRouter } from '@angular/router';
describe('VerticalProfileListComponent', () => {
let component: VerticalProfileListComponent;
let fixture: ComponentFixture<VerticalProfileListComponent>;
const routerSpy = {
navigate: jest.fn(),
navigateByUrl: jest.fn(),
};
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [VerticalProfileListComponent],
providers: [{ provide: Router, useValue: routerSpy }],
providers: [provideRouter([])],
}).compileComponents();
fixture = TestBed.createComponent(VerticalProfileListComponent);

View File

@@ -0,0 +1,15 @@
import { SectorRepository } from '@app/domain/sectors/sector.repository';
import { Observable, of } from 'rxjs';
import { Sector } from '@app/domain/sectors/sector.model';
import { fakeSectors } from '@app/testing/sector.mock';
export class FakeSectorRepository implements SectorRepository {
getOne(sectorId: string): Observable<Sector> {
const sector = fakeSectors.find((s) => s.id === sectorId) ?? ({} as Sector);
return of(sector);
}
list(): Observable<Sector[]> {
return of(fakeSectors);
}
}

View File

@@ -2,6 +2,7 @@ import { PbProfileRepository } from '@app/infrastructure/profiles/pb-profile.rep
import { Profile } from '@app/domain/profiles/profile.model';
import { ProfileDTO } from '@app/domain/profiles/dto/create-profile.dto';
import PocketBase from 'pocketbase';
import { mockProfiles } from '@app/testing/profile.mock';
jest.mock('pocketbase'); // on mock le module PocketBase
@@ -36,29 +37,12 @@ describe('PbProfileRepository', () => {
// 🔹 TEST : list()
// ------------------------------------------
it('devrait appeler pb.collection("profiles").getFullList() avec un tri par profession', (done) => {
const fakeProfiles: Profile[] = [
{
id: '1',
created: '',
updated: '',
profession: 'Développeur',
utilisateur: 'u001',
estVerifier: false,
secteur: 'Informatique',
reseaux: {} as JSON,
bio: 'Bio test',
cv: '',
projets: [],
apropos: 'À propos...',
},
];
mockCollection.getFullList.mockResolvedValue(fakeProfiles);
mockCollection.getFullList.mockResolvedValue(mockProfiles);
repo.list().subscribe((result) => {
expect(mockPocketBase.collection).toHaveBeenCalledWith('profiles');
expect(mockCollection.getFullList).toHaveBeenCalledWith({ sort: 'profession' });
expect(result).toEqual(fakeProfiles);
expect(result).toEqual(mockProfiles);
done();
});
});
@@ -66,29 +50,14 @@ describe('PbProfileRepository', () => {
// ------------------------------------------
// 🔹 TEST : getByUserId()
// ------------------------------------------
it('devrait appeler pb.collection("profiles").getFirstListItem() avec le bon filtre utilisateur', (done) => {
const userId = 'user_001';
const fakeProfile: Profile = {
id: 'p001',
created: '',
updated: '',
profession: 'Designer',
utilisateur: userId,
estVerifier: true,
secteur: 'Création',
reseaux: {} as JSON,
bio: 'Bio',
cv: '',
projets: [],
apropos: 'À propos...',
};
it('devrait appeler pb.collection("profiles").getFirstListItem() avec le bon filtre utilisateur', () => {
const userId = '1';
mockCollection.getFirstListItem.mockResolvedValue(fakeProfile);
mockCollection.getFirstListItem.mockResolvedValue(mockProfiles);
repo.getByUserId(userId).subscribe((result) => {
expect(mockCollection.getFirstListItem).toHaveBeenCalledWith(`utilisateur="${userId}"`);
expect(result).toEqual(fakeProfile);
done();
expect(result).toEqual(mockProfiles[0]);
});
});
@@ -128,7 +97,7 @@ describe('PbProfileRepository', () => {
// 🔹 TEST : update()
// ------------------------------------------
it('devrait appeler pb.collection("profiles").update() avec ID et données partielle', (done) => {
const id = 'p002';
const id = '1';
const data = { bio: 'Bio mise à jour' };
const updatedProfile: Profile = {
id,

View File

@@ -2,6 +2,7 @@ import { PbProjectRepository } from '@app/infrastructure/projects/pb-project.rep
import { CreateProjectDto } from '@app/domain/projects/dto/create-project.dto';
import { Project } from '@app/domain/projects/project.model';
import PocketBase from 'pocketbase';
import { fakeProjects } from '@app/testing/project.mock';
jest.mock('pocketbase'); // on mock le module entier
@@ -63,19 +64,7 @@ describe('PbProjectRepository', () => {
// 🔹 TEST : list()
// ------------------------------------------
it('devrait appeler pb.collection("projets").getFullList() avec le filtre utilisateur', (done) => {
const userId = 'user_001';
const fakeProjects: Project[] = [
{
id: '1',
created: '',
updated: '',
nom: 'P1',
lien: '',
description: '',
fichier: [],
utilisateur: userId,
},
];
const userId = '1';
mockCollection.getFullList.mockResolvedValue(fakeProjects);
@@ -92,7 +81,7 @@ describe('PbProjectRepository', () => {
// 🔹 TEST : get()
// ------------------------------------------
it('devrait appeler pb.collection("projets").getOne() avec le bon ID', (done) => {
const id = 'p123';
const id = '1';
const fakeProject: Project = {
id,
created: '',

View File

@@ -0,0 +1,57 @@
import { FakeSectorRepository } from '@app/testing/domain/sectors/fake-sector.repository';
import { PbSectorRepository } from '@app/infrastructure/sectors/pb-sector.repository';
import { fakeSectors } from '@app/testing/sector.mock';
import PocketBase from 'pocketbase';
jest.mock('pocketbase'); // on mock le module entier
describe('SectorRepository', () => {
let sectorRepo: FakeSectorRepository;
let mockCollection: any;
let mockPocketBase: any;
beforeEach(() => {
// Création dun faux client PocketBase avec des méthodes mockées
mockPocketBase = {
collection: jest.fn().mockReturnValue({
getFullList: 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('sectors');
// on instancie le repository à tester
sectorRepo = new PbSectorRepository();
});
// ------------------------------------------
// 🔹 TEST : list()
// ------------------------------------------
it('devrait appeler pb.collection("sectors").getFullList() ', () => {
mockCollection.getFullList.mockResolvedValue(fakeSectors);
sectorRepo.list().subscribe((result) => {
expect(mockPocketBase.collection).toHaveBeenCalledWith('sectors');
expect(result).toEqual(fakeSectors);
});
});
// ------------------------------------------
// 🔹 TEST : getOne()
// ------------------------------------------
it('devrait appeler pb.collection("sectors").getFirstListItem() ', () => {
const sectorId = 'sector_001';
mockCollection.getOne.mockResolvedValue(fakeSectors.find((s) => s.id === sectorId));
sectorRepo.getOne(sectorId).subscribe((result) => {
expect(mockCollection.getFirstListItem).toHaveBeenCalledWith(`sectors="${sectorId}"`);
expect(result).toEqual(fakeSectors[0]);
});
});
});

View File

@@ -0,0 +1,34 @@
import { Sector } from '@app/domain/sectors/sector.model';
export const fakeSectors: Sector[] = [
{
id: 'sector_001',
created: '2025-01-01T10:00:00Z',
updated: '2025-01-05T14:00:00Z',
nom: 'Informatique',
},
{
id: 'sector_002',
created: '2025-01-02T11:00:00Z',
updated: '2025-01-06T15:00:00Z',
nom: 'Design graphique',
},
{
id: 'sector_003',
created: '2025-01-03T12:00:00Z',
updated: '2025-01-07T16:00:00Z',
nom: 'Marketing digital',
},
{
id: 'sector_004',
created: '2025-01-04T13:00:00Z',
updated: '2025-01-08T17:00:00Z',
nom: 'Finance et comptabilité',
},
{
id: 'sector_005',
created: '2025-01-05T14:00:00Z',
updated: '2025-01-09T18:00:00Z',
nom: 'Ressources humaines',
},
];

View File

@@ -0,0 +1,46 @@
import { TestBed } from '@angular/core/testing';
import { SectorFacade } from '@app/ui/sectors/sector.facade';
import { SECTOR_REPOSITORY_TOKEN } from '@app/infrastructure/sectors/sector-repository.token';
import { FakeSectorRepository } from '@app/testing/domain/sectors/fake-sector.repository';
import { fakeSectors } from '@app/testing/sector.mock';
describe('SectorFacade', () => {
let sectorFacade: SectorFacade;
beforeEach(() => {
TestBed.configureTestingModule({
providers: [
SectorFacade,
{ provide: SECTOR_REPOSITORY_TOKEN, useClass: FakeSectorRepository },
],
});
sectorFacade = TestBed.inject(SectorFacade);
});
// ------------------------------------------
// 🔹 TEST : Chargement des secteurs (load)
// ------------------------------------------
it("devrait charger les secteurs d'activité ", () => {
sectorFacade.load();
setTimeout(() => {
expect(sectorFacade.sectors().length).toBe(fakeSectors.length);
expect(sectorFacade.loading().isLoading).toBe(false);
expect(sectorFacade.error().hasError).toBe(false);
}, 0);
});
// ------------------------------------------
// 🔹 TEST : Chargement dun secteur unique
// ------------------------------------------
it('devrait charger un secteur par son ID', () => {
const sectorId = '1';
sectorFacade.loadOne(sectorId);
setTimeout(() => {
expect(sectorFacade.sector()).toBe(fakeSectors.find((s) => s.id === sectorId));
expect(sectorFacade.error().hasError).toBe(false);
}, 0);
});
});

View File

@@ -0,0 +1,30 @@
import { SectorPresenter } from '@app/ui/sectors/sector.presenter';
import { Sector } from '@app/domain/sectors/sector.model';
import { fakeSectors } from '@app/testing/sector.mock';
describe('SectorPresenter', () => {
let presenter: SectorPresenter;
beforeEach(() => {
presenter = new SectorPresenter();
});
it('devrait transformer un Sector en SectorPresenterModel', () => {
const sector: Sector = fakeSectors[0];
const viewModel = presenter.toViewModel(sector);
expect(viewModel).toEqual({
id: sector.id,
nom: sector.nom,
noms: ['Informatique'],
});
});
it('devrait transformer un tableau complet', () => {
const result = presenter.toViewModels(fakeSectors);
expect(result.length).toBe(5);
expect(result[0].nom).toBe(fakeSectors[0].nom);
});
});

View File

@@ -0,0 +1,20 @@
import { FakeSectorRepository } from '@app/testing/domain/sectors/fake-sector.repository';
import { fakeSectors } from '@app/testing/sector.mock';
import { GetSectorUseCase } from '@app/usecase/sectors/get-sector.usecase';
describe('ListSectorUsecase', () => {
let useCase: GetSectorUseCase;
let repo: FakeSectorRepository;
beforeEach(() => {
repo = new FakeSectorRepository();
useCase = new GetSectorUseCase(repo);
});
it('doit retourne la liste des secteurs ', () => {
const sectorId = '1';
useCase.execute(sectorId).subscribe((sector) => {
expect(sector).toBe(fakeSectors.find((s) => s.id === sectorId));
});
});
});

View File

@@ -0,0 +1,19 @@
import { ListSectorUsecase } from '@app/usecase/sectors/list-sector.usecase';
import { FakeSectorRepository } from '@app/testing/domain/sectors/fake-sector.repository';
import { fakeSectors } from '@app/testing/sector.mock';
describe('ListSectorUsecase', () => {
let useCase: ListSectorUsecase;
let repo: FakeSectorRepository;
beforeEach(() => {
repo = new FakeSectorRepository();
useCase = new ListSectorUsecase(repo);
});
it('doit retourne la liste des secteurs ', () => {
useCase.execute().subscribe((sectors) => {
expect(sectors.length).toBe(fakeSectors.length);
});
});
});

View File

@@ -35,10 +35,7 @@ export class ProjectFacade {
private readonly projectPresenter = new ProjectPresenter();
load(userId: string) {
this.loading.set({
action: ActionType.READ,
isLoading: true,
});
this.handleError(ActionType.READ, false, null, true);
this.listUseCase.execute(userId).subscribe({
next: (projects: Project[]) => {
@@ -53,10 +50,7 @@ export class ProjectFacade {
}
loadOne(projectId: string) {
this.loading.set({
action: ActionType.READ,
isLoading: true,
});
this.handleError(ActionType.READ, false, null, true);
this.getUseCase.execute(projectId).subscribe({
next: (project: Project) => {
@@ -70,10 +64,7 @@ export class ProjectFacade {
}
create(projectDto: CreateProjectDto) {
this.loading.set({
action: ActionType.CREATE,
isLoading: true,
});
this.handleError(ActionType.CREATE, false, null, true);
this.createUseCase.execute(projectDto).subscribe({
next: (project: Project) => {
@@ -88,10 +79,7 @@ export class ProjectFacade {
}
update(userId: string, data: any) {
this.loading.set({
action: ActionType.UPDATE,
isLoading: true,
});
this.handleError(ActionType.UPDATE, false, null, true);
this.UpdateUseCase.execute(userId, data).subscribe({
next: (project: Project) => {

View File

@@ -0,0 +1,65 @@
import { inject, signal } from '@angular/core';
import { SECTOR_REPOSITORY_TOKEN } from '@app/infrastructure/sectors/sector-repository.token';
import { ListSectorUsecase } from '@app/usecase/sectors/list-sector.usecase';
import { GetSectorUseCase } from '@app/usecase/sectors/get-sector.usecase';
import { ActionType } from '@app/domain/action-type.util';
import { LoaderAction } from '@app/domain/loader-action.util';
import { ErrorResponse } from '@app/domain/error-response.util';
import { SectorPresenterModel } from '@app/ui/sectors/sector.presenter.model';
import { Sector } from '@app/domain/sectors/sector.model';
import { SectorPresenter } from '@app/ui/sectors/sector.presenter';
export class SectorFacade {
private readonly sectorRepo = inject(SECTOR_REPOSITORY_TOKEN);
private readonly listSectorUseCase = new ListSectorUsecase(this.sectorRepo);
private readonly getSectorUseCase = new GetSectorUseCase(this.sectorRepo);
readonly sectors = signal<SectorPresenterModel[]>([]);
readonly sector = signal<SectorPresenterModel>({} as SectorPresenterModel);
readonly loading = signal<LoaderAction>({ isLoading: false, action: ActionType.NONE });
readonly error = signal<ErrorResponse>({
action: ActionType.NONE,
hasError: false,
message: null,
});
private readonly sectorPresenter = new SectorPresenter();
load() {
this.handleError(ActionType.READ, false, null, true);
this.listSectorUseCase.execute().subscribe({
next: (sectors: Sector[]) => {
this.sectors.set(this.sectorPresenter.toViewModels(sectors));
this.handleError(ActionType.READ, false, null, false);
},
error: (err) => {
this.handleError(ActionType.READ, false, err, false);
},
});
}
loadOne(sectorId: string) {
this.handleError(ActionType.READ, false, null, true);
this.getSectorUseCase.execute(sectorId).subscribe({
next: (sector: Sector) => {
this.sector.set(this.sectorPresenter.toViewModel(sector));
this.handleError(ActionType.READ, false, null, false);
},
error: (err) => {
this.handleError(ActionType.READ, false, err, false);
},
});
}
private handleError(
action: ActionType = ActionType.NONE,
hasError: boolean,
message: string | null = null,
isLoading = false
) {
this.error.set({ action, hasError, message });
this.loading.set({ action, isLoading });
}
}

View File

@@ -0,0 +1,5 @@
export interface SectorPresenterModel {
id: string;
nom: string;
noms?: string[];
}

View File

@@ -0,0 +1,17 @@
import { SectorPresenterModel } from '@app/ui/sectors/sector.presenter.model';
import { Sector } from '@app/domain/sectors/sector.model';
export class SectorPresenter {
toViewModel(sector: Sector): SectorPresenterModel {
const names = sector.nom ? sector.nom.split('-') : [];
return {
id: sector.id,
nom: sector.nom,
noms: names,
};
}
toViewModels(sectors: Sector[]): SectorPresenterModel[] {
return sectors.map(this.toViewModel);
}
}

View File

@@ -0,0 +1,11 @@
import { SectorRepository } from '@app/domain/sectors/sector.repository';
import { Observable } from 'rxjs';
import { Sector } from '@app/domain/sectors/sector.model';
export class GetSectorUseCase {
constructor(private readonly sectorRepository: SectorRepository) {}
execute(sectorId: string): Observable<Sector> {
return this.sectorRepository.getOne(sectorId);
}
}

View File

@@ -0,0 +1,11 @@
import { SectorRepository } from '@app/domain/sectors/sector.repository';
import { Observable } from 'rxjs';
import { Sector } from '@app/domain/sectors/sector.model';
export class ListSectorUsecase {
constructor(private readonly sectorRepository: SectorRepository) {}
execute(): Observable<Sector[]> {
return this.sectorRepository.list();
}
}