From b6241ff91185a1732015f60e001941b15502d857 Mon Sep 17 00:00:00 2001 From: styve Lioumba Date: Fri, 24 Oct 2025 19:25:23 +0200 Subject: [PATCH] sector => clean archi et test vert --- src/app/app.config.ts | 3 + .../services/sector/sector.service.spec.ts | 27 -------- .../core/services/sector/sector.service.ts | 24 ------- .../domain/sectors/dto/create-sector.dto.ts | 6 ++ .../sectors/sector.model.ts} | 0 src/app/domain/sectors/sector.repository.ts | 7 ++ .../sectors/pb-sector.repository.ts | 25 +++++++ .../sectors/sector-repository.token.ts | 4 ++ .../profile-detail.component.spec.ts | 12 +++- .../components/chips/chips.component.html | 4 +- .../components/chips/chips.component.spec.ts | 16 +++++ .../components/chips/chips.component.ts | 15 ++--- .../horizental-profile-item.component.html | 65 ------------------- .../horizental-profile-item.component.scss | 0 .../horizental-profile-item.component.spec.ts | 22 ------- .../horizental-profile-item.component.ts | 31 --------- .../horizental-profile-list.component.html | 11 ---- .../horizental-profile-list.component.scss | 0 .../horizental-profile-list.component.spec.ts | 22 ------- .../horizental-profile-list.component.ts | 14 ---- .../my-profile-update-form.component.spec.ts | 10 +++ .../my-profile-update-form.component.ts | 25 ++++--- .../vertical-profile-list.component.spec.ts | 9 +-- .../domain/sectors/fake-sector.repository.ts | 15 +++++ .../profiles/pb-profile.repository.spec.ts | 47 +++----------- .../projects/pb-project.repository.spec.ts | 17 +---- .../sectors/pb-sector.repository.spec.ts | 57 ++++++++++++++++ src/app/testing/sector.mock.ts | 34 ++++++++++ .../testing/ui/sectors/sector.facade.spec.ts | 46 +++++++++++++ .../ui/sectors/sector.presenter.spec.ts | 30 +++++++++ .../sectors/get-sector.usecase.spec.ts | 20 ++++++ .../sectors/list-sector.usecase.spec.ts | 19 ++++++ src/app/ui/projects/project.facade.ts | 20 ++---- src/app/ui/sectors/sector.facade.ts | 65 +++++++++++++++++++ src/app/ui/sectors/sector.presenter.model.ts | 5 ++ src/app/ui/sectors/sector.presenter.ts | 17 +++++ src/app/usecase/sectors/get-sector.usecase.ts | 11 ++++ .../usecase/sectors/list-sector.usecase.ts | 11 ++++ 38 files changed, 453 insertions(+), 313 deletions(-) delete mode 100644 src/app/core/services/sector/sector.service.spec.ts delete mode 100644 src/app/core/services/sector/sector.service.ts create mode 100644 src/app/domain/sectors/dto/create-sector.dto.ts rename src/app/{shared/models/sector.ts => domain/sectors/sector.model.ts} (100%) create mode 100644 src/app/domain/sectors/sector.repository.ts create mode 100644 src/app/infrastructure/sectors/pb-sector.repository.ts create mode 100644 src/app/infrastructure/sectors/sector-repository.token.ts delete mode 100644 src/app/shared/components/horizental-profile-item/horizental-profile-item.component.html delete mode 100644 src/app/shared/components/horizental-profile-item/horizental-profile-item.component.scss delete mode 100644 src/app/shared/components/horizental-profile-item/horizental-profile-item.component.spec.ts delete mode 100644 src/app/shared/components/horizental-profile-item/horizental-profile-item.component.ts delete mode 100644 src/app/shared/components/horizental-profile-list/horizental-profile-list.component.html delete mode 100644 src/app/shared/components/horizental-profile-list/horizental-profile-list.component.scss delete mode 100644 src/app/shared/components/horizental-profile-list/horizental-profile-list.component.spec.ts delete mode 100644 src/app/shared/components/horizental-profile-list/horizental-profile-list.component.ts create mode 100644 src/app/testing/domain/sectors/fake-sector.repository.ts create mode 100644 src/app/testing/infrastructure/sectors/pb-sector.repository.spec.ts create mode 100644 src/app/testing/sector.mock.ts create mode 100644 src/app/testing/ui/sectors/sector.facade.spec.ts create mode 100644 src/app/testing/ui/sectors/sector.presenter.spec.ts create mode 100644 src/app/testing/usecase/sectors/get-sector.usecase.spec.ts create mode 100644 src/app/testing/usecase/sectors/list-sector.usecase.spec.ts create mode 100644 src/app/ui/sectors/sector.facade.ts create mode 100644 src/app/ui/sectors/sector.presenter.model.ts create mode 100644 src/app/ui/sectors/sector.presenter.ts create mode 100644 src/app/usecase/sectors/get-sector.usecase.ts create mode 100644 src/app/usecase/sectors/list-sector.usecase.ts diff --git a/src/app/app.config.ts b/src/app/app.config.ts index 41c2385..d87264e 100644 --- a/src/app/app.config.ts +++ b/src/app/app.config.ts @@ -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', diff --git a/src/app/core/services/sector/sector.service.spec.ts b/src/app/core/services/sector/sector.service.spec.ts deleted file mode 100644 index f3e501c..0000000 --- a/src/app/core/services/sector/sector.service.spec.ts +++ /dev/null @@ -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(); - }); -}); diff --git a/src/app/core/services/sector/sector.service.ts b/src/app/core/services/sector/sector.service.ts deleted file mode 100644 index e8c57dc..0000000 --- a/src/app/core/services/sector/sector.service.ts +++ /dev/null @@ -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('secteur').getFullList({ - sort: 'nom', - }) - ); - } - - getSectorById(id: string) { - const pb = new PocketBase(environment.baseUrl); - return from(pb.collection('secteur').getOne(id)); - } -} diff --git a/src/app/domain/sectors/dto/create-sector.dto.ts b/src/app/domain/sectors/dto/create-sector.dto.ts new file mode 100644 index 0000000..946da29 --- /dev/null +++ b/src/app/domain/sectors/dto/create-sector.dto.ts @@ -0,0 +1,6 @@ +export interface CreateSectorDto { + id: string; + created: string; + updated: string; + nom: string; +} diff --git a/src/app/shared/models/sector.ts b/src/app/domain/sectors/sector.model.ts similarity index 100% rename from src/app/shared/models/sector.ts rename to src/app/domain/sectors/sector.model.ts diff --git a/src/app/domain/sectors/sector.repository.ts b/src/app/domain/sectors/sector.repository.ts new file mode 100644 index 0000000..dee8089 --- /dev/null +++ b/src/app/domain/sectors/sector.repository.ts @@ -0,0 +1,7 @@ +import { Observable } from 'rxjs'; +import { Sector } from '@app/domain/sectors/sector.model'; + +export interface SectorRepository { + list(): Observable; + getOne(sectorId: string): Observable; +} diff --git a/src/app/infrastructure/sectors/pb-sector.repository.ts b/src/app/infrastructure/sectors/pb-sector.repository.ts new file mode 100644 index 0000000..3fcbc61 --- /dev/null +++ b/src/app/infrastructure/sectors/pb-sector.repository.ts @@ -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 { + return from(this.pb.collection('secteur').getOne(sectorId)); + } + + list(): Observable { + return from( + this.pb.collection('secteur').getFullList({ + sort: 'nom', + }) + ); + } +} diff --git a/src/app/infrastructure/sectors/sector-repository.token.ts b/src/app/infrastructure/sectors/sector-repository.token.ts new file mode 100644 index 0000000..27a98ed --- /dev/null +++ b/src/app/infrastructure/sectors/sector-repository.token.ts @@ -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'); diff --git a/src/app/routes/profile/profile-detail/profile-detail.component.spec.ts b/src/app/routes/profile/profile-detail/profile-detail.component.spec.ts index 4383139..90469d3 100644 --- a/src/app/routes/profile/profile-detail/profile-detail.component.spec.ts +++ b/src/app/routes/profile/profile-detail/profile-detail.component.spec.ts @@ -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; let mockProjectRepository: jest.Mocked; + 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(); diff --git a/src/app/shared/components/chips/chips.component.html b/src/app/shared/components/chips/chips.component.html index 49691d4..0b5411a 100644 --- a/src/app/shared/components/chips/chips.component.html +++ b/src/app/shared/components/chips/chips.component.html @@ -1,6 +1,6 @@ -@if (sector != undefined) { +@if (sector() != undefined) {
- @for (chip of sector.nom.split('-'); track chip) { + @for (chip of sector().noms; track chip) { {{ chip | titlecase }} { let component: ChipsComponent; let fixture: ComponentFixture; + 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); diff --git a/src/app/shared/components/chips/chips.component.ts b/src/app/shared/components/chips/chips.component.ts index 024dade..dd1e90a 100644 --- a/src/app/shared/components/chips/chips.component.ts +++ b/src/app/shared/components/chips/chips.component.ts @@ -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); + } } } diff --git a/src/app/shared/components/horizental-profile-item/horizental-profile-item.component.html b/src/app/shared/components/horizental-profile-item/horizental-profile-item.component.html deleted file mode 100644 index 7aa6aeb..0000000 --- a/src/app/shared/components/horizental-profile-item/horizental-profile-item.component.html +++ /dev/null @@ -1,65 +0,0 @@ -@if (user != undefined) { - -
-
- @if (user.avatar) { - {{ user.username }} - } @else { - {{ user.username }} - } -
- -
- @if (profile.estVerifier) { - - - - } - - @if (user.name) { -

- {{ user.name }} -

- } @else if (user.username) { -

- {{ user.username }} -

- } @else { -

- Non mentionné -

- } - {{ profile.profession }} - - - - -
-
-
-} diff --git a/src/app/shared/components/horizental-profile-item/horizental-profile-item.component.scss b/src/app/shared/components/horizental-profile-item/horizental-profile-item.component.scss deleted file mode 100644 index e69de29..0000000 diff --git a/src/app/shared/components/horizental-profile-item/horizental-profile-item.component.spec.ts b/src/app/shared/components/horizental-profile-item/horizental-profile-item.component.spec.ts deleted file mode 100644 index c4c46c7..0000000 --- a/src/app/shared/components/horizental-profile-item/horizental-profile-item.component.spec.ts +++ /dev/null @@ -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; - - beforeEach(async () => { - await TestBed.configureTestingModule({ - imports: [HorizentalProfileItemComponent], - }).compileComponents(); - - fixture = TestBed.createComponent(HorizentalProfileItemComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); - - it('should create', () => { - expect(component).toBeTruthy(); - }); -}); diff --git a/src/app/shared/components/horizental-profile-item/horizental-profile-item.component.ts b/src/app/shared/components/horizental-profile-item/horizental-profile-item.component.ts deleted file mode 100644 index a425f83..0000000 --- a/src/app/shared/components/horizental-profile-item/horizental-profile-item.component.ts +++ /dev/null @@ -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; -} diff --git a/src/app/shared/components/horizental-profile-list/horizental-profile-list.component.html b/src/app/shared/components/horizental-profile-list/horizental-profile-list.component.html deleted file mode 100644 index 098ccde..0000000 --- a/src/app/shared/components/horizental-profile-list/horizental-profile-list.component.html +++ /dev/null @@ -1,11 +0,0 @@ -
-
-
- @for (profile of profiles; track profile.id) { - - } @empty { -

Aucun profile trouvée

- } -
-
-
diff --git a/src/app/shared/components/horizental-profile-list/horizental-profile-list.component.scss b/src/app/shared/components/horizental-profile-list/horizental-profile-list.component.scss deleted file mode 100644 index e69de29..0000000 diff --git a/src/app/shared/components/horizental-profile-list/horizental-profile-list.component.spec.ts b/src/app/shared/components/horizental-profile-list/horizental-profile-list.component.spec.ts deleted file mode 100644 index 8f03679..0000000 --- a/src/app/shared/components/horizental-profile-list/horizental-profile-list.component.spec.ts +++ /dev/null @@ -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; - - beforeEach(async () => { - await TestBed.configureTestingModule({ - imports: [HorizentalProfileListComponent], - }).compileComponents(); - - fixture = TestBed.createComponent(HorizentalProfileListComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); - - it('should create', () => { - expect(component).toBeTruthy(); - }); -}); diff --git a/src/app/shared/components/horizental-profile-list/horizental-profile-list.component.ts b/src/app/shared/components/horizental-profile-list/horizental-profile-list.component.ts deleted file mode 100644 index 171bebd..0000000 --- a/src/app/shared/components/horizental-profile-list/horizental-profile-list.component.ts +++ /dev/null @@ -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[] = []; -} diff --git a/src/app/shared/components/my-profile-update-form/my-profile-update-form.component.spec.ts b/src/app/shared/components/my-profile-update-form/my-profile-update-form.component.spec.ts index a51312e..d499b4b 100644 --- a/src/app/shared/components/my-profile-update-form/my-profile-update-form.component.spec.ts +++ b/src/app/shared/components/my-profile-update-form/my-profile-update-form.component.spec.ts @@ -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; 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(); diff --git a/src/app/shared/components/my-profile-update-form/my-profile-update-form.component.ts b/src/app/shared/components/my-profile-update-form/my-profile-update-form.component.ts index 79df41a..ae2e85c 100644 --- a/src/app/shared/components/my-profile-update-form/my-profile-update-form.component.ts +++ b/src/app/shared/components/my-profile-update-form/my-profile-update-form.component.ts @@ -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([]); 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() { diff --git a/src/app/shared/components/vertical-profile-list/vertical-profile-list.component.spec.ts b/src/app/shared/components/vertical-profile-list/vertical-profile-list.component.spec.ts index 24276fd..2bd3ef1 100644 --- a/src/app/shared/components/vertical-profile-list/vertical-profile-list.component.spec.ts +++ b/src/app/shared/components/vertical-profile-list/vertical-profile-list.component.spec.ts @@ -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; - 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); diff --git a/src/app/testing/domain/sectors/fake-sector.repository.ts b/src/app/testing/domain/sectors/fake-sector.repository.ts new file mode 100644 index 0000000..06785c5 --- /dev/null +++ b/src/app/testing/domain/sectors/fake-sector.repository.ts @@ -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 { + const sector = fakeSectors.find((s) => s.id === sectorId) ?? ({} as Sector); + return of(sector); + } + + list(): Observable { + return of(fakeSectors); + } +} diff --git a/src/app/testing/infrastructure/profiles/pb-profile.repository.spec.ts b/src/app/testing/infrastructure/profiles/pb-profile.repository.spec.ts index f2b6e59..9738f89 100644 --- a/src/app/testing/infrastructure/profiles/pb-profile.repository.spec.ts +++ b/src/app/testing/infrastructure/profiles/pb-profile.repository.spec.ts @@ -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, diff --git a/src/app/testing/infrastructure/projects/pb-project.repository.spec.ts b/src/app/testing/infrastructure/projects/pb-project.repository.spec.ts index adc9ea0..5880841 100644 --- a/src/app/testing/infrastructure/projects/pb-project.repository.spec.ts +++ b/src/app/testing/infrastructure/projects/pb-project.repository.spec.ts @@ -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: '', diff --git a/src/app/testing/infrastructure/sectors/pb-sector.repository.spec.ts b/src/app/testing/infrastructure/sectors/pb-sector.repository.spec.ts new file mode 100644 index 0000000..9d11a92 --- /dev/null +++ b/src/app/testing/infrastructure/sectors/pb-sector.repository.spec.ts @@ -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 d’un 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]); + }); + }); +}); diff --git a/src/app/testing/sector.mock.ts b/src/app/testing/sector.mock.ts new file mode 100644 index 0000000..fe1b2db --- /dev/null +++ b/src/app/testing/sector.mock.ts @@ -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', + }, +]; diff --git a/src/app/testing/ui/sectors/sector.facade.spec.ts b/src/app/testing/ui/sectors/sector.facade.spec.ts new file mode 100644 index 0000000..0f5dc46 --- /dev/null +++ b/src/app/testing/ui/sectors/sector.facade.spec.ts @@ -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 d’un 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); + }); +}); diff --git a/src/app/testing/ui/sectors/sector.presenter.spec.ts b/src/app/testing/ui/sectors/sector.presenter.spec.ts new file mode 100644 index 0000000..4650340 --- /dev/null +++ b/src/app/testing/ui/sectors/sector.presenter.spec.ts @@ -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); + }); +}); diff --git a/src/app/testing/usecase/sectors/get-sector.usecase.spec.ts b/src/app/testing/usecase/sectors/get-sector.usecase.spec.ts new file mode 100644 index 0000000..b14aa80 --- /dev/null +++ b/src/app/testing/usecase/sectors/get-sector.usecase.spec.ts @@ -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)); + }); + }); +}); diff --git a/src/app/testing/usecase/sectors/list-sector.usecase.spec.ts b/src/app/testing/usecase/sectors/list-sector.usecase.spec.ts new file mode 100644 index 0000000..9a121fb --- /dev/null +++ b/src/app/testing/usecase/sectors/list-sector.usecase.spec.ts @@ -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); + }); + }); +}); diff --git a/src/app/ui/projects/project.facade.ts b/src/app/ui/projects/project.facade.ts index 6efa504..de36362 100644 --- a/src/app/ui/projects/project.facade.ts +++ b/src/app/ui/projects/project.facade.ts @@ -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) => { diff --git a/src/app/ui/sectors/sector.facade.ts b/src/app/ui/sectors/sector.facade.ts new file mode 100644 index 0000000..5a16a76 --- /dev/null +++ b/src/app/ui/sectors/sector.facade.ts @@ -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([]); + readonly sector = signal({} as SectorPresenterModel); + + readonly loading = signal({ isLoading: false, action: ActionType.NONE }); + readonly error = signal({ + 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 }); + } +} diff --git a/src/app/ui/sectors/sector.presenter.model.ts b/src/app/ui/sectors/sector.presenter.model.ts new file mode 100644 index 0000000..e8d1331 --- /dev/null +++ b/src/app/ui/sectors/sector.presenter.model.ts @@ -0,0 +1,5 @@ +export interface SectorPresenterModel { + id: string; + nom: string; + noms?: string[]; +} diff --git a/src/app/ui/sectors/sector.presenter.ts b/src/app/ui/sectors/sector.presenter.ts new file mode 100644 index 0000000..5db21b9 --- /dev/null +++ b/src/app/ui/sectors/sector.presenter.ts @@ -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); + } +} diff --git a/src/app/usecase/sectors/get-sector.usecase.ts b/src/app/usecase/sectors/get-sector.usecase.ts new file mode 100644 index 0000000..5888465 --- /dev/null +++ b/src/app/usecase/sectors/get-sector.usecase.ts @@ -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 { + return this.sectorRepository.getOne(sectorId); + } +} diff --git a/src/app/usecase/sectors/list-sector.usecase.ts b/src/app/usecase/sectors/list-sector.usecase.ts new file mode 100644 index 0000000..5372313 --- /dev/null +++ b/src/app/usecase/sectors/list-sector.usecase.ts @@ -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 { + return this.sectorRepository.list(); + } +}