Skip to content

Commit

Permalink
refactor: unmergecardservice to usecase and tests (#1312)
Browse files Browse the repository at this point in the history
  • Loading branch information
GoncaloCanteiro authored Mar 29, 2023
1 parent 12b648d commit aa202d9
Show file tree
Hide file tree
Showing 13 changed files with 338 additions and 186 deletions.
164 changes: 164 additions & 0 deletions backend/src/modules/cards/applications/unmerge-card.use-case.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
import { Test, TestingModule } from '@nestjs/testing';
import { TYPES } from '../interfaces/types';
import { DeepMocked, createMock } from '@golevelup/ts-jest';
import { CardRepositoryInterface } from '../repository/card.repository.interface';
import { UnmergeCardUseCase } from './unmerge-card.use-case';
import { GetCardServiceInterface } from '../interfaces/services/get.card.service.interface';
import UnmergeCardUseCaseDto from '../dto/useCase/unmerge-card.use-case.dto';
import faker from '@faker-js/faker';
import { CardFactory } from 'src/libs/test-utils/mocks/factories/card-factory.mock';
import { BoardFactory } from 'src/libs/test-utils/mocks/factories/board-factory.mock';
import { BadRequestException } from '@nestjs/common';

const unmergeCardDto: UnmergeCardUseCaseDto = {
boardId: faker.datatype.uuid(),
cardGroupId: faker.datatype.uuid(),
draggedCardId: faker.datatype.uuid(),
columnId: faker.datatype.uuid(),
position: faker.datatype.number()
};

const updateResultMock = {
acknowledged: true,
matchedCount: 1,
modifiedCount: 1,
upsertedCount: 1,
upsertedId: null
};
const cardMock = CardFactory.create();
const boardMock = BoardFactory.create();

describe('UnmergeCardUseCase', () => {
let useCase: UnmergeCardUseCase;
let cardRepositoryMock: DeepMocked<CardRepositoryInterface>;
let cardServiceMock: DeepMocked<GetCardServiceInterface>;

beforeAll(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
UnmergeCardUseCase,
{
provide: TYPES.services.GetCardService,
useValue: createMock<GetCardServiceInterface>()
},
{
provide: TYPES.repository.CardRepository,
useValue: createMock<CardRepositoryInterface>()
}
]
}).compile();
useCase = module.get<UnmergeCardUseCase>(UnmergeCardUseCase);
cardRepositoryMock = module.get(TYPES.repository.CardRepository);
cardServiceMock = module.get(TYPES.services.GetCardService);
cardServiceMock.getCardItemFromGroup.mockResolvedValue(cardMock);
cardServiceMock.getCardFromBoard.mockResolvedValue(cardMock);
cardRepositoryMock.pullItem.mockResolvedValue(updateResultMock);
cardRepositoryMock.updateCardFromGroupOnUnmerge.mockResolvedValue(boardMock);
});

beforeEach(() => {
jest.clearAllMocks();
jest.restoreAllMocks();
});

it('should be defined', () => {
expect(useCase).toBeDefined();
});

it('should call repository startTransaction', async () => {
await useCase.execute(unmergeCardDto);
expect(cardRepositoryMock.startTransaction).toHaveBeenCalledTimes(1);
});

it('should call repository commitTransaction', async () => {
await useCase.execute(unmergeCardDto);
expect(cardRepositoryMock.commitTransaction).toHaveBeenCalledTimes(1);
});

it('should call repository endSession', async () => {
await useCase.execute(unmergeCardDto);
expect(cardRepositoryMock.endSession).toHaveBeenCalledTimes(1);
});

it('should call cardService.getCardItemFromGroup with boardId and draggedCardId', async () => {
await useCase.execute(unmergeCardDto);
expect(cardServiceMock.getCardItemFromGroup).toHaveBeenNthCalledWith(
1,
unmergeCardDto.boardId,
unmergeCardDto.draggedCardId
);
});

it('should throw badRequest if getCardItemFromGroup fails', async () => {
cardServiceMock.getCardItemFromGroup.mockResolvedValueOnce(null);
await expect(useCase.execute(unmergeCardDto)).rejects.toThrow(BadRequestException);
await expect(cardRepositoryMock.endSession).toHaveBeenCalledTimes(1);
});

it('should call pullItem ', async () => {
await useCase.execute(unmergeCardDto);
expect(cardRepositoryMock.pullItem).toHaveBeenNthCalledWith(
1,
unmergeCardDto.boardId,
unmergeCardDto.draggedCardId,
true
);
});

it('should throw badRequest if pullItem fails', async () => {
updateResultMock.modifiedCount = 2;
cardRepositoryMock.pullItem.mockResolvedValueOnce(updateResultMock);

await expect(useCase.execute(unmergeCardDto)).rejects.toThrow(BadRequestException);
await expect(cardRepositoryMock.abortTransaction).toHaveBeenCalledTimes(1);
await expect(cardRepositoryMock.endSession).toHaveBeenCalledTimes(1);
updateResultMock.modifiedCount = 1;
});

it('should call cardService.getCardFromBoard with boardId and cardGroupId', async () => {
await useCase.execute(unmergeCardDto);
expect(cardServiceMock.getCardFromBoard).toHaveBeenNthCalledWith(
1,
unmergeCardDto.boardId,
unmergeCardDto.cardGroupId
);
});

it('should throw badRequest if cardGroup not found', async () => {
cardServiceMock.getCardFromBoard.mockResolvedValueOnce(null);
await expect(useCase.execute(unmergeCardDto)).rejects.toThrow(BadRequestException);
await expect(cardRepositoryMock.abortTransaction).toHaveBeenCalledTimes(1);
await expect(cardRepositoryMock.endSession).toHaveBeenCalledTimes(1);
});

it('should throw badRequest if getCardFromBoard the new card', async () => {
const newCardSavedMock = CardFactory.create();
newCardSavedMock.items[0]._id = null;
cardServiceMock.getCardFromBoard
.mockResolvedValueOnce(cardMock)
.mockResolvedValueOnce(newCardSavedMock);

await expect(useCase.execute(unmergeCardDto)).rejects.toThrow(BadRequestException);
await expect(cardRepositoryMock.endSession).toHaveBeenCalledTimes(1);
});

it('should throw badRequest when cardRep.updateCardFromGroupOnUnmerge fails', async () => {
cardRepositoryMock.updateCardFromGroupOnUnmerge.mockResolvedValueOnce(null);
await expect(useCase.execute(unmergeCardDto)).rejects.toThrow(BadRequestException);
await expect(cardRepositoryMock.abortTransaction).toHaveBeenCalledTimes(1);
await expect(cardRepositoryMock.endSession).toHaveBeenCalledTimes(1);
});

it('should throw badRequest when pushCard fails', async () => {
cardRepositoryMock.pushCard.mockResolvedValueOnce(null);
await expect(useCase.execute(unmergeCardDto)).rejects.toThrow(BadRequestException);
await expect(cardRepositoryMock.abortTransaction).toHaveBeenCalledTimes(1);
await expect(cardRepositoryMock.endSession).toHaveBeenCalledTimes(1);
});

it('should throw badRequest when unexpected error occurs', async () => {
cardServiceMock.getCardItemFromGroup.mockRejectedValueOnce(Error);
await expect(useCase.execute(unmergeCardDto)).rejects.toThrow(BadRequestException);
await expect(cardRepositoryMock.endSession).toHaveBeenCalledTimes(1);
});
});
128 changes: 128 additions & 0 deletions backend/src/modules/cards/applications/unmerge-card.use-case.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
import { Inject, Injectable } from '@nestjs/common';
import { TYPES } from '../interfaces/types';
import { UseCase } from 'src/libs/interfaces/use-case.interface';
import { CardRepositoryInterface } from '../repository/card.repository.interface';
import { GetCardServiceInterface } from '../interfaces/services/get.card.service.interface';
import {
CARD_NOT_FOUND,
CARD_NOT_INSERTED,
CARD_NOT_REMOVED,
UPDATE_FAILED
} from 'src/libs/exceptions/messages';
import UnmergeCardUseCaseDto from '../dto/useCase/unmerge-card.use-case.dto';
import { UpdateFailedException } from 'src/libs/exceptions/updateFailedBadRequestException';

@Injectable()
export class UnmergeCardUseCase implements UseCase<UnmergeCardUseCaseDto, string> {
constructor(
@Inject(TYPES.services.GetCardService)
private readonly cardService: GetCardServiceInterface,
@Inject(TYPES.repository.CardRepository)
private readonly cardRepository: CardRepositoryInterface
) {}

async execute(unMergeCardDto: UnmergeCardUseCaseDto) {
const { boardId, draggedCardId } = unMergeCardDto;
await this.cardRepository.startTransaction();

try {
const cardItemToMove = await this.cardService.getCardItemFromGroup(boardId, draggedCardId);

if (!cardItemToMove) throw Error(CARD_NOT_FOUND);

const cardId = await this.unmergeCard(cardItemToMove, unMergeCardDto);

await this.cardRepository.commitTransaction();

const newCardSaved = await this.cardService.getCardFromBoard(boardId, cardId);

const itemId = newCardSaved.items[0]._id;

if (!itemId) throw Error(UPDATE_FAILED);

return itemId;
} catch (e) {
throw new UpdateFailedException(e.message ? e.message : UPDATE_FAILED);
} finally {
await this.cardRepository.endSession();
}
}

private async unmergeCard(cardItemToMove, unMergeCardDto) {
const { boardId, cardGroupId, draggedCardId } = unMergeCardDto;
try {
//Removes card from group
const pullResult = await this.cardRepository.pullItem(boardId, draggedCardId, true);

if (pullResult.modifiedCount !== 1) throw Error(CARD_NOT_REMOVED);

const cardGroup = await this.cardService.getCardFromBoard(boardId, cardGroupId);

if (!cardGroup) throw Error(CARD_NOT_FOUND);

await this.updateLastCardOnGroup(cardGroup, unMergeCardDto);

const cardId = await this.createNewCard(cardItemToMove, unMergeCardDto);

return cardId;
} catch (e) {
await this.cardRepository.abortTransaction();
throw Error(e.message);
}
}

//When card group has only one item (!== draggedCardId) move the votes and comments to a new card
private async updateLastCardOnGroup(cardGroup, unMergeCardDto) {
const { boardId, cardGroupId, draggedCardId } = unMergeCardDto;

const items = cardGroup.items.filter((item) => item._id.toString() !== draggedCardId);

if (items.length === 1) {
const [{ comments, votes: itemVotes }] = items;
const newComments = cardGroup.comments.concat(comments);
const newVotes = (cardGroup.votes as unknown as string[]).concat(
itemVotes as unknown as string[]
);

const updateCard = await this.cardRepository.updateCardFromGroupOnUnmerge(
boardId,
cardGroupId,
items[0],
newComments,
newVotes,
true
);

if (!updateCard) throw Error(UPDATE_FAILED);
}
}

//Creates new card for the card that was removed from the group
private async createNewCard(cardItemToMove, unMergeCardDto) {
const { boardId, columnId, position } = unMergeCardDto;

const newCardItem = { ...cardItemToMove };
const cardId = newCardItem._id;
delete newCardItem._id;

const newCard = {
_id: cardId,
...cardItemToMove,
comments: [],
votes: [],
items: [newCardItem]
};

const pushResult = await this.cardRepository.pushCard(
boardId,
columnId,
position,
newCard,
true
);

if (!pushResult) throw Error(CARD_NOT_INSERTED);

return cardId;
}
}
28 changes: 0 additions & 28 deletions backend/src/modules/cards/applications/unmerge.card.application.ts

This file was deleted.

6 changes: 2 additions & 4 deletions backend/src/modules/cards/cards.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,7 @@ import {
deleteCardService,
getCardService,
mergeCardUseCase,
unmergeCardApplication,
unmergeCardService,
unmergeCardUseCase,
updateCardApplication,
updateCardService
} from './cards.providers';
Expand All @@ -24,12 +23,11 @@ import CardsController from './controller/cards.controller';
getCardService,
deleteCardService,
updateCardService,
unmergeCardService,
updateCardApplication,
deleteCardApplication,
unmergeCardApplication,
cardRepository,
creacteCardUseCase,
unmergeCardUseCase,
mergeCardUseCase
],
exports: [getCardService, deleteCardService]
Expand Down
18 changes: 6 additions & 12 deletions backend/src/modules/cards/cards.providers.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
import { CreateCardUseCase } from './applications/create-card.use-case';
import { DeleteCardApplication } from './applications/delete.card.application';
import { UnmergeCardUseCase } from './applications/unmerge-card.use-case';
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 { UnmergeCardService } from './services/unmerge.card.service';
import UpdateCardService from './services/update.card.service';

export const getCardService = {
Expand All @@ -25,11 +24,6 @@ export const deleteCardService = {
useClass: DeleteCardService
};

export const unmergeCardService = {
provide: TYPES.services.UnmergeCardService,
useClass: UnmergeCardService
};

export const updateCardApplication = {
provide: TYPES.applications.UpdateCardApplication,
useClass: UpdateCardApplication
Expand All @@ -40,11 +34,6 @@ export const deleteCardApplication = {
useClass: DeleteCardApplication
};

export const unmergeCardApplication = {
provide: TYPES.applications.UnmergeCardApplication,
useClass: UnmergeCardApplication
};

export const cardRepository = {
provide: TYPES.repository.CardRepository,
useClass: CardRepository
Expand All @@ -59,3 +48,8 @@ export const mergeCardUseCase = {
provide: TYPES.applications.MergeCardUseCase,
useClass: MergeCardUseCase
};

export const unmergeCardUseCase = {
provide: TYPES.applications.UnmergeCardUseCase,
useClass: UnmergeCardUseCase
};
Loading

0 comments on commit aa202d9

Please sign in to comment.