Merge pull request 'feat : #8 inverser avec #7 , suppression d'un projet' (#11) from ttp-8 into main
All checks were successful
Build Check / build (push) Successful in 2m15s

Reviewed-on: #11
Reviewed-by: technostrea <contact@technostrea.fr>
This commit is contained in:
2025-12-26 11:04:13 +00:00
17 changed files with 433 additions and 56 deletions

View File

@@ -1,9 +1,9 @@
import { inject, Injectable, signal } from '@angular/core';
import { PROJECT_REPOSITORY_TOKEN } from '@app/infrastructure/projects/project-repository.token';
import { CreateProjectUseCase } from '../../application/projects/create-project.usecase';
import { ListProjectUseCase } from '../../application/projects/list-project.usecase';
import { GetProjectUseCase } from '../../application/projects/get-project.usecase';
import { UpdateProjectUseCase } from '../../application/projects/update-project.usecase';
import { CreateProjectUseCase } from '@app/application/projects/create-project.usecase';
import { ListProjectUseCase } from '@app/application/projects/list-project.usecase';
import { GetProjectUseCase } from '@app/application/projects/get-project.usecase';
import { UpdateProjectUseCase } from '@app/application/projects/update-project.usecase';
import { Project } from '@app/domain/projects/project.model';
import { ProjectViewModel } from '../projects/project.presenter.model';
import { ProjectPresenter } from '../projects/project.presenter';
@@ -13,6 +13,7 @@ import { ActionType } from '@app/domain/action-type.util';
import { LoaderAction } from '@app/domain/loader-action.util';
import { first, Subscription } from 'rxjs';
import { FeedbackService } from '../shared/services/feedback.service';
import { DeleteProjectUseCase } from '@app/application/projects/delete-project.usecase';
@Injectable({
providedIn: 'root',
@@ -24,7 +25,8 @@ export class ProjectFacade {
private readonly createUseCase = new CreateProjectUseCase(this.projectRepo);
private readonly listUseCase = new ListProjectUseCase(this.projectRepo);
private readonly getUseCase = new GetProjectUseCase(this.projectRepo);
private readonly UpdateUseCase = new UpdateProjectUseCase(this.projectRepo);
private readonly updateUseCase = new UpdateProjectUseCase(this.projectRepo);
private readonly deleteUseCase = new DeleteProjectUseCase(this.projectRepo);
readonly projects = signal<ProjectViewModel[]>([]);
readonly project = signal<ProjectViewModel>({} as ProjectViewModel);
@@ -101,7 +103,8 @@ export class ProjectFacade {
update(userId: string, data: any) {
this.handleError(ActionType.UPDATE, false, null, true);
this.UpdateUseCase.execute(userId, data)
this.updateUseCase
.execute(userId, data)
.pipe(first())
.subscribe({
next: (project: Project) => {
@@ -117,6 +120,26 @@ export class ProjectFacade {
});
}
delete(projectId: string) {
this.handleError(ActionType.DELETE, false, null, true);
this.deleteUseCase
.execute(projectId)
.pipe(first())
.subscribe({
next: (res: boolean) => {
if (res) {
this.handleError(ActionType.UPDATE, false, null, false);
const message = `Le projet a bien été supprimé !`;
this.feedbackService.notify(ActionType.UPDATE, message);
}
},
error: (err) => {
this.handleError(ActionType.UPDATE, false, err, false);
},
});
}
private handleError(
action: ActionType = ActionType.NONE,
hasError: boolean,

View File

@@ -0,0 +1,10 @@
import { ProjectRepository } from '@app/domain/projects/project.repository';
import { Observable } from 'rxjs';
export class DeleteProjectUseCase {
constructor(private readonly repo: ProjectRepository) {}
execute(projectId: string): Observable<boolean> {
return this.repo.delete(projectId);
}
}

View File

@@ -7,4 +7,5 @@ export interface ProjectRepository {
list(userId: string): Observable<Project[]>;
get(projectId: string): Observable<Project>;
update(id: string, data: Project | any): Observable<Project>;
delete(id: string): Observable<boolean>;
}

View File

@@ -13,7 +13,7 @@ export class PbProjectRepository implements ProjectRepository {
private pb = new PocketBase(environment.baseUrl);
create(project: CreateProjectDto): Observable<Project> {
return from(this.pb.collection('projets').create<Project>(project));
return from(this.pb.collection<Project>('projets').create<Project>(project));
}
list(userId: string): Observable<Project[]> {
return from(
@@ -24,6 +24,9 @@ export class PbProjectRepository implements ProjectRepository {
return from(this.pb.collection<Project>('projets').getOne<Project>(projectId));
}
update(id: string, data: any): Observable<Project> {
return from(this.pb.collection('projets').update<Project>(id, data));
return from(this.pb.collection<Project>('projets').update<Project>(id, data));
}
delete(id: string): Observable<boolean> {
return from(this.pb.collection<Project>('projets').delete(id));
}
}

View File

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

View File

@@ -0,0 +1,13 @@
import { Component } from '@angular/core';
import { ActionType } from '@app/domain/action-type.util';
@Component({
selector: 'app-dialog-box',
standalone: true,
imports: [],
templateUrl: './dialog-box.component.html',
styleUrl: './dialog-box.component.scss',
})
export class DialogBoxComponent {
protected readonly ActionType = ActionType;
}

View File

@@ -142,7 +142,7 @@
<div class="flex flex-col sm:flex-row gap-3 pt-4">
<button
type="submit"
[disabled]="projectForm.invalid"
[disabled]="projectForm.invalid || loading().isLoading"
class="flex-1 py-3 px-4 bg-gradient-to-r from-indigo-600 to-purple-600 hover:from-indigo-700 hover:to-purple-700 text-white font-medium rounded-lg shadow-lg hover:shadow-xl transform hover:-translate-y-0.5 transition-all duration-200 disabled:opacity-50 disabled:cursor-not-allowed disabled:transform-none"
>
<span class="flex items-center justify-center gap-2">
@@ -156,10 +156,35 @@
d="M7.707 10.293a1 1 0 10-1.414 1.414l3 3a1 1 0 001.414 0l3-3a1 1 0 00-1.414-1.414L11 11.586V6h5a2 2 0 012 2v7a2 2 0 01-2 2H4a2 2 0 01-2-2V8a2 2 0 012-2h5v5.586l-1.293-1.293zM9 4a1 1 0 012 0v2H9V4z"
/>
</svg>
Sauvegarder
{{ isEditMode ? 'Mettre à jour' : 'Créer le projet' }}
</span>
</button>
@if (isEditMode) {
<button
type="button"
(click)="openDeleteDialog()"
[disabled]="isDeleteDisabled"
class="sm:w-auto py-3 px-6 bg-red-600 hover:bg-red-700 text-white font-medium rounded-lg transition-all duration-200 disabled:opacity-50 disabled:cursor-not-allowed"
>
<span class="flex items-center justify-center gap-2">
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-5 w-5"
viewBox="0 0 20 20"
fill="currentColor"
>
<path
fill-rule="evenodd"
d="M9 2a1 1 0 00-.894.553L7.382 4H4a1 1 0 000 2v10a2 2 0 002 2h8a2 2 0 002-2V6a1 1 0 100-2h-3.382l-.724-1.447A1 1 0 0011 2H9zM7 8a1 1 0 012 0v6a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v6a1 1 0 102 0V8a1 1 0 00-1-1z"
clip-rule="evenodd"
/>
</svg>
Supprimer
</span>
</button>
}
<button
type="button"
(click)="formIsUpdated.emit(null)"
@@ -187,3 +212,118 @@
</div>
</div>
}
<!-- Boîte de dialogue de confirmation de suppression -->
@if (showDeleteDialog()) {
<div
class="fixed inset-0 z-50 overflow-y-auto"
aria-labelledby="modal-title"
role="dialog"
aria-modal="true"
>
<!-- Overlay -->
<div
class="flex items-center justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0"
>
<div
class="fixed inset-0 bg-gray-500 dark:bg-gray-900 bg-opacity-75 dark:bg-opacity-75 transition-opacity"
(click)="closeDeleteDialog()"
aria-hidden="true"
></div>
<!-- Centre le modal -->
<span class="hidden sm:inline-block sm:align-middle sm:h-screen" aria-hidden="true"
>&#8203;</span
>
<!-- Contenu du modal -->
<div
class="inline-block align-bottom bg-white dark:bg-gray-800 rounded-lg text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-lg sm:w-full"
>
<div class="bg-white dark:bg-gray-800 px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
<div class="sm:flex sm:items-start">
<div
class="mx-auto flex-shrink-0 flex items-center justify-center h-12 w-12 rounded-full bg-red-100 dark:bg-red-900/20 sm:mx-0 sm:h-10 sm:w-10"
>
<svg
class="h-6 w-6 text-red-600 dark:text-red-400"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"
/>
</svg>
</div>
<div class="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
<h3
class="text-lg leading-6 font-medium text-gray-900 dark:text-white"
id="modal-title"
>
Supprimer le projet
</h3>
<div class="mt-2">
<p class="text-sm text-gray-500 dark:text-gray-400">
Êtes-vous sûr de vouloir supprimer le projet
<strong class="text-gray-900 dark:text-white">{{ project().nom }}</strong> ? Cette
action est irréversible et toutes les données associées seront définitivement
supprimées.
</p>
</div>
</div>
</div>
</div>
<div
class="bg-gray-50 dark:bg-gray-700 px-4 py-3 sm:px-6 sm:flex sm:flex-row-reverse gap-3"
>
<button
type="button"
(click)="confirmDelete()"
[disabled]="loading().isLoading && loading().action === ActionType.DELETE"
class="w-full inline-flex justify-center rounded-lg border border-transparent shadow-sm px-4 py-2 bg-red-600 hover:bg-red-700 text-base font-medium text-white focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 sm:ml-3 sm:w-auto sm:text-sm disabled:opacity-50 disabled:cursor-not-allowed transition-all"
>
@if (loading().isLoading && loading().action === ActionType.DELETE) {
<svg
class="animate-spin -ml-1 mr-2 h-4 w-4 text-white"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
>
<circle
class="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
stroke-width="4"
></circle>
<path
class="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
></path>
</svg>
Suppression...
} @else {
Supprimer
}
</button>
<button
type="button"
(click)="closeDeleteDialog()"
[disabled]="loading().isLoading && loading().action === ActionType.DELETE"
class="mt-3 w-full inline-flex justify-center rounded-lg border border-gray-300 dark:border-gray-600 shadow-sm px-4 py-2 bg-white dark:bg-gray-800 text-base font-medium text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 sm:mt-0 sm:ml-3 sm:w-auto sm:text-sm disabled:opacity-50 disabled:cursor-not-allowed transition-all"
>
Annuler
</button>
</div>
</div>
</div>
</div>
}

View File

@@ -3,47 +3,145 @@ import { ComponentFixture, TestBed } from '@angular/core/testing';
import { MyProfileUpdateProjectFormComponent } from './my-profile-update-project-form.component';
import { ToastrService } from 'ngx-toastr';
import { provideRouter } from '@angular/router';
import { PROJECT_REPOSITORY_TOKEN } from '@app/infrastructure/projects/project-repository.token';
import { ProjectRepository } from '@app/domain/projects/project.repository';
import { AUTH_REPOSITORY_TOKEN } from '@app/infrastructure/authentification/auth-repository.token';
import { PROFILE_REPOSITORY_TOKEN } from '@app/infrastructure/profiles/profile-repository.token';
import { AuthRepository } from '@app/domain/authentification/auth.repository';
import { ProfileRepository } from '@app/domain/profiles/profile.repository';
import { mockAuthRepo } from '@app/testing/auth.mock';
import { mockProfileRepo } from '@app/testing/profile.mock';
import { mockProjectRepo } from '@app/testing/project.mock';
import { mockProjects } from '@app/testing/project.mock';
import { mockToastR } from '@app/testing/toastr.mock';
import { ProjectViewModel } from '@app/adapters/projects/project.presenter.model';
import { ActionType } from '@app/domain/action-type.util';
import { ProjectFacade } from '@app/adapters/projects/project.facade';
import { AuthFacade } from '@app/adapters/authentification/auth.facade';
import { mockAuthenticationFacade } from '@app/testing/adapters/authentification/auth.facade.mock';
import { mockProjectFac } from '@app/testing/adapters/projects/project.facade.mock';
describe('MyProfileUpdateProjectFormComponent', () => {
let component: MyProfileUpdateProjectFormComponent;
let fixture: ComponentFixture<MyProfileUpdateProjectFormComponent>;
let mockToastrService: jest.Mocked<Partial<ToastrService>> = mockToastR;
let mockProjectRepository: jest.Mocked<Partial<ProjectRepository>> = mockProjectRepo;
// 1. Mock ProjectFacade
const mockProjectFacade = mockProjectFac;
let mockAuthRepository: jest.Mocked<Partial<AuthRepository>> = mockAuthRepo;
let mockProfileRepository: jest.Mocked<Partial<ProfileRepository>> = mockProfileRepo;
// 2. Mock AuthFacade
const mockAuthFacade = mockAuthenticationFacade;
// Donnée de test
const mockProjectData: ProjectViewModel = mockProjects[0] as ProjectViewModel;
beforeEach(async () => {
// Reset des mocks
mockProjectFacade.project.set(undefined);
mockProjectFacade.loading.set({ isLoading: false, action: ActionType.NONE });
mockProjectFacade.error.set({ hasError: false, message: null });
jest.clearAllMocks();
await TestBed.configureTestingModule({
imports: [MyProfileUpdateProjectFormComponent],
providers: [
provideRouter([]),
{ provide: ToastrService, useValue: mockToastrService },
{ provide: PROJECT_REPOSITORY_TOKEN, useValue: mockProjectRepository },
{ provide: AUTH_REPOSITORY_TOKEN, useValue: mockAuthRepository },
{ provide: PROFILE_REPOSITORY_TOKEN, useValue: mockProfileRepository },
{ provide: ToastrService, useValue: mockToastR },
{ provide: ProjectFacade, useValue: mockProjectFacade },
{ provide: AuthFacade, useValue: mockAuthFacade },
],
}).compileComponents();
fixture = TestBed.createComponent(MyProfileUpdateProjectFormComponent);
component = fixture.componentInstance;
// Initialisation de l'input requis
fixture.componentRef.setInput('projectId', 'fakeId');
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
describe('Mode création (projectId = "add")', () => {
beforeEach(() => {
fixture.componentRef.setInput('projectId', 'add');
fixture.detectChanges();
});
it('devrait créer un nouveau projet lors de la soumission', () => {
component.projectForm.patchValue({
nom: 'Nouveau Projet',
description: 'Description',
lien: 'https://nouveau.com',
});
component.onSubmit();
expect(mockProjectFacade.create).toHaveBeenCalled();
const callArgs = mockProjectFacade.create.mock.calls[0][0];
expect(callArgs.nom).toBe('Nouveau Projet');
expect(callArgs.utilisateur).toBe('user_001');
});
});
describe('Mode édition (projectId = ID valide)', () => {
beforeEach(() => {
// On simule que la façade a chargé un projet
mockProjectFacade.project.set(mockProjectData);
fixture.componentRef.setInput('projectId', '1');
fixture.detectChanges();
});
it('devrait charger le projet existant', () => {
// L'appel doit avoir été fait grâce à l'effect qui détecte projectId='project-123'
expect(mockProjectFacade.loadOne).toHaveBeenCalledWith('1');
});
it('devrait remplir le formulaire avec les données du projet', () => {
// Comme on a set le signal 'project' dans le beforeEach, le formulaire doit être rempli
expect(component.projectForm.get('nom')?.value).toBe('Portfolio Web 3D');
expect(component.projectForm.get('description')?.value).toBe(
'Un site web interactif utilisant Three.js et Angular 17 pour présenter un portfolio 3D.'
);
});
it('devrait mettre à jour le projet lors de la soumission', () => {
component.projectForm.patchValue({
nom: 'Projet Modifié',
});
component.onSubmit();
expect(mockProjectFacade.update).toHaveBeenCalledWith(
'1',
expect.objectContaining({
nom: 'Projet Modifié',
})
);
});
});
describe('Suppression du projet', () => {
beforeEach(() => {
mockProjectFacade.project.set(mockProjectData);
fixture.componentRef.setInput('projectId', '1');
fixture.detectChanges();
});
it('devrait confirmer et exécuter la suppression', () => {
component.confirmDelete();
expect(mockProjectFacade.delete).toHaveBeenCalledWith('1');
});
});
describe('Événements de sortie', () => {
it('devrait émettre formIsUpdated après suppression réussie', () => {
// CORRECTION : On définit un projet pour que le template puisse lire 'project().nom' sans planter
mockProjectFacade.project.set({ id: '123', nom: 'Projet à supprimer' } as any);
const spy = jest.fn();
component.formIsUpdated.subscribe(spy);
// On ouvre la modale (qui affiche surement le nom du projet)
component.showDeleteDialog.set(true);
// 1. Simuler la fin de suppression (succès) via le signal loading
mockProjectFacade.loading.set({ isLoading: false, action: ActionType.DELETE });
mockProjectFacade.error.set({ hasError: false, message: null });
// C'est ici que ça plantait car le template essayait d'afficher le nom d'un projet undefined
fixture.detectChanges();
expect(component.showDeleteDialog()).toBe(false);
expect(spy).toHaveBeenCalledWith(null);
});
});
});

View File

@@ -1,4 +1,4 @@
import { Component, effect, inject, input, output } from '@angular/core';
import { Component, effect, inject, input, output, signal } from '@angular/core';
import { FormBuilder, FormControl, ReactiveFormsModule, Validators } from '@angular/forms';
import { PaginatorModule } from 'primeng/paginator';
import { ProjectPictureFormComponent } from '@app/shared/components/project-picture-form/project-picture-form.component';
@@ -18,7 +18,7 @@ import { AuthFacade } from '@app/adapters/authentification/auth.facade';
export class MyProfileUpdateProjectFormComponent {
projectId = input.required<string | null>();
private readonly projectFacade = new ProjectFacade();
private readonly projectFacade = inject(ProjectFacade);
protected readonly ActionType = ActionType;
protected readonly project = this.projectFacade.project;
protected readonly loading = this.projectFacade.loading;
@@ -29,7 +29,7 @@ export class MyProfileUpdateProjectFormComponent {
private readonly formBuilder = inject(FormBuilder);
protected projectForm = this.formBuilder.group({
projectForm = this.formBuilder.group({
nom: new FormControl('', [Validators.required]),
description: new FormControl('', [Validators.required]),
lien: new FormControl(''),
@@ -37,25 +37,37 @@ export class MyProfileUpdateProjectFormComponent {
formIsUpdated = output<string | null>();
constructor() {
effect(() => {
if (!this.loading().isLoading) {
switch (this.loading().action) {
case ActionType.CREATE:
break;
case ActionType.UPDATE:
break;
}
// Gestion de la boîte de dialogue de suppression
showDeleteDialog = signal<boolean>(false);
if (this.project() !== undefined) {
this.projectForm.setValue({
nom: this.project().nom ?? '',
description: this.project().description ?? '',
lien: this.project().lien ?? '',
});
constructor() {
effect(
() => {
if (!this.loading().isLoading) {
switch (this.loading().action) {
case ActionType.CREATE:
break;
case ActionType.UPDATE:
break;
case ActionType.DELETE:
if (!this.error().hasError) {
this.showDeleteDialog.set(false);
this.formIsUpdated.emit(null);
}
break;
}
if (this.project() !== undefined) {
this.projectForm.setValue({
nom: this.project().nom ?? '',
description: this.project().description ?? '',
lien: this.project().lien ?? '',
});
}
}
}
});
},
{ allowSignalWrites: true }
);
effect(
() => {
@@ -96,4 +108,26 @@ export class MyProfileUpdateProjectFormComponent {
this.projectFacade.create(projectDto);
}
}
openDeleteDialog(): void {
this.showDeleteDialog.set(true);
}
closeDeleteDialog(): void {
this.showDeleteDialog.set(false);
}
confirmDelete(): void {
if (this.project()?.id) {
this.projectFacade.delete(this.project().id);
}
}
get isEditMode(): boolean {
return this.projectId() !== null && this.projectId() !== 'add'.toLowerCase();
}
get isDeleteDisabled(): boolean {
return !this.isEditMode || this.loading().isLoading;
}
}

View File

@@ -4,9 +4,9 @@ import { ProjectPictureFormComponent } from './project-picture-form.component';
import { mockProjects } from '@app/testing/project.mock';
import { mockFileManagerSvc } from '@app/testing/file-manager.service.mock';
import { mockProjectFac } from '@app/testing/adapters/projects/project.facade.mock';
import { ProjectViewModel } from '../../../adapters/projects/project.presenter.model';
import { ProjectFacade } from '../../../adapters/projects/project.facade';
import { FileManagerService } from '../../../adapters/shared/services/file-manager.service';
import { ProjectViewModel } from '@app/adapters/projects/project.presenter.model';
import { ProjectFacade } from '@app/adapters/projects/project.facade';
import { FileManagerService } from '@app/adapters/shared/services/file-manager.service';
import { ActionType } from '@app/domain/action-type.util';
describe('ProjectPictureFormComponent', () => {
@@ -30,7 +30,7 @@ describe('ProjectPictureFormComponent', () => {
mockProjectFacade.update.mockClear();
mockProjectFacade.loading.set({ isLoading: false, action: ActionType.NONE });
mockProjectFacade.error.set({ hasError: false });
mockProjectFacade.error.set({ hasError: false, message: null });
await TestBed.configureTestingModule({
imports: [ProjectPictureFormComponent],

View File

@@ -1,8 +1,10 @@
import { signal } from '@angular/core';
import { ActionType } from '@app/domain/action-type.util';
import { mockUsers } from '@app/testing/user.mock';
export const mockAuthenticationFacade = {
sendRequestPasswordReset: jest.fn(),
loading: signal({ isLoading: false, action: ActionType.NONE }),
error: signal({ hasError: false }),
user: signal(mockUsers[0]),
};

View File

@@ -1,8 +1,13 @@
import { signal } from '@angular/core';
import { ActionType } from '@app/domain/action-type.util';
import { ProjectViewModel } from '@app/adapters/projects/project.presenter.model';
export const mockProjectFac = {
update: jest.fn(),
project: signal<ProjectViewModel | undefined>(undefined),
loading: signal({ isLoading: false, action: ActionType.NONE }),
error: signal({ hasError: false }),
error: signal({ hasError: false, message: null }),
loadOne: jest.fn(),
create: jest.fn(),
update: jest.fn(),
delete: jest.fn(),
};

View File

@@ -0,0 +1,20 @@
import { FakeProjectRepository } from '@app/testing/domain/projects/fake-project.repository';
import { DeleteProjectUseCase } from '@app/application/projects/delete-project.usecase';
describe('DeleteProjectUseCase', () => {
let useCase: DeleteProjectUseCase;
let repo: FakeProjectRepository;
beforeEach(() => {
repo = new FakeProjectRepository();
useCase = new DeleteProjectUseCase(repo);
});
it('doit retourné le vrai si le projet est supprimé', (done) => {
const projectId = '1';
useCase.execute(projectId).subscribe((project) => {
expect(project).toBe(true);
done();
});
});
});

View File

@@ -42,4 +42,9 @@ export class FakeProjectRepository implements ProjectRepository {
this.projects[index] = updated;
return of(updated);
}
delete(id: string): Observable<boolean> {
const projects = this.projects.filter((currentProject) => currentProject.id != id);
return of(projects.length != this.projects.length);
}
}

View File

@@ -64,4 +64,5 @@ export const mockProjectRepo = {
list: jest.fn().mockReturnValue(of([])),
get: jest.fn().mockReturnValue(of({} as Project)),
update: jest.fn().mockReturnValue(of({} as Project)),
delete: jest.fn().mockReturnValue(of(true)),
};