diff --git a/backend/src/modules/cards/applications/merge-card.use-case.spec.ts b/backend/src/modules/cards/applications/merge-card.use-case.spec.ts new file mode 100644 index 000000000..480b3a4d9 --- /dev/null +++ b/backend/src/modules/cards/applications/merge-card.use-case.spec.ts @@ -0,0 +1,112 @@ +import { DeepMocked, createMock } from '@golevelup/ts-jest'; +import { Test, TestingModule } from '@nestjs/testing'; +import { CardRepositoryInterface } from '../repository/card.repository.interface'; +import { MergeCardUseCase } from './merge-card.use-case'; +import { TYPES } from '../interfaces/types'; +import { GetCardServiceInterface } from '../interfaces/services/get.card.service.interface'; +import MergeCardUseCaseDto from '../dto/useCase/merge-card.use-case.dto'; +import faker from '@faker-js/faker'; +import { BadRequestException } from '@nestjs/common'; +import { CardFactory } from 'src/libs/test-utils/mocks/factories/card-factory.mock'; +import { UpdateResult } from 'mongodb'; +import { CardItemFactory } from 'src/libs/test-utils/mocks/factories/cardItem-factory.mock'; + +const mergeCardDtoMock: MergeCardUseCaseDto = { + boardId: faker.datatype.uuid(), + draggedCardId: faker.datatype.uuid(), + targetCardId: faker.datatype.uuid() +}; +const cardMock = CardFactory.createMany(2, [ + { items: CardItemFactory.createMany(1) }, + { items: CardItemFactory.createMany(1) } +]); +const updateResult: UpdateResult = { + acknowledged: true, + matchedCount: 1, + modifiedCount: 1, + upsertedCount: 1, + upsertedId: null +}; + +describe('MergeCardUseCase', () => { + let useCase: MergeCardUseCase; + let cardRepositoryMock: DeepMocked; + let getCardServiceMock: DeepMocked; + beforeAll(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + MergeCardUseCase, + { + provide: TYPES.services.GetCardService, + useValue: createMock() + }, + { + provide: TYPES.repository.CardRepository, + useValue: createMock() + } + ] + }).compile(); + useCase = module.get(MergeCardUseCase); + getCardServiceMock = module.get(TYPES.services.GetCardService); + cardRepositoryMock = module.get(TYPES.repository.CardRepository); + + getCardServiceMock.getCardFromBoard + .mockResolvedValue(cardMock[0]) + .mockResolvedValue(cardMock[1]); + cardRepositoryMock.pullCard.mockResolvedValue(updateResult); + }); + + beforeEach(() => { + jest.clearAllMocks(); + jest.restoreAllMocks(); + }); + + it('should be defined', () => { + expect(useCase).toBeDefined(); + }); + it('should ', async () => { + await useCase.execute(mergeCardDtoMock); + await expect(cardRepositoryMock.updateCardOnMerge).toHaveBeenNthCalledWith( + 1, + mergeCardDtoMock.boardId, + mergeCardDtoMock.targetCardId, + expect.anything(), + expect.anything(), + expect.anything(), + true + ); + }); + + it('should throw badRequest if getCardFromBoard not found', async () => { + getCardServiceMock.getCardFromBoard.mockResolvedValueOnce(null); + await expect(useCase.execute(mergeCardDtoMock)).rejects.toThrow(BadRequestException); + }); + + it('should throw badRequest if repository pullCard not found', async () => { + cardRepositoryMock.pullCard.mockResolvedValueOnce(null); + await expect(useCase.execute(mergeCardDtoMock)).rejects.toThrow(BadRequestException); + }); + + it('should throw badRequest if getCardFromBoard not found', async () => { + getCardServiceMock.getCardFromBoard + .mockResolvedValueOnce(cardMock[0]) + .mockResolvedValueOnce(null); + await expect(useCase.execute(mergeCardDtoMock)).rejects.toThrow(BadRequestException); + }); + + it('should throw badRequest if pullResult.modifiedCount different then 1', async () => { + updateResult.modifiedCount = 2; + await expect(useCase.execute(mergeCardDtoMock)).rejects.toThrow(BadRequestException); + updateResult.modifiedCount = 1; + }); + + it('should throw badRequest if updateCardMerge fails', async () => { + cardRepositoryMock.updateCardOnMerge.mockResolvedValue(null); + await expect(useCase.execute(mergeCardDtoMock)).rejects.toThrow(BadRequestException); + }); + + it('should throw badRequest with default message when a non expected error occurs', async () => { + cardRepositoryMock.updateCardOnMerge.mockRejectedValueOnce(Error); + await expect(useCase.execute(mergeCardDtoMock)).rejects.toThrow(BadRequestException); + }); +}); diff --git a/backend/src/modules/cards/services/merge.card.service.ts b/backend/src/modules/cards/applications/merge-card.use-case.ts similarity index 54% rename from backend/src/modules/cards/services/merge.card.service.ts rename to backend/src/modules/cards/applications/merge-card.use-case.ts index 8e3983e2d..6c0ad2597 100644 --- a/backend/src/modules/cards/services/merge.card.service.ts +++ b/backend/src/modules/cards/applications/merge-card.use-case.ts @@ -1,11 +1,14 @@ -import { Inject } from '@nestjs/common'; -import { CARD_NOT_FOUND, CARD_NOT_REMOVED, UPDATE_FAILED } from 'src/libs/exceptions/messages'; +import { Inject, Injectable } from '@nestjs/common'; +import { UseCase } from 'src/libs/interfaces/use-case.interface'; import { GetCardServiceInterface } from '../interfaces/services/get.card.service.interface'; -import { MergeCardServiceInterface } from '../interfaces/services/merge.card.service.interface'; -import { TYPES } from '../interfaces/types'; import { CardRepositoryInterface } from '../repository/card.repository.interface'; +import { TYPES } from '../interfaces/types'; +import MergeCardUseCaseDto from '../dto/useCase/merge-card.use-case.dto'; +import { CARD_NOT_FOUND, CARD_NOT_REMOVED, UPDATE_FAILED } from 'src/libs/exceptions/messages'; +import { UpdateFailedException } from 'src/libs/exceptions/updateFailedBadRequestException'; -export class MergeCardService implements MergeCardServiceInterface { +@Injectable() +export class MergeCardUseCase implements UseCase { constructor( @Inject(TYPES.services.GetCardService) private readonly getCardService: GetCardServiceInterface, @@ -13,35 +16,27 @@ export class MergeCardService implements MergeCardServiceInterface { private readonly cardRepository: CardRepositoryInterface ) {} - // eslint-disable-next-line @typescript-eslint/no-unused-vars - async mergeCards(boardId: string, draggedCardId: string, cardId: string) { + async execute(mergeCardUseCaseDto: MergeCardUseCaseDto) { + const { boardId, draggedCardId, targetCardId } = mergeCardUseCaseDto; await this.cardRepository.startTransaction(); - try { const cardToMove = await this.getCardService.getCardFromBoard(boardId, draggedCardId); - if (!cardToMove) return null; + if (!cardToMove) throw Error(CARD_NOT_FOUND); const pullResult = await this.cardRepository.pullCard(boardId, draggedCardId, true); if (pullResult.modifiedCount !== 1) throw Error(CARD_NOT_REMOVED); - const cardGroup = await this.getCardService.getCardFromBoard(boardId, cardId); + const cardGroup = await this.getCardService.getCardFromBoard(boardId, targetCardId); if (!cardGroup) throw Error(CARD_NOT_FOUND); - const { items, comments, votes } = cardToMove; - const newItems = cardGroup.items.concat(items); - - const newVotes = (cardGroup.votes as unknown as string[]).concat( - votes as unknown as string[] - ); - - const newComments = cardGroup.comments.concat(comments); + const { newItems, newVotes, newComments } = this.concatCards(cardToMove, cardGroup); const updateCard = await this.cardRepository.updateCardOnMerge( boardId, - cardId, + targetCardId, newItems, newVotes, newComments, @@ -54,10 +49,20 @@ export class MergeCardService implements MergeCardServiceInterface { return true; } catch (e) { await this.cardRepository.abortTransaction(); + throw new UpdateFailedException(e.message ? e.message : UPDATE_FAILED); } finally { await this.cardRepository.endSession(); } + } + + private concatCards(cardToMove, cardGroup) { + const { items, comments, votes } = cardToMove; + const newItems = cardGroup.items.concat(items); + + const newVotes = (cardGroup.votes as unknown as string[]).concat(votes as unknown as string[]); + + const newComments = cardGroup.comments.concat(comments); - return false; + return { newItems, newVotes, newComments }; } } diff --git a/backend/src/modules/cards/applications/merge.card.application.ts b/backend/src/modules/cards/applications/merge.card.application.ts deleted file mode 100644 index f7ef46898..000000000 --- a/backend/src/modules/cards/applications/merge.card.application.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { Inject, Injectable } from '@nestjs/common'; -import { MergeCardApplicationInterface } from '../interfaces/applications/merge.card.application.interface'; -import { MergeCardServiceInterface } from '../interfaces/services/merge.card.service.interface'; -import { TYPES } from '../interfaces/types'; - -@Injectable() -export class MergeCardApplication implements MergeCardApplicationInterface { - constructor( - @Inject(TYPES.services.MergeCardService) - private mergeCardService: MergeCardServiceInterface - ) {} - - mergeCards(boardId: string, draggedCardId: string, cardId: string) { - return this.mergeCardService.mergeCards(boardId, draggedCardId, cardId); - } -} diff --git a/backend/src/modules/cards/cards.module.ts b/backend/src/modules/cards/cards.module.ts index b7aee5f7c..b8773459d 100644 --- a/backend/src/modules/cards/cards.module.ts +++ b/backend/src/modules/cards/cards.module.ts @@ -8,8 +8,7 @@ import { deleteCardApplication, deleteCardService, getCardService, - mergeCardApplication, - mergeCardService, + mergeCardUseCase, unmergeCardApplication, unmergeCardService, updateCardApplication, @@ -25,14 +24,13 @@ import CardsController from './controller/cards.controller'; getCardService, deleteCardService, updateCardService, - mergeCardService, unmergeCardService, updateCardApplication, deleteCardApplication, - mergeCardApplication, unmergeCardApplication, cardRepository, - creacteCardUseCase + creacteCardUseCase, + mergeCardUseCase ], exports: [getCardService, deleteCardService] }) diff --git a/backend/src/modules/cards/cards.providers.ts b/backend/src/modules/cards/cards.providers.ts index 7e0221693..09cb92743 100644 --- a/backend/src/modules/cards/cards.providers.ts +++ b/backend/src/modules/cards/cards.providers.ts @@ -1,13 +1,12 @@ import { CreateCardUseCase } from './applications/create-card.use-case'; import { DeleteCardApplication } from './applications/delete.card.application'; -import { MergeCardApplication } from './applications/merge.card.application'; +import { MergeCardUseCase } from './applications/merge-card.use-case'; import { UnmergeCardApplication } from './applications/unmerge.card.application'; import { UpdateCardApplication } from './applications/update.card.application'; import { TYPES } from './interfaces/types'; import { CardRepository } from './repository/card.repository'; import DeleteCardService from './services/delete.card.service'; import GetCardService from './services/get.card.service'; -import { MergeCardService } from './services/merge.card.service'; import { UnmergeCardService } from './services/unmerge.card.service'; import UpdateCardService from './services/update.card.service'; @@ -26,11 +25,6 @@ export const deleteCardService = { useClass: DeleteCardService }; -export const mergeCardService = { - provide: TYPES.services.MergeCardService, - useClass: MergeCardService -}; - export const unmergeCardService = { provide: TYPES.services.UnmergeCardService, useClass: UnmergeCardService @@ -46,11 +40,6 @@ export const deleteCardApplication = { useClass: DeleteCardApplication }; -export const mergeCardApplication = { - provide: TYPES.applications.MergeCardApplication, - useClass: MergeCardApplication -}; - export const unmergeCardApplication = { provide: TYPES.applications.UnmergeCardApplication, useClass: UnmergeCardApplication @@ -65,3 +54,8 @@ export const creacteCardUseCase = { provide: TYPES.applications.CreateCardUseCase, useClass: CreateCardUseCase }; + +export const mergeCardUseCase = { + provide: TYPES.applications.MergeCardUseCase, + useClass: MergeCardUseCase +}; diff --git a/backend/src/modules/cards/controller/cards.controller.ts b/backend/src/modules/cards/controller/cards.controller.ts index fcafe564e..063668562 100644 --- a/backend/src/modules/cards/controller/cards.controller.ts +++ b/backend/src/modules/cards/controller/cards.controller.ts @@ -45,11 +45,12 @@ import { TYPES } from '../interfaces/types'; import { MergeCardDto } from '../dto/group/merge.card.dto'; import { UpdateCardApplicationInterface } from '../interfaces/applications/update.card.application.interface'; import { DeleteCardApplicationInterface } from '../interfaces/applications/delete.card.application.interface'; -import { MergeCardApplicationInterface } from '../interfaces/applications/merge.card.application.interface'; import { UnmergeCardApplicationInterface } from '../interfaces/applications/unmerge.card.application.interface'; import CreateCardUseCaseDto from '../dto/useCase/create-card.use-case.dto'; import { UseCase } from 'src/libs/interfaces/use-case.interface'; import CardCreationPresenter from '../dto/useCase/presenters/create-card-res.use-case.dto'; +import MergeCardUseCaseDto from '../dto/useCase/merge-card.use-case.dto'; + @ApiBearerAuth('access-token') @ApiTags('Cards') @@ -63,8 +64,8 @@ export default class CardsController { private updateCardApp: UpdateCardApplicationInterface, @Inject(TYPES.applications.DeleteCardApplication) private deleteCardApp: DeleteCardApplicationInterface, - @Inject(TYPES.applications.MergeCardApplication) - private mergeCardApp: MergeCardApplicationInterface, + @Inject(TYPES.applications.MergeCardUseCase) + private mergeCardUseCase: UseCase, @Inject(TYPES.applications.UnmergeCardApplication) private unmergeCardApp: UnmergeCardApplicationInterface, private socketService: SocketGateway @@ -330,10 +331,9 @@ export default class CardsController { const { boardId, cardId: draggedCardId, targetCardId } = params; const { socketId } = mergeCardsDto; - const board = await this.mergeCardApp.mergeCards(boardId, draggedCardId, targetCardId); + const board = await this.mergeCardUseCase.execute({ boardId, draggedCardId, targetCardId }); - if (!board) throw new BadRequestException(UPDATE_FAILED); - this.socketService.sendMergeCards(socketId, mergeCardsDto); + if (board) this.socketService.sendMergeCards(socketId, mergeCardsDto); return HttpStatus.OK; } diff --git a/backend/src/modules/cards/dto/useCase/merge-card.use-case.dto.ts b/backend/src/modules/cards/dto/useCase/merge-card.use-case.dto.ts new file mode 100644 index 000000000..fd4919ed8 --- /dev/null +++ b/backend/src/modules/cards/dto/useCase/merge-card.use-case.dto.ts @@ -0,0 +1,19 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsNotEmpty, IsString } from 'class-validator'; + +export default class MergeCardUseCaseDto { + @ApiProperty() + @IsNotEmpty() + @IsString() + boardId: string; + + @ApiProperty() + @IsNotEmpty() + @IsString() + draggedCardId: string; + + @ApiProperty() + @IsNotEmpty() + @IsString() + targetCardId: string; +} diff --git a/backend/src/modules/cards/interfaces/applications/merge.card.application.interface.ts b/backend/src/modules/cards/interfaces/applications/merge.card.application.interface.ts deleted file mode 100644 index ec8849fa0..000000000 --- a/backend/src/modules/cards/interfaces/applications/merge.card.application.interface.ts +++ /dev/null @@ -1,3 +0,0 @@ -export interface MergeCardApplicationInterface { - mergeCards(boardId: string, draggedCardId: string, cardId: string): Promise; -} diff --git a/backend/src/modules/cards/interfaces/services/merge.card.service.interface.ts b/backend/src/modules/cards/interfaces/services/merge.card.service.interface.ts deleted file mode 100644 index 29ab9c528..000000000 --- a/backend/src/modules/cards/interfaces/services/merge.card.service.interface.ts +++ /dev/null @@ -1,3 +0,0 @@ -export interface MergeCardServiceInterface { - mergeCards(boardId: string, draggedCardId: string, cardId: string): Promise; -} diff --git a/backend/src/modules/cards/interfaces/types.ts b/backend/src/modules/cards/interfaces/types.ts index f494777bf..fe95b8d27 100644 --- a/backend/src/modules/cards/interfaces/types.ts +++ b/backend/src/modules/cards/interfaces/types.ts @@ -13,7 +13,8 @@ export const TYPES = { UpdateCardApplication: 'UpdateCardApplication', MergeCardApplication: 'MergeCardApplication', UnmergeCardApplication: 'UnmergeCardApplication', - CreateCardUseCase: 'CreateCardUseCase' + CreateCardUseCase: 'CreateCardUseCase', + MergeCardUseCase: 'MergeCardUseCase' }, repository: { CardRepository: 'CardRepository'