Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: add usecases for createvoteservice #1354

Merged
merged 15 commits into from
Apr 5, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
import { Test, TestingModule } from '@nestjs/testing';
import { TYPES } from '../interfaces/types';
import * as BoardUsers from 'src/modules/boardUsers/interfaces/types';
import { DeepMocked, createMock } from '@golevelup/ts-jest';
import { CreateVoteServiceInterface } from '../interfaces/services/create.vote.service.interface';
import { VoteRepositoryInterface } from '../interfaces/repositories/vote.repository.interface';
import { UpdateBoardServiceInterface } from 'src/modules/boards/interfaces/services/update.board.service.interface';
import faker from '@faker-js/faker';
import Board from 'src/modules/boards/entities/board.schema';
import { BoardFactory } from 'src/libs/test-utils/mocks/factories/board-factory.mock';
import { CardFactory } from 'src/libs/test-utils/mocks/factories/card-factory.mock';
import Card from 'src/modules/cards/entities/card.schema';
import { InsertFailedException } from 'src/libs/exceptions/insertFailedBadRequestException';
import { WRITE_LOCK_ERROR } from 'src/libs/constants/database';
import { UseCase } from 'src/libs/interfaces/use-case.interface';
import { INSERT_VOTE_FAILED } from 'src/libs/exceptions/messages';
import { CardGroupVoteUseCase } from './card-group-vote.use-case';
import CardGroupVoteUseCaseDto from '../dto/useCase/card-group-vote.use-case.dto';
import { DeleteVoteServiceInterface } from '../interfaces/services/delete.vote.service.interface';

const userId: string = faker.datatype.uuid();
const board: Board = BoardFactory.create({ maxVotes: 3 });
const card: Card = CardFactory.create();

describe('CardGroupVoteUseCase', () => {
let useCase: UseCase<CardGroupVoteUseCaseDto, void>;
let voteRepositoryMock: DeepMocked<VoteRepositoryInterface>;
let createVoteServiceMock: DeepMocked<CreateVoteServiceInterface>;

beforeAll(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
CardGroupVoteUseCase,
{
provide: TYPES.repositories.VoteRepository,
useValue: createMock<VoteRepositoryInterface>()
},
{
provide: TYPES.services.DeleteVoteService,
useValue: createMock<DeleteVoteServiceInterface>()
},
{
provide: TYPES.services.CreateVoteService,
useValue: createMock<CreateVoteServiceInterface>()
},
{
provide: BoardUsers.TYPES.services.UpdateBoardUserService,
useValue: createMock<UpdateBoardServiceInterface>()
}
]
}).compile();

useCase = module.get(CardGroupVoteUseCase);
voteRepositoryMock = module.get(TYPES.repositories.VoteRepository);
createVoteServiceMock = module.get(TYPES.services.CreateVoteService);
});

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

createVoteServiceMock.canUserVote.mockResolvedValue();
createVoteServiceMock.incrementVoteUser.mockResolvedValue();
voteRepositoryMock.insertCardGroupVote.mockResolvedValue(board);
});

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

describe('execute', () => {
it('should throw an error when the canUserVote function returns error', async () => {
try {
createVoteServiceMock.canUserVote.mockRejectedValueOnce(new InsertFailedException());

await useCase.execute({
boardId: board._id,
cardId: card._id,
userId,
count: 1,
completionHandler: () => {
return;
}
});
} catch (ex) {
expect(ex).toBeInstanceOf(InsertFailedException);
}
});

it('should throw an error when the addVoteToCardAndUser fails', async () => {
//if createVoteServiceMock.incrementVoteUser fails
try {
createVoteServiceMock.incrementVoteUser.mockRejectedValueOnce(INSERT_VOTE_FAILED);

await useCase.execute({
boardId: board._id,
cardId: card._id,
userId,
count: 1,
completionHandler: () => {
return;
}
});
} catch (ex) {
expect(ex).toBeInstanceOf(InsertFailedException);
}

//if voteRepositoryMock.insertCardGroupVote fails
try {
voteRepositoryMock.insertCardGroupVote.mockResolvedValueOnce(null);
await useCase.execute({
boardId: board._id,
cardId: card._id,
userId,
count: 1,
completionHandler: () => {
return;
}
});
} catch (ex) {
expect(ex).toBeInstanceOf(InsertFailedException);
}

//if the error code is WRITE_ERROR_LOCK and the retryCount is less than 5
try {
voteRepositoryMock.insertCardGroupVote.mockRejectedValueOnce({ code: WRITE_LOCK_ERROR });
await useCase.execute({
boardId: board._id,
cardId: card._id,
userId,
count: 1,
completionHandler: () => {
return;
}
});
} catch (ex) {
expect(ex).toBeInstanceOf(InsertFailedException);
}
});

it('should call all the functions when execute succeeds', async () => {
await useCase.execute({
boardId: board._id,
cardId: card._id,
userId,
count: 1,
completionHandler: () => {
return;
}
});
expect(createVoteServiceMock.canUserVote).toBeCalled();
expect(createVoteServiceMock.incrementVoteUser).toBeCalled();
expect(voteRepositoryMock.insertCardGroupVote).toBeCalled();
});

it('should throw an error when a commit transaction fails', async () => {
voteRepositoryMock.commitTransaction.mockRejectedValueOnce('Commit transaction failed');

expect(
async () =>
await useCase.execute({
boardId: board._id,
cardId: card._id,
userId,
count: 1,
completionHandler: () => {
return;
}
})
).rejects.toThrow(InsertFailedException);
});
});
});
97 changes: 97 additions & 0 deletions backend/src/modules/votes/applications/card-group-vote.use-case.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import { Inject, Injectable, Logger } from '@nestjs/common';
import { TYPES } from '../interfaces/types';
import * as BoardUsers from 'src/modules/boardUsers/interfaces/types';
import { UseCase } from 'src/libs/interfaces/use-case.interface';
import { CreateVoteServiceInterface } from '../interfaces/services/create.vote.service.interface';
import { UpdateBoardUserServiceInterface } from 'src/modules/boardUsers/interfaces/services/update.board.user.service.interface';
import { InsertFailedException } from 'src/libs/exceptions/insertFailedBadRequestException';
import { INSERT_VOTE_FAILED } from 'src/libs/exceptions/messages';
import { WRITE_LOCK_ERROR } from 'src/libs/constants/database';
import { VoteRepositoryInterface } from '../interfaces/repositories/vote.repository.interface';
import CardGroupVoteUseCaseDto from '../dto/useCase/card-group-vote.use-case.dto';
import { DeleteVoteServiceInterface } from '../interfaces/services/delete.vote.service.interface';

@Injectable()
export class CardGroupVoteUseCase implements UseCase<CardGroupVoteUseCaseDto, void> {
private logger: Logger = new Logger('CreateVoteService');
constructor(
@Inject(TYPES.services.CreateVoteService)
private readonly createVoteService: CreateVoteServiceInterface,
@Inject(TYPES.repositories.VoteRepository)
private readonly voteRepository: VoteRepositoryInterface,
@Inject(BoardUsers.TYPES.services.UpdateBoardUserService)
private readonly updateBoardUserService: UpdateBoardUserServiceInterface,
@Inject(TYPES.services.DeleteVoteService)
private readonly deleteVoteService: DeleteVoteServiceInterface
) {}

async execute({ boardId, cardId, userId, count, completionHandler }: CardGroupVoteUseCaseDto) {
if (count < 0) {
await this.deleteVoteService.deleteVoteFromCardGroup(boardId, cardId, userId, count);
} else {
await this.addVoteToCardGroupAndUser(boardId, userId, count, cardId);
}

completionHandler();
}

private async addVoteToCardGroupAndUser(
boardId: string,
userId: string,
count: number,
cardId: string,
retryCount?: number
) {
await this.createVoteService.canUserVote(boardId, userId, count);

await this.updateBoardUserService.startTransaction();
await this.voteRepository.startTransaction();

try {
await this.addVoteToCardGroupAndUserOperations(boardId, userId, count, cardId, retryCount);
await this.updateBoardUserService.commitTransaction();
await this.voteRepository.commitTransaction();
} catch (e) {
throw new InsertFailedException(INSERT_VOTE_FAILED);
} finally {
await this.updateBoardUserService.endSession();
await this.voteRepository.endSession();
}
}

private async addVoteToCardGroupAndUserOperations(
boardId: string,
userId: string,
count: number,
cardId: string,
retryCount?: number
) {
let retryCountOperation = retryCount ?? 0;
const withSession = true;
try {
await this.createVoteService.incrementVoteUser(boardId, userId, count, withSession);
const updatedBoard = await this.voteRepository.insertCardGroupVote(
boardId,
userId,
count,
cardId,
withSession
);

if (!updatedBoard) throw new InsertFailedException(INSERT_VOTE_FAILED);
} catch (e) {
this.logger.error(e);
await this.updateBoardUserService.abortTransaction();
await this.voteRepository.abortTransaction();

if (e.code === WRITE_LOCK_ERROR && retryCountOperation < 5) {
retryCountOperation++;
await this.updateBoardUserService.endSession();
await this.voteRepository.endSession();
await this.addVoteToCardGroupAndUser(boardId, userId, count, cardId, retryCountOperation);
} else {
throw new InsertFailedException(INSERT_VOTE_FAILED);
}
}
}
}
Loading