Skip to content
Merged
163 changes: 163 additions & 0 deletions backend/src/modules/cards/applications/create-card.use-case.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
import { Test, TestingModule } from '@nestjs/testing';
import { CreateCardUseCase } from './create-card.use-case';
import { TYPES } from '../interfaces/types';
import { DeepMocked, createMock } from '@golevelup/ts-jest';
import { CardRepositoryInterface } from '../repository/card.repository.interface';
import faker from '@faker-js/faker';
import { CardDtoFactory } from 'src/libs/test-utils/mocks/factories/dto/cardDto-factory.mock';
import CreateCardUseCaseDto from '../dto/useCase/create-card.use-case.dto';
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 { CardItemDtoFactory } from 'src/libs/test-utils/mocks/factories/dto/cardItemDto-factory.mock';
import { CardItemFactory } from 'src/libs/test-utils/mocks/factories/cardItem-factory.mock';
import { hideText } from 'src/libs/utils/hideText';
import User from 'src/modules/users/entities/user.schema';
import { BadRequestException, HttpException } from '@nestjs/common';

//Create Card Items Mocks
const cardIdtemDto = CardItemDtoFactory.create({ text: 'New Card', comments: [] });

const cardItemFactory = CardItemFactory.create({
_id: cardIdtemDto._id,
text: cardIdtemDto.text,
comments: [],
anonymous: cardIdtemDto.anonymous,
votes: cardIdtemDto.votes
});

//Create Card Mocks
const cardDtoMock = CardDtoFactory.create({
_id: faker.datatype.uuid(),
text: 'New Card',
comments: [],
votes: [],
items: [cardIdtemDto]
});

const newCardMock = CardFactory.create({
_id: cardDtoMock._id,
text: cardDtoMock.text,
comments: [],
votes: [],
items: [cardItemFactory]
});

let boardMock;
let createCardUseCaseDtoMock: CreateCardUseCaseDto;

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

beforeAll(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
CreateCardUseCase,
{
provide: TYPES.repository.CardRepository,
useValue: createMock<CardRepositoryInterface>()
}
]
}).compile();
useCase = module.get<CreateCardUseCase>(CreateCardUseCase);
cardRepositoryMock = module.get(TYPES.repository.CardRepository);
});

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

//Create Mock Board
boardMock = BoardFactory.create();
boardMock.columns[0]._id = 'colId';
boardMock.columns[0].cards[0] = newCardMock;
cardRepositoryMock.pushCardWithPopulate.mockResolvedValue(boardMock);

//Create Mock CreateCardUseCaseDto
createCardUseCaseDtoMock = {
boardId: faker.datatype.uuid(),
userId: faker.datatype.uuid(),
createCardDto: {
socketId: faker.datatype.uuid(),
colIdToAdd: 'colId',
card: cardDtoMock,
boardId: faker.datatype.uuid(),
newCard: null
}
};
});

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

it('should call cardRepository once', async () => {
await useCase.execute(createCardUseCaseDtoMock);
expect(cardRepositoryMock.pushCardWithPopulate).toHaveBeenCalledTimes(1);
});

it('should return new card and send a card with hidden data for the websocket', async () => {
boardMock.hideCards = true;
boardMock.hideVotes = true;

cardRepositoryMock.pushCardWithPopulate.mockResolvedValueOnce(boardMock);
await expect(useCase.execute(createCardUseCaseDtoMock)).resolves.toEqual(
expect.objectContaining({
newCardToReturn: expect.objectContaining({ _id: cardDtoMock._id, text: cardDtoMock.text }),
newCardToSocket: expect.objectContaining({
text: hideText(cardDtoMock.text),
createdBy: expect.objectContaining({
firstName: hideText((newCardMock.createdBy as User).firstName),
lastName: hideText((newCardMock.createdBy as User).lastName)
})
})
})
);
});

it('should return new card and a card with no hidden data for the websocket', async () => {
boardMock.hideCards = false;
boardMock.hideVotes = false;
boardMock.columns[0].cards[0].anonymous = false;
boardMock.columns[0].cards[0].createdByTeam = null;

cardRepositoryMock.pushCardWithPopulate.mockResolvedValueOnce(boardMock);
await expect(useCase.execute(createCardUseCaseDtoMock)).resolves.toEqual(
expect.objectContaining({
newCardToReturn: expect.objectContaining({ _id: cardDtoMock._id, text: cardDtoMock.text }),
newCardToSocket: expect.objectContaining({
text: cardDtoMock.text,
createdBy: expect.objectContaining({
firstName: (newCardMock.createdBy as User).firstName,
lastName: (newCardMock.createdBy as User).lastName
})
})
})
);
});

it('should return new card when card.items isEmpty', async () => {
createCardUseCaseDtoMock.createCardDto.card.items = [];

await expect(useCase.execute(createCardUseCaseDtoMock)).resolves.toEqual(
expect.objectContaining({
newCardToReturn: expect.objectContaining({
items: expect.arrayContaining([expect.objectContaining({ text: cardDtoMock.text })])
})
})
);
});

it('should throw error if board.columns doesnt exists', async () => {
boardMock.columns = null;
cardRepositoryMock.pushCardWithPopulate.mockResolvedValueOnce(boardMock);
await expect(useCase.execute(createCardUseCaseDtoMock)).rejects.toThrowError(HttpException);
});

it('should return BadRequestException if insert fail ', async () => {
createCardUseCaseDtoMock.createCardDto.colIdToAdd = 'FakeColId';
await expect(useCase.execute(createCardUseCaseDtoMock)).rejects.toThrowError(
BadRequestException
);
});
});
95 changes: 95 additions & 0 deletions backend/src/modules/cards/applications/create-card.use-case.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import { BadRequestException, HttpException, HttpStatus, Inject, Injectable } from '@nestjs/common';
import { TYPES } from '../interfaces/types';
import { UseCase } from 'src/libs/interfaces/use-case.interface';
import CreateCardUseCaseDto from '../dto/useCase/create-card.use-case.dto';
import { CardRepositoryInterface } from '../repository/card.repository.interface';
import isEmpty from 'src/libs/utils/isEmpty';
import CardItem from '../entities/card.item.schema';
import { INSERT_FAILED } from 'src/libs/exceptions/messages';
import CardCreationPresenter from '../dto/useCase/presenters/create-card-res.use-case.dto';
import { replaceCard } from 'src/modules/boards/utils/clean-board';
import Card from '../entities/card.schema';
import { hideText } from 'src/libs/utils/hideText';
import CardDto from '../dto/card.dto';

@Injectable()
export class CreateCardUseCase implements UseCase<CreateCardUseCaseDto, CardCreationPresenter> {
constructor(
@Inject(TYPES.repository.CardRepository)
private readonly cardRepository: CardRepositoryInterface
) {}

async execute(createCardUseCaseDto: CreateCardUseCaseDto) {
const { createCardDto, userId, boardId } = createCardUseCaseDto;
const { card, colIdToAdd } = createCardDto;

const transformedCard = this.transformCardToStore(card, userId);

const board = await this.cardRepository.pushCardWithPopulate(
boardId,
colIdToAdd,
0,
transformedCard
);

if (!board.columns) throw new HttpException(INSERT_FAILED, HttpStatus.BAD_REQUEST);

const newCard = this.extractCardFromBoard(board, colIdToAdd);

const newCardToSocket = this.transformCardForSocket(
newCard,
userId,
board.hideCards,
board.hideVotes
);

return {
newCardToReturn: newCard,
newCardToSocket: newCardToSocket
};
}

private transformCardToStore(card: CardDto, userId: string) {
card.createdBy = userId;

if (isEmpty(card.items)) {
(card.items as CardItem[]).push({
text: card.text,
createdBy: userId,
comments: [],
votes: [],
anonymous: false,
createdByTeam: undefined,
createdAt: new Date()
});
} else {
card.items[0].createdBy = userId;
}

return card;
}

private transformCardForSocket(newCard, userId: string, hideCards, hideVotes): Card {
const cardWithHiddenInfo = replaceCard(
newCard,
hideText(userId.toString()),
hideCards,
hideVotes
);

return cardWithHiddenInfo as Card;
}

//Extract the card from the populated board
private extractCardFromBoard(board, colIdToAdd) {
try {
const colIndex = board.columns.findIndex((col) => col._id.toString() === colIdToAdd);

const newCard = board.columns[colIndex].cards[0];

return newCard;
} catch {
throw new BadRequestException(INSERT_FAILED);
}
}
}
17 changes: 0 additions & 17 deletions backend/src/modules/cards/applications/create.card.application.ts

This file was deleted.

8 changes: 3 additions & 5 deletions backend/src/modules/cards/cards.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,7 @@ import SocketModule from '../socket/socket.module';
import { VotesModule } from '../votes/votes.module';
import {
cardRepository,
createCardApplication,
createCardService,
creacteCardUseCase,
deleteCardApplication,
deleteCardService,
getCardService,
Expand All @@ -22,19 +21,18 @@ import CardsController from './controller/cards.controller';
imports: [mongooseBoardModule, forwardRef(() => SocketModule), forwardRef(() => VotesModule)],
controllers: [CardsController],
providers: [
createCardService,
updateCardService,
getCardService,
deleteCardService,
updateCardService,
mergeCardService,
unmergeCardService,
createCardApplication,
updateCardApplication,
deleteCardApplication,
mergeCardApplication,
unmergeCardApplication,
cardRepository
cardRepository,
creacteCardUseCase
],
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,22 +1,16 @@
import { CreateCardApplication } from './applications/create.card.application';
import { CreateCardUseCase } from './applications/create-card.use-case';
import { DeleteCardApplication } from './applications/delete.card.application';
import { MergeCardApplication } from './applications/merge.card.application';
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 CreateCardService from './services/create.card.service';
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';

export const createCardService = {
provide: TYPES.services.CreateCardService,
useClass: CreateCardService
};

export const getCardService = {
provide: TYPES.services.GetCardService,
useClass: GetCardService
Expand All @@ -42,11 +36,6 @@ export const unmergeCardService = {
useClass: UnmergeCardService
};

export const createCardApplication = {
provide: TYPES.applications.CreateCardApplication,
useClass: CreateCardApplication
};

export const updateCardApplication = {
provide: TYPES.applications.UpdateCardApplication,
useClass: UpdateCardApplication
Expand All @@ -71,3 +60,8 @@ export const cardRepository = {
provide: TYPES.repository.CardRepository,
useClass: CardRepository
};

export const creacteCardUseCase = {
provide: TYPES.applications.CreateCardUseCase,
useClass: CreateCardUseCase
};
Loading