diff --git a/backend/src/libs/test-utils/mocks/factories/board-factory.mock.ts b/backend/src/libs/test-utils/mocks/factories/board-factory.mock.ts index ee24f7782..9fca1bc1d 100644 --- a/backend/src/libs/test-utils/mocks/factories/board-factory.mock.ts +++ b/backend/src/libs/test-utils/mocks/factories/board-factory.mock.ts @@ -3,8 +3,6 @@ import { ColumnFactory } from './column-factory.mock'; import Board from 'src/modules/boards/entities/board.schema'; import { buildTestFactory } from './generic-factory.mock'; -const userId = faker.datatype.uuid(); - const mockBoardData = () => { return { _id: faker.database.mongodbObjectId(), @@ -26,7 +24,7 @@ const mockBoardData = () => { slackEnable: faker.datatype.boolean(), addCards: faker.datatype.boolean(), responsibles: ['1'], - createdBy: userId, + createdBy: faker.datatype.uuid(), addcards: faker.datatype.boolean(), postAnonymously: faker.datatype.boolean(), createdAt: faker.datatype.datetime().toISOString() diff --git a/backend/src/libs/test-utils/mocks/factories/dto/boardDto-factory.mock.ts b/backend/src/libs/test-utils/mocks/factories/dto/boardDto-factory.mock.ts new file mode 100644 index 000000000..6194142d2 --- /dev/null +++ b/backend/src/libs/test-utils/mocks/factories/dto/boardDto-factory.mock.ts @@ -0,0 +1,42 @@ +import faker from '@faker-js/faker'; +import { BoardPhases } from 'src/libs/enum/board.phases'; +import BoardDto from 'src/modules/boards/dto/board.dto'; +import { buildTestFactory } from '../generic-factory.mock'; +import { ColumnDtoFactory } from './columnDto-factory.mock'; + +const mockBoardDto = () => { + return { + _id: faker.database.mongodbObjectId(), + title: faker.lorem.words(), + columns: ColumnDtoFactory.createMany(3), + isPublic: faker.datatype.boolean(), + maxVotes: faker.datatype.number({ min: 0, max: 6 }), + maxUsers: 0, + maxTeams: '1', + hideCards: faker.datatype.boolean(), + hideVotes: faker.datatype.boolean(), + dividedBoards: [], + team: '1', + socketId: faker.datatype.uuid(), + users: [], + recurrent: faker.datatype.boolean(), + isSubBoard: faker.datatype.boolean(), + boardNumber: 0, + slackEnable: faker.datatype.boolean(), + addCards: faker.datatype.boolean(), + responsibles: ['1'], + createdBy: faker.datatype.uuid(), + addcards: faker.datatype.boolean(), + postAnonymously: faker.datatype.boolean(), + createdAt: faker.datatype.datetime().toISOString(), + phase: faker.helpers.arrayElement([ + BoardPhases.ADDCARDS, + BoardPhases.SUBMITTED, + BoardPhases.VOTINGPHASE + ]) + }; +}; + +export const BoardDtoFactory = buildTestFactory(() => { + return mockBoardDto(); +}); diff --git a/backend/src/libs/test-utils/mocks/factories/dto/boardUserDto-factory.mock.ts b/backend/src/libs/test-utils/mocks/factories/dto/boardUserDto-factory.mock.ts new file mode 100644 index 000000000..530da0621 --- /dev/null +++ b/backend/src/libs/test-utils/mocks/factories/dto/boardUserDto-factory.mock.ts @@ -0,0 +1,20 @@ +import faker from '@faker-js/faker'; +import { BoardRoles } from 'src/libs/enum/board.roles'; +import BoardUserDto from 'src/modules/boards/dto/board.user.dto'; +import { buildTestFactory } from '../generic-factory.mock'; +import { UserFactory } from '../user-factory'; + +const mockBoardUserDto = () => { + return { + id: faker.datatype.uuid(), + role: faker.helpers.arrayElement([BoardRoles.MEMBER, BoardRoles.RESPONSIBLE]), + user: UserFactory.create(), + board: faker.datatype.uuid(), + votesCount: Math.random() * 10, + isNewJoiner: faker.datatype.boolean() + }; +}; + +export const BoardUserDtoFactory = buildTestFactory(() => { + return mockBoardUserDto(); +}); diff --git a/backend/src/libs/test-utils/mocks/factories/dto/cardDto-factory.mock.ts b/backend/src/libs/test-utils/mocks/factories/dto/cardDto-factory.mock.ts new file mode 100644 index 000000000..83e63dd9d --- /dev/null +++ b/backend/src/libs/test-utils/mocks/factories/dto/cardDto-factory.mock.ts @@ -0,0 +1,21 @@ +import faker from '@faker-js/faker'; +import CardDto from 'src/modules/cards/dto/card.dto'; +import { buildTestFactory } from '../generic-factory.mock'; +import { CardItemDtoFactory } from './cardItemDto-factory.mock'; +import { CommentDtoFactory } from './commentsDto-factory.mock'; + +const mockCardDto = () => { + return { + items: [CardItemDtoFactory.create()], + id: faker.database.mongodbObjectId(), + text: faker.lorem.words(), + createdBy: faker.datatype.uuid(), + comments: [CommentDtoFactory.create()], + votes: [], + anonymous: faker.datatype.boolean() + }; +}; + +export const CardDtoFactory = buildTestFactory(() => { + return mockCardDto(); +}); diff --git a/backend/src/libs/test-utils/mocks/factories/dto/cardItemDto-factory.mock.ts b/backend/src/libs/test-utils/mocks/factories/dto/cardItemDto-factory.mock.ts new file mode 100644 index 000000000..5864aa9f7 --- /dev/null +++ b/backend/src/libs/test-utils/mocks/factories/dto/cardItemDto-factory.mock.ts @@ -0,0 +1,19 @@ +import faker from '@faker-js/faker'; +import CardItemDto from 'src/modules/cards/dto/card.item.dto'; +import { buildTestFactory } from '../generic-factory.mock'; +import { CommentDtoFactory } from './commentsDto-factory.mock'; + +const mockCardItemDto = () => { + return { + id: faker.database.mongodbObjectId(), + text: faker.lorem.words(), + createdBy: faker.datatype.uuid(), + comments: [CommentDtoFactory.create()], + votes: [], + anonymous: faker.datatype.boolean() + }; +}; + +export const CardItemDtoFactory = buildTestFactory(() => { + return mockCardItemDto(); +}); diff --git a/backend/src/libs/test-utils/mocks/factories/dto/columnDto-factory.mock.ts b/backend/src/libs/test-utils/mocks/factories/dto/columnDto-factory.mock.ts new file mode 100644 index 000000000..8be2803b0 --- /dev/null +++ b/backend/src/libs/test-utils/mocks/factories/dto/columnDto-factory.mock.ts @@ -0,0 +1,26 @@ +import faker from '@faker-js/faker'; +import ColumnDto from 'src/modules/columns/dto/column.dto'; +import { buildTestFactory } from '../generic-factory.mock'; +import { CardDtoFactory } from './cardDto-factory.mock'; + +const mockColumnDto = () => { + return { + _id: faker.database.mongodbObjectId(), + title: faker.lorem.words(), + color: faker.helpers.arrayElement([ + '#CDFAE0', + '#DEB7FF', + '#9BFDFA', + '#FE9EBF', + '#9DCAFF', + '#FEB9A9' + ]), + cards: [CardDtoFactory.create()], + cardText: faker.lorem.words(), + isDefaultText: faker.datatype.boolean() + }; +}; + +export const ColumnDtoFactory = buildTestFactory(() => { + return mockColumnDto(); +}); diff --git a/backend/src/libs/test-utils/mocks/factories/dto/commentsDto-factory.mock.ts b/backend/src/libs/test-utils/mocks/factories/dto/commentsDto-factory.mock.ts new file mode 100644 index 000000000..112a597cf --- /dev/null +++ b/backend/src/libs/test-utils/mocks/factories/dto/commentsDto-factory.mock.ts @@ -0,0 +1,15 @@ +import faker from '@faker-js/faker'; +import CommentDto from 'src/modules/comments/dto/comment.dto'; +import { buildTestFactory } from '../generic-factory.mock'; + +const mockCommentDto = () => { + return { + text: faker.lorem.words(), + createdBy: faker.datatype.uuid(), + anonymous: faker.datatype.boolean() + }; +}; + +export const CommentDtoFactory = buildTestFactory(() => { + return mockCommentDto(); +}); diff --git a/backend/src/libs/test-utils/mocks/factories/dto/teamDto-factory.ts b/backend/src/libs/test-utils/mocks/factories/dto/teamDto-factory.ts new file mode 100644 index 000000000..dfa1c2451 --- /dev/null +++ b/backend/src/libs/test-utils/mocks/factories/dto/teamDto-factory.ts @@ -0,0 +1,22 @@ +import faker from '@faker-js/faker'; +import { ForTeamDtoEnum, TeamDto } from 'src/modules/communication/dto/team.dto'; +import { BoardRoles } from 'src/modules/communication/dto/types'; +import { buildTestFactory } from '../generic-factory.mock'; +import { UserCommunicationDtoFactory } from './userCommunicationDto-factory.mock'; + +const mockTeamCommunicationDto = () => { + return { + name: faker.company.companyName(), + normalName: faker.company.companyName(), + boardId: faker.datatype.uuid(), + channelId: faker.datatype.uuid(), + type: faker.helpers.arrayElement([ForTeamDtoEnum.SUBTEAM, ForTeamDtoEnum.TEAM]), + for: faker.helpers.arrayElement([BoardRoles.MEMBER, BoardRoles.RESPONSIBLE]), + participants: UserCommunicationDtoFactory.createMany(2), + teamNumber: Math.random() * 10 + }; +}; + +export const TeamCommunicationDtoFactory = buildTestFactory(() => { + return mockTeamCommunicationDto(); +}); diff --git a/backend/src/libs/test-utils/mocks/factories/dto/updateBoardDto-factory.mock.ts b/backend/src/libs/test-utils/mocks/factories/dto/updateBoardDto-factory.mock.ts new file mode 100644 index 000000000..c2f8ed755 --- /dev/null +++ b/backend/src/libs/test-utils/mocks/factories/dto/updateBoardDto-factory.mock.ts @@ -0,0 +1,15 @@ +import { UpdateBoardDto } from 'src/modules/boards/dto/update-board.dto'; +import { BoardUserFactory } from '../boardUser-factory.mock'; +import { buildTestFactory } from '../generic-factory.mock'; +import { BoardDtoFactory } from './boardDto-factory.mock'; + +const mockUpdateBoardDto = () => { + return { + responsible: BoardUserFactory.create(), + ...BoardDtoFactory.create() + }; +}; + +export const UpdateBoardDtoFactory = buildTestFactory(() => { + return mockUpdateBoardDto(); +}); diff --git a/backend/src/libs/test-utils/mocks/factories/dto/userCommunicationDto-factory.mock.ts b/backend/src/libs/test-utils/mocks/factories/dto/userCommunicationDto-factory.mock.ts new file mode 100644 index 000000000..a8b1bfe80 --- /dev/null +++ b/backend/src/libs/test-utils/mocks/factories/dto/userCommunicationDto-factory.mock.ts @@ -0,0 +1,19 @@ +import faker from '@faker-js/faker'; +import { UserDto } from 'src/modules/communication/dto/user.dto'; +import { buildTestFactory } from '../generic-factory.mock'; + +const mockUserCommunicationDto = () => { + return { + id: faker.datatype.uuid(), + firstName: faker.name.firstName(), + lastName: faker.name.lastName(), + email: faker.internet.email(), + responsible: faker.datatype.boolean(), + boardId: faker.datatype.uuid(), + slackId: faker.datatype.uuid() + }; +}; + +export const UserCommunicationDtoFactory = buildTestFactory(() => { + return mockUserCommunicationDto(); +}); diff --git a/backend/src/libs/test-utils/mocks/factories/dto/userDto-factory.ts b/backend/src/libs/test-utils/mocks/factories/dto/userDto-factory.mock.ts similarity index 100% rename from backend/src/libs/test-utils/mocks/factories/dto/userDto-factory.ts rename to backend/src/libs/test-utils/mocks/factories/dto/userDto-factory.mock.ts diff --git a/backend/src/modules/boards/applications/update.board.application.ts b/backend/src/modules/boards/applications/update.board.application.ts index b36446b33..9efdab036 100644 --- a/backend/src/modules/boards/applications/update.board.application.ts +++ b/backend/src/modules/boards/applications/update.board.application.ts @@ -17,8 +17,8 @@ export class UpdateBoardApplication implements UpdateBoardApplicationInterface { return this.updateBoardService.update(boardId, boardData); } - mergeBoards(subBoardId: string, userId: string) { - return this.updateBoardService.mergeBoards(subBoardId, userId); + mergeBoards(subBoardId: string, userId: string, socketId?: string) { + return this.updateBoardService.mergeBoards(subBoardId, userId, socketId); } updateBoardParticipants(boardData: UpdateBoardUserDto) { diff --git a/backend/src/modules/boards/controller/boards.controller.ts b/backend/src/modules/boards/controller/boards.controller.ts index 59bae858e..c271bfdff 100644 --- a/backend/src/modules/boards/controller/boards.controller.ts +++ b/backend/src/modules/boards/controller/boards.controller.ts @@ -36,7 +36,7 @@ import { import { BaseParam } from 'src/libs/dto/param/base.param'; import { PaginationParams } from 'src/libs/dto/param/pagination.params'; import { BaseParamWSocket } from 'src/libs/dto/param/socket.param'; -import { BOARD_NOT_FOUND, INSERT_FAILED, UPDATE_FAILED } from 'src/libs/exceptions/messages'; +import { BOARD_NOT_FOUND, INSERT_FAILED } from 'src/libs/exceptions/messages'; import JwtAuthenticationGuard from 'src/libs/guards/jwtAuth.guard'; import RequestWithUser from 'src/libs/interfaces/requestWithUser.interface'; import { BadRequestResponse } from 'src/libs/swagger/errors/bad-request.swagger'; @@ -328,22 +328,12 @@ export default class BoardsController { type: InternalServerErrorResponse }) @Put(':boardId/merge') - async mergeBoard( + mergeBoard( @Param() { boardId }: BaseParam, @Query() { socketId }: BaseParamWSocket, @Req() request: RequestWithUser ) { - const result = await this.updateBoardApp.mergeBoards(boardId, request.user._id); - - if (!result) { - throw new BadRequestException(UPDATE_FAILED); - } - - if (socketId) { - this.socketService.sendUpdatedAllBoard(boardId, socketId); - } - - return result; + return this.updateBoardApp.mergeBoards(boardId, request.user._id, socketId); } @ApiOperation({ summary: 'Update board phase' }) @@ -371,7 +361,7 @@ export default class BoardsController { type: UnauthorizedResponse }) @Put(':boardId/phase') - async updateBoardPhase(@Body() boardPhaseDto: BoardPhaseDto) { + updateBoardPhase(@Body() boardPhaseDto: BoardPhaseDto) { this.updateBoardApp.updatePhase(boardPhaseDto); } } diff --git a/backend/src/modules/boards/dto/board.dto.ts b/backend/src/modules/boards/dto/board.dto.ts index 782dfdeb5..92993ad8e 100644 --- a/backend/src/modules/boards/dto/board.dto.ts +++ b/backend/src/modules/boards/dto/board.dto.ts @@ -78,7 +78,6 @@ export default class BoardDto { @ApiProperty({ type: BoardDto, isArray: true }) @ValidateNested({ each: true }) @Type(() => BoardDto) - @IsOptional() dividedBoards!: BoardDto[] | string[]; @ApiPropertyOptional({ type: String }) @@ -92,22 +91,24 @@ export default class BoardDto { socketId?: string; @ApiPropertyOptional({ type: BoardUserDto, isArray: true }) - @IsOptional() @Validate(CheckUniqueUsers) users!: BoardUserDto[]; @ApiPropertyOptional({ default: true }) @IsNotEmpty() + @IsOptional() @IsBoolean() recurrent?: boolean; @ApiPropertyOptional({ default: false }) @IsNotEmpty() + @IsOptional() @IsBoolean() isSubBoard?: boolean; @ApiPropertyOptional({ default: 0 }) @IsNotEmpty() + @IsOptional() @IsNumber() boardNumber?: number; diff --git a/backend/src/modules/boards/dto/board.user.dto.ts b/backend/src/modules/boards/dto/board.user.dto.ts index 3e375c986..2e85e83ec 100644 --- a/backend/src/modules/boards/dto/board.user.dto.ts +++ b/backend/src/modules/boards/dto/board.user.dto.ts @@ -9,6 +9,7 @@ import { IsString } from 'class-validator'; import { BoardRoles } from 'src/libs/enum/board.roles'; +import User from 'src/modules/users/entities/user.schema'; export default class BoardUserDto { @ApiPropertyOptional() @@ -23,10 +24,8 @@ export default class BoardUserDto { role!: string; @ApiProperty() - @IsMongoId() - @IsString() @IsNotEmpty() - user!: string; + user!: string | User; @ApiProperty() @IsMongoId() diff --git a/backend/src/modules/boards/interfaces/applications/update.board.application.interface.ts b/backend/src/modules/boards/interfaces/applications/update.board.application.interface.ts index ce5fe99a8..52b49490b 100644 --- a/backend/src/modules/boards/interfaces/applications/update.board.application.interface.ts +++ b/backend/src/modules/boards/interfaces/applications/update.board.application.interface.ts @@ -1,14 +1,18 @@ import { LeanDocument } from 'mongoose'; import { UpdateBoardDto } from '../../dto/update-board.dto'; -import { BoardDocument } from '../../entities/board.schema'; +import Board, { BoardDocument } from '../../entities/board.schema'; import BoardUser from '../../entities/board.user.schema'; import UpdateBoardUserDto from 'src/modules/boards/dto/update-board-user.dto'; import { BoardPhaseDto } from 'src/libs/dto/board-phase.dto'; export interface UpdateBoardApplicationInterface { - update(boardId: string, boardData: UpdateBoardDto): Promise | null>; + update(boardId: string, boardData: UpdateBoardDto): Promise; - mergeBoards(subBoardId: string, userId: string): Promise | null>; + mergeBoards( + subBoardId: string, + userId: string, + socketId?: string + ): Promise | null>; updateBoardParticipants(boardData: UpdateBoardUserDto): Promise; diff --git a/backend/src/modules/boards/interfaces/services/update.board.service.interface.ts b/backend/src/modules/boards/interfaces/services/update.board.service.interface.ts index 47c7dd923..6d59b44b3 100644 --- a/backend/src/modules/boards/interfaces/services/update.board.service.interface.ts +++ b/backend/src/modules/boards/interfaces/services/update.board.service.interface.ts @@ -2,14 +2,18 @@ import BoardUserDto from 'src/modules/boards/dto/board.user.dto'; import { LeanDocument } from 'mongoose'; import { TeamDto } from 'src/modules/communication/dto/team.dto'; import { UpdateBoardDto } from '../../dto/update-board.dto'; -import { BoardDocument } from '../../entities/board.schema'; +import Board, { BoardDocument } from '../../entities/board.schema'; import BoardUser from '../../entities/board.user.schema'; import { BoardPhaseDto } from 'src/libs/dto/board-phase.dto'; export interface UpdateBoardServiceInterface { - update(boardId: string, boardData: UpdateBoardDto): Promise | null>; + update(boardId: string, boardData: UpdateBoardDto): Promise; - mergeBoards(subBoardId: string, userId: string): Promise | null>; + mergeBoards( + subBoardId: string, + userId: string, + socketId?: string + ): Promise | null>; updateChannelId(teams: TeamDto[]); updateBoardParticipants( diff --git a/backend/src/modules/boards/repositories/board.repository.interface.ts b/backend/src/modules/boards/repositories/board.repository.interface.ts index f2ff6ed8a..ad8ba74e8 100644 --- a/backend/src/modules/boards/repositories/board.repository.interface.ts +++ b/backend/src/modules/boards/repositories/board.repository.interface.ts @@ -30,8 +30,8 @@ export interface BoardRepositoryInterface extends BaseInterfaceRepository ): Promise; deleteBoard(boardId: string, withSession: boolean): Promise; updateBoard(boardId: string, board: Board, isNew: boolean): Promise; - updateMergedSubBoard(subBoardId: string, userId: string): Promise; - updateMergedBoard(boardId: string, newColumns: Column[]): Promise; + updateMergedSubBoard(subBoardId: string, userId: string, withSession: boolean): Promise; + updateMergedBoard(boardId: string, newColumns: Column[], withSession: boolean): Promise; updatedChannelId(boardId: string, channelId: string): Promise; updatePhase(boardId: string, phase: BoardPhases): Promise; } diff --git a/backend/src/modules/boards/repositories/board.repository.ts b/backend/src/modules/boards/repositories/board.repository.ts index ca3c0c096..c177e6a35 100644 --- a/backend/src/modules/boards/repositories/board.repository.ts +++ b/backend/src/modules/boards/repositories/board.repository.ts @@ -147,7 +147,7 @@ export class BoardRepository ); } - updateMergedSubBoard(subBoardId: string, userId: string) { + updateMergedSubBoard(subBoardId: string, userId: string, withSession: boolean) { return this.findOneByFieldAndUpdate( { _id: subBoardId @@ -157,11 +157,14 @@ export class BoardRepository submitedByUser: userId, submitedAt: new Date() } - } + }, + null, + null, + withSession ); } - updateMergedBoard(boardId: string, newColumns: Column[]) { + updateMergedBoard(boardId: string, newColumns: Column[], withSession: boolean) { return this.findOneByFieldAndUpdate( { _id: boardId @@ -169,7 +172,12 @@ export class BoardRepository { $set: { columns: newColumns } }, - { new: true } + { new: true }, + { + path: 'dividedBoards', + select: 'submitedByUser' + }, + withSession ); } diff --git a/backend/src/modules/boards/services/get.board.service.spec.ts b/backend/src/modules/boards/services/get.board.service.spec.ts index e87b537eb..c2ac896d1 100644 --- a/backend/src/modules/boards/services/get.board.service.spec.ts +++ b/backend/src/modules/boards/services/get.board.service.spec.ts @@ -16,8 +16,6 @@ import * as Auth from 'src/modules/auth/interfaces/types'; import faker from '@faker-js/faker'; import { BoardUserFactory } from 'src/libs/test-utils/mocks/factories/boardUser-factory.mock'; import { TeamFactory } from 'src/libs/test-utils/mocks/factories/team-factory.mock'; -import { UserDtoFactory } from 'src/libs/test-utils/mocks/factories/dto/userDto-factory'; -import { BadRequestException, NotFoundException } from '@nestjs/common'; import { GetTeamServiceInterface } from 'src/modules/teams/interfaces/services/get.team.service.interface'; import { GetTokenAuthServiceInterface } from 'src/modules/auth/interfaces/services/get-token.auth.service.interface'; import { Tokens } from 'src/libs/interfaces/jwt/tokens.interface'; @@ -28,6 +26,8 @@ import { hideVotes } from '../utils/clean-boards.spec'; import Column from 'src/modules/columns/entities/column.schema'; import { CreateBoardUserServiceInterface } from 'src/modules/boardusers/interfaces/services/create.board.user.service.interface'; import { createBoardUserService } from 'src/modules/boardusers/boardusers.providers'; +import { UserDtoFactory } from 'src/libs/test-utils/mocks/factories/dto/userDto-factory.mock'; +import { BadRequestException, NotFoundException } from '@nestjs/common'; const hideVotesFromColumns = (columns: Column[], userId: string) => { return columns.map((column) => { diff --git a/backend/src/modules/boards/services/update.board.service.spec.ts b/backend/src/modules/boards/services/update.board.service.spec.ts index 9182b71e0..d64456dcf 100644 --- a/backend/src/modules/boards/services/update.board.service.spec.ts +++ b/backend/src/modules/boards/services/update.board.service.spec.ts @@ -1,82 +1,109 @@ -import { DeleteBoardUserServiceInterface } from 'src/modules/boardusers/interfaces/services/delete.board.user.service.interface'; -import { UpdateBoardUserServiceInterface } from 'src/modules/boardusers/interfaces/services/update.board.user.service.interface'; -import { GetBoardUserServiceInterface } from 'src/modules/boardusers/interfaces/services/get.board.user.service.interface'; -import { CreateBoardUserServiceInterface } from 'src/modules/boardusers/interfaces/services/create.board.user.service.interface'; +import { updateBoardService } from './../boards.providers'; import { Test, TestingModule } from '@nestjs/testing'; -import { ConfigService } from '@nestjs/config'; +import { boardRepository } from '../boards.providers'; import SocketGateway from 'src/modules/socket/gateway/socket.gateway'; -import { EventEmitter2 } from '@nestjs/event-emitter'; +import { DeepMocked, createMock } from '@golevelup/ts-jest'; +import { UpdateBoardServiceInterface } from '../interfaces/services/update.board.service.interface'; +import { BoardRepositoryInterface } from '../repositories/board.repository.interface'; +import { DeleteCardServiceInterface } from 'src/modules/cards/interfaces/services/delete.card.service.interface'; +import { deleteCardService } from 'src/modules/cards/cards.providers'; import * as CommunicationsType from 'src/modules/communication/interfaces/types'; -import * as Cards from 'src/modules/cards/interfaces/types'; import * as Boards from 'src/modules/boards/interfaces/types'; +import * as Cards from 'src/modules/cards/interfaces/types'; import * as BoardUsers from 'src/modules/boardusers/interfaces/types'; -import * as Teams from 'src/modules/teams/interfaces/types'; -import { DeepMocked, createMock } from '@golevelup/ts-jest'; -import { BoardPhases } from 'src/libs/enum/board.phases'; -import { BadRequestException } from '@nestjs/common'; -import { BoardFactory } from 'src/libs/test-utils/mocks/factories/board-factory.mock'; -import { SLACK_ENABLE, SLACK_MASTER_CHANNEL_ID } from 'src/libs/constants/slack'; -import { FRONTEND_URL } from 'src/libs/constants/frontend'; import { CommunicationServiceInterface } from 'src/modules/communication/interfaces/slack-communication.service.interface'; import { SendMessageServiceInterface } from 'src/modules/communication/interfaces/send-message.service.interface'; -import { DeleteCardServiceInterface } from 'src/modules/cards/interfaces/services/delete.card.service.interface'; -import { BoardRepositoryInterface } from '../repositories/board.repository.interface'; -import { updateBoardService } from '../boards.providers'; -import { UpdateBoardServiceInterface } from '../interfaces/services/update.board.service.interface'; +import { EventEmitter2 } from '@nestjs/event-emitter'; +import { ConfigService } from '@nestjs/config'; +import { BoardFactory } from 'src/libs/test-utils/mocks/factories/board-factory.mock'; +import { UpdateBoardDtoFactory } from 'src/libs/test-utils/mocks/factories/dto/updateBoardDto-factory.mock'; +import { BoardUserFactory } from 'src/libs/test-utils/mocks/factories/boardUser-factory.mock'; +import { NotFoundException } from '@nestjs/common'; +import { BoardUserRepositoryInterface } from 'src/modules/boardusers/interfaces/repositories/board-user.repository.interface'; +import { + boardUserRepository, + createBoardUserService, + deleteBoardUserService, + getBoardUserService, + updateBoardUserService +} from 'src/modules/boardusers/boardusers.providers'; +import { UpdateBoardUserServiceInterface } from 'src/modules/boardusers/interfaces/services/update.board.user.service.interface'; +import { GetBoardUserServiceInterface } from 'src/modules/boardusers/interfaces/services/get.board.user.service.interface'; +import { CreateBoardUserServiceInterface } from 'src/modules/boardusers/interfaces/services/create.board.user.service.interface'; +import { DeleteBoardUserServiceInterface } from 'src/modules/boardusers/interfaces/services/delete.board.user.service.interface'; +import { BoardPhases } from 'src/libs/enum/board.phases'; import { TeamFactory } from 'src/libs/test-utils/mocks/factories/team-factory.mock'; +import { SLACK_ENABLE, SLACK_MASTER_CHANNEL_ID } from 'src/libs/constants/slack'; +import { FRONTEND_URL } from 'src/libs/constants/frontend'; +import ColumnDto from 'src/modules/columns/dto/column.dto'; +import faker from '@faker-js/faker'; +import { BoardRoles } from 'src/libs/enum/board.roles'; +import { BoardUserDtoFactory } from 'src/libs/test-utils/mocks/factories/dto/boardUserDto-factory.mock'; +import { UserFactory } from 'src/libs/test-utils/mocks/factories/user-factory'; +import User from 'src/modules/users/entities/user.schema'; +import { generateNewSubColumns } from '../utils/generate-subcolumns'; +import { mergeCardsFromSubBoardColumnsIntoMainBoard } from '../utils/merge-cards-from-subboard'; +import { TeamCommunicationDtoFactory } from 'src/libs/test-utils/mocks/factories/dto/teamDto-factory'; +import { UpdateFailedException } from 'src/libs/exceptions/updateFailedBadRequestException'; -describe('UpdateBoardService', () => { - let service: UpdateBoardServiceInterface; - let eventEmitterMock: DeepMocked; +describe('GetUpdateBoardService', () => { + let boardService: UpdateBoardServiceInterface; + let updateBoardUserServiceMock: DeepMocked; let boardRepositoryMock: DeepMocked; + let eventEmitterMock: DeepMocked; + let getBoardUserServiceMock: DeepMocked; let configServiceMock: DeepMocked; let slackSendMessageServiceMock: DeepMocked; + let slackCommunicationServiceMock: DeepMocked; + let deleteCardServiceMock: DeepMocked; + let createBoardUserServiceMock: DeepMocked; + let deleteBoardUserServiceMock: DeepMocked; - const boardPhaseDto = { boardId: '6405f9a04633b1668f71c068', phase: BoardPhases.ADDCARDS }; + let socketServiceMock: DeepMocked; beforeAll(async () => { const module: TestingModule = await Test.createTestingModule({ providers: [ updateBoardService, { - provide: Teams.TYPES.services.GetTeamService, - useValue: {} + provide: CommunicationsType.TYPES.services.SlackSendMessageService, + useValue: createMock() }, { provide: CommunicationsType.TYPES.services.SlackCommunicationService, useValue: createMock() }, { - provide: CommunicationsType.TYPES.services.SlackSendMessageService, - useValue: createMock() + provide: deleteCardService.provide, + useValue: createMock() }, { - provide: SocketGateway, - useValue: createMock() + provide: getBoardUserService.provide, + useValue: createMock() }, { - provide: Cards.TYPES.services.DeleteCardService, - useValue: createMock() + provide: createBoardUserService.provide, + useValue: createMock() }, { - provide: Boards.TYPES.repositories.BoardRepository, - useValue: createMock() + provide: updateBoardUserService.provide, + useValue: createMock() }, { - provide: BoardUsers.TYPES.services.CreateBoardUserService, - useValue: createMock() + provide: deleteBoardUserService.provide, + useValue: createMock() }, { - provide: BoardUsers.TYPES.services.GetBoardUserService, - useValue: createMock() + provide: boardRepository.provide, + useValue: createMock() }, { - provide: BoardUsers.TYPES.services.UpdateBoardUserService, - useValue: createMock() + provide: boardUserRepository.provide, + useValue: createMock() }, { - provide: BoardUsers.TYPES.services.DeleteBoardUserService, - useValue: createMock() + provide: SocketGateway, + useValue: createMock() }, { provide: EventEmitter2, @@ -88,52 +115,505 @@ describe('UpdateBoardService', () => { } ] }).compile(); - service = module.get(updateBoardService.provide); - eventEmitterMock = module.get(EventEmitter2); + + boardService = module.get(updateBoardService.provide); boardRepositoryMock = module.get(Boards.TYPES.repositories.BoardRepository); + updateBoardUserServiceMock = module.get(BoardUsers.TYPES.services.UpdateBoardUserService); + getBoardUserServiceMock = module.get(BoardUsers.TYPES.services.GetBoardUserService); + deleteCardServiceMock = module.get(Cards.TYPES.services.DeleteCardService); + eventEmitterMock = module.get(EventEmitter2); configServiceMock = module.get(ConfigService); slackSendMessageServiceMock = module.get( CommunicationsType.TYPES.services.SlackSendMessageService ); + slackCommunicationServiceMock = module.get( + CommunicationsType.TYPES.services.SlackCommunicationService + ); + socketServiceMock = module.get(SocketGateway); + createBoardUserServiceMock = module.get(BoardUsers.TYPES.services.CreateBoardUserService); + deleteBoardUserServiceMock = module.get(BoardUsers.TYPES.services.DeleteBoardUserService); }); beforeEach(() => { + jest.restoreAllMocks(); jest.clearAllMocks(); }); it('should be defined', () => { - expect(service).toBeDefined(); + expect(boardService).toBeDefined(); + }); + + describe('update', () => { + it('should throw an error if max votes is less than the highest votes on board', async () => { + const updateBoardDto = UpdateBoardDtoFactory.create({ maxVotes: 2 }); + const boardUsers = BoardUserFactory.createMany(2, [{ votesCount: 3 }, { votesCount: 1 }]); + + jest.spyOn(getBoardUserServiceMock, 'getVotesCount').mockResolvedValueOnce(boardUsers); + + expect(async () => await boardService.update('1', updateBoardDto)).rejects.toThrow( + UpdateFailedException + ); + }); + + it('should throw an error if board not found', async () => { + const updateBoardDto = UpdateBoardDtoFactory.create({ maxVotes: null }); + + boardRepositoryMock.getBoard.mockResolvedValue(null); + expect(async () => await boardService.update('-1', updateBoardDto)).rejects.toThrow( + NotFoundException + ); + }); + + it('should call the changeResponsibleOnBoard method if the current responsible is not equal to the new responsible', async () => { + const board = BoardFactory.create({ isSubBoard: true }); + const mainBoard = BoardFactory.create(); + const currentResponsible = BoardUserFactory.create({ + role: BoardRoles.RESPONSIBLE, + board: board._id + }); + const boardUsersDto = BoardUserDtoFactory.createMany(3, [ + { board: board._id, role: BoardRoles.RESPONSIBLE }, + { + board: board._id, + role: BoardRoles.MEMBER, + user: currentResponsible.user as User, + _id: String(currentResponsible._id) + }, + { board: board._id, role: BoardRoles.MEMBER } + ]); + const newResponsible = BoardUserFactory.create({ + board: board._id, + role: BoardRoles.RESPONSIBLE, + user: UserFactory.create({ _id: (boardUsersDto[0].user as User)._id }), + _id: String(boardUsersDto[0]._id) + }); + const updateBoardDto = UpdateBoardDtoFactory.create({ + responsible: newResponsible, + mainBoardId: mainBoard._id, + users: boardUsersDto, + maxVotes: null, + _id: board._id, + isSubBoard: true + }); + + boardRepositoryMock.getBoard.mockResolvedValueOnce(board); + + //gets the current responsible from the board + getBoardUserServiceMock.getBoardResponsible.mockResolvedValueOnce(currentResponsible); + + await boardService.update(board._id, updateBoardDto); + //update the changeResponsibleOnBoard + expect(updateBoardUserServiceMock.updateBoardUserRole).toBeCalled(); + }); + + it('should throw an error when update fails', async () => { + const updateBoardDto = UpdateBoardDtoFactory.create({ maxVotes: null }); + const board = BoardFactory.create(); + + boardRepositoryMock.getBoard.mockResolvedValue(board); + boardRepositoryMock.updateBoard.mockResolvedValueOnce(null); + + expect(async () => await boardService.update('1', updateBoardDto)).rejects.toThrow( + UpdateFailedException + ); + }); + + it('should call the socketService.sendUpdatedBoard if the socketId exists', async () => { + const board = BoardFactory.create(); + const updateBoardDto = UpdateBoardDtoFactory.create({ + maxVotes: null, + title: 'Mock 2.0', + _id: board._id, + socketId: faker.datatype.uuid() + }); + const boardResult = { ...board, title: updateBoardDto.title }; + + boardRepositoryMock.getBoard.mockResolvedValue(board); + boardRepositoryMock.updateBoard.mockResolvedValue(boardResult); + + await boardService.update(board._id, updateBoardDto); + + expect(socketServiceMock.sendUpdatedBoard).toBeCalledTimes(1); + }); + + it('should call the slackCommunicationService.executeResponsibleChange if the board has a newResponsible and slack enable', async () => { + const board = BoardFactory.create({ + isSubBoard: true, + slackEnable: true, + slackChannelId: faker.datatype.uuid() + }); + const mainBoard = BoardFactory.create(); + const currentResponsible = BoardUserFactory.create({ + role: BoardRoles.RESPONSIBLE, + board: board._id + }); + const boardUsersDto = BoardUserDtoFactory.createMany(3, [ + { board: board._id, role: BoardRoles.RESPONSIBLE }, + { + board: board._id, + role: BoardRoles.MEMBER, + user: currentResponsible.user as User, + _id: String(currentResponsible._id) + }, + { board: board._id, role: BoardRoles.MEMBER } + ]); + const newResponsible = BoardUserFactory.create({ + board: board._id, + role: BoardRoles.RESPONSIBLE, + user: UserFactory.create({ _id: (boardUsersDto[0].user as User)._id }), + _id: String(boardUsersDto[0]._id) + }); + const updateBoardDto = UpdateBoardDtoFactory.create({ + responsible: newResponsible, + mainBoardId: mainBoard._id, + users: boardUsersDto, + maxVotes: null, + _id: board._id, + isSubBoard: true + }); + + boardRepositoryMock.getBoard.mockResolvedValueOnce(board); + boardRepositoryMock.updateBoard.mockResolvedValueOnce(board); + + jest + .spyOn(getBoardUserServiceMock, 'getBoardResponsible') + .mockResolvedValue(currentResponsible); + + await boardService.update(board._id, updateBoardDto); + + expect(slackCommunicationServiceMock.executeResponsibleChange).toBeCalledTimes(1); + }); + + it('should update a split board', async () => { + const board = BoardFactory.create({ addCards: false }); + const updateBoardDto = UpdateBoardDtoFactory.create({ + maxVotes: null, + title: 'Mock 2.0', + _id: board._id, + addCards: true + }); + const boardResult = { ...board, title: updateBoardDto.title }; + + boardRepositoryMock.getBoard.mockResolvedValue(board); + boardRepositoryMock.updateBoard.mockResolvedValue(boardResult); + + const result = await boardService.update(board._id, updateBoardDto); + + expect(result).toEqual(boardResult); + }); + + it('should update a regular board', async () => { + const board = BoardFactory.create({ isSubBoard: false, dividedBoards: [] }); + + board.columns[1].title = 'Make things'; + board.columns[1].color = '#FEB9A9'; + + const updateBoardDto = UpdateBoardDtoFactory.create({ + maxVotes: null, + _id: board._id, + isSubBoard: false, + dividedBoards: [], + columns: board.columns as ColumnDto[], + deletedColumns: [board.columns[0]._id] + }); + + boardRepositoryMock.getBoard.mockResolvedValueOnce(board); + getBoardUserServiceMock.getBoardResponsible.mockResolvedValueOnce(null); + deleteCardServiceMock.deleteCardVotesFromColumn.mockResolvedValue(null); + + const boardResult = { ...board, columns: board.columns.slice(1) }; + + boardRepositoryMock.updateBoard.mockResolvedValue(boardResult); + + const result = await boardService.update(board._id, updateBoardDto); + + expect(result).toEqual(boardResult); + }); + }); + + describe('mergeBoards', () => { + it('should throw an error when the subBoard, board or subBoard.submittedByUser are undefined', async () => { + const userId = faker.datatype.uuid(); + const board = BoardFactory.create({ isSubBoard: false }); + + boardRepositoryMock.getBoard.mockResolvedValueOnce(null); + boardRepositoryMock.getBoardByQuery.mockResolvedValueOnce(board); + + expect(async () => await boardService.mergeBoards('-1', userId)).rejects.toThrowError( + NotFoundException + ); + }); + + it('should throw an error if the boardRepository.updateMergedSubBoard fails', async () => { + const userId = faker.datatype.uuid(); + const board = BoardFactory.create({ isSubBoard: false }); + const subBoard = BoardFactory.create({ isSubBoard: true }); + + boardRepositoryMock.getBoard.mockResolvedValueOnce(subBoard); + boardRepositoryMock.getBoardByQuery.mockResolvedValueOnce(board); + boardRepositoryMock.updateMergedSubBoard.mockResolvedValueOnce(null); + + expect(async () => await boardService.mergeBoards(subBoard._id, userId)).rejects.toThrowError( + UpdateFailedException + ); + }); + + it('should throw an error if the boardRepository.updateMergedBoard fails', async () => { + const userId = faker.datatype.uuid(); + const board = BoardFactory.create({ isSubBoard: false }); + const subBoard = BoardFactory.create({ isSubBoard: true }); + + boardRepositoryMock.getBoard.mockResolvedValueOnce(subBoard); + boardRepositoryMock.getBoardByQuery.mockResolvedValueOnce(board); + boardRepositoryMock.updateMergedBoard.mockResolvedValueOnce(null); + + expect(async () => await boardService.mergeBoards(subBoard._id, userId)).rejects.toThrowError( + UpdateFailedException + ); + }); + + it('should call the slackCommunicationService.executeMergeBoardNotification if the board has slackChannelId and slackEnable', async () => { + const userId = faker.datatype.uuid(); + const subBoards = BoardFactory.createMany(2, [ + { isSubBoard: true, boardNumber: 1, submitedByUser: userId, submitedAt: new Date() }, + { isSubBoard: true, boardNumber: 2 } + ]); + const board = BoardFactory.create({ + isSubBoard: false, + slackEnable: true, + slackChannelId: faker.datatype.uuid(), + dividedBoards: subBoards + }); + + boardRepositoryMock.getBoard.mockResolvedValueOnce(subBoards[1]); + boardRepositoryMock.getBoardByQuery.mockResolvedValueOnce(board); + + //mocks update of subBoard that is being merged + const subBoardUpdated = { ...subBoards[1], submitedByUser: userId, submitedAt: new Date() }; + boardRepositoryMock.updateMergedSubBoard.mockResolvedValueOnce(subBoardUpdated); + + //merges columns of the sub-boards to the main board + const newSubColumnsSubBoard_1 = generateNewSubColumns(subBoards[0]); + const newSubColumnsSubBoard_2 = generateNewSubColumns(subBoardUpdated); + + const mergeSubBoard_1 = { + ...board, + columns: mergeCardsFromSubBoardColumnsIntoMainBoard( + [...board.columns], + newSubColumnsSubBoard_1 + ) + }; + + const boardResult = { + ...mergeSubBoard_1, + columns: mergeCardsFromSubBoardColumnsIntoMainBoard( + [...board.columns], + newSubColumnsSubBoard_2 + ), + dividedBoards: [subBoards[0], subBoardUpdated] + }; + + boardRepositoryMock.updateMergedBoard.mockResolvedValueOnce(boardResult); + + await boardService.mergeBoards(subBoards[1]._id, userId); + + expect(slackCommunicationServiceMock.executeMergeBoardNotification).toBeCalledTimes(1); + }); + + it('should call the socketService.sendUpdatedAllBoard if there is a socketId', async () => { + const userId = faker.datatype.uuid(); + const board = BoardFactory.create({ + isSubBoard: false, + slackEnable: true, + slackChannelId: faker.datatype.uuid() + }); + const subBoard = BoardFactory.create({ isSubBoard: true }); + const socketId = faker.datatype.uuid(); + + boardRepositoryMock.getBoard.mockResolvedValueOnce(subBoard); + boardRepositoryMock.getBoardByQuery.mockResolvedValueOnce(board); + boardRepositoryMock.updateMergedSubBoard.mockResolvedValueOnce(subBoard); + boardRepositoryMock.updateMergedBoard.mockResolvedValueOnce(board); + + await boardService.mergeBoards(subBoard._id, userId, socketId); + + expect(socketServiceMock.sendUpdatedAllBoard).toBeCalledTimes(1); + }); + + it('should return the merged board', async () => { + const userId = faker.datatype.uuid(); + const subBoards = BoardFactory.createMany(2, [ + { isSubBoard: true, boardNumber: 1 }, + { isSubBoard: true, boardNumber: 2 } + ]); + const board = BoardFactory.create({ + isSubBoard: false, + slackEnable: true, + slackChannelId: faker.datatype.uuid(), + dividedBoards: subBoards + }); + + boardRepositoryMock.getBoard.mockResolvedValueOnce(subBoards[0]); + boardRepositoryMock.getBoardByQuery.mockResolvedValueOnce(board); + + //mocks update of subBoard that is being merged + const subBoardUpdated = { ...subBoards[0], submitedByUser: userId, submitedAt: new Date() }; + boardRepositoryMock.updateMergedSubBoard.mockResolvedValueOnce(subBoardUpdated); + + //merges columns of the sub-boards to the main board + const newSubColumnsSubBoard_1 = generateNewSubColumns(subBoards[1]); + const newSubColumnsSubBoard_2 = generateNewSubColumns(subBoardUpdated); + + const mergeSubBoard_1 = { + ...board, + columns: mergeCardsFromSubBoardColumnsIntoMainBoard( + [...board.columns], + newSubColumnsSubBoard_1 + ) + }; + + const boardResult = { + ...mergeSubBoard_1, + columns: mergeCardsFromSubBoardColumnsIntoMainBoard( + [...board.columns], + newSubColumnsSubBoard_2 + ), + dividedBoards: [subBoardUpdated, subBoards[1]] + }; + + boardRepositoryMock.updateMergedBoard.mockResolvedValueOnce(boardResult); + + const result = await boardService.mergeBoards(subBoards[0]._id, userId); + + expect(result).toEqual(boardResult); + }); + }); + + describe('updateChannelId', () => { + it('should call the boardRepository.updatedChannelId', async () => { + const teamsDto = TeamCommunicationDtoFactory.createMany(2); + const board = BoardFactory.create(); + + boardRepositoryMock.updatedChannelId.mockResolvedValue(board); + boardService.updateChannelId(teamsDto); + + expect(boardRepositoryMock.updatedChannelId).toBeCalledTimes(teamsDto.length); + }); + }); + + describe('updateBoardParticipants', () => { + it('should throw an error when the inserting of board users fails', async () => { + const addUsers = BoardUserDtoFactory.createMany(3); + const removedUsers = []; + + createBoardUserServiceMock.saveBoardUsers.mockRejectedValueOnce('Error inserting users'); + expect( + async () => await boardService.updateBoardParticipants(addUsers, removedUsers) + ).rejects.toThrowError(UpdateFailedException); + }); + + it('should throw an error when the deleting of board users fails', async () => { + const addUsers = BoardUserDtoFactory.createMany(3); + const boardUserToRemove = BoardUserFactory.create(); + const removedUsers = [boardUserToRemove._id]; + + deleteBoardUserServiceMock.deleteBoardUsers.mockResolvedValueOnce(0); + expect( + async () => await boardService.updateBoardParticipants(addUsers, removedUsers) + ).rejects.toThrowError(UpdateFailedException); + }); + + it('should return the created boardUsers', async () => { + const addUsers = BoardUserDtoFactory.createMany(2); + const boardUserToRemove = BoardUserFactory.create(); + const removedUsers = [boardUserToRemove._id]; + const saveBoardUsersResult = BoardUserFactory.createMany(2, [ + { + _id: addUsers[0]._id, + role: addUsers[0].role, + user: addUsers[0].user, + board: addUsers[0].board + }, + { + _id: addUsers[1]._id, + role: addUsers[1].role, + user: addUsers[1].user, + board: addUsers[1].board + } + ]); + + createBoardUserServiceMock.saveBoardUsers.mockResolvedValueOnce(saveBoardUsersResult); + deleteBoardUserServiceMock.deleteBoardUsers.mockResolvedValueOnce(1); + + const boardUsersCreatedResult = await boardService.updateBoardParticipants( + addUsers, + removedUsers + ); + expect(boardUsersCreatedResult).toEqual(saveBoardUsersResult); + }); + }); + + describe('updateBoardParticipantsRole', () => { + it('should throw an error if the updateBoardUserRole fails', async () => { + const boardUserDto = BoardUserDtoFactory.create(); + + updateBoardUserServiceMock.updateBoardUserRole.mockResolvedValueOnce(null); + + expect( + async () => await boardService.updateBoardParticipantsRole(boardUserDto) + ).rejects.toThrowError(UpdateFailedException); + }); + + it('should return the boardUser with the updated role', async () => { + const boardUserDto = BoardUserDtoFactory.create({ role: BoardRoles.MEMBER }); + const boardUserUpdated = BoardUserFactory.create({ + _id: boardUserDto._id, + role: BoardRoles.RESPONSIBLE, + user: boardUserDto.user, + board: boardUserDto.board + }); + + updateBoardUserServiceMock.updateBoardUserRole.mockResolvedValueOnce(boardUserUpdated); + + const boardUserResult = await boardService.updateBoardParticipantsRole(boardUserDto); + + expect(boardUserResult).toEqual(boardUserUpdated); + }); }); describe('updatePhase', () => { it('should be defined', () => { - expect(service.updatePhase).toBeDefined(); + expect(boardService.updatePhase).toBeDefined(); }); - it('should call boardRepository ', async () => { - await service.updatePhase(boardPhaseDto); + it('should call the boardRepository ', async () => { + const boardPhaseDto = { boardId: '6405f9a04633b1668f71c068', phase: BoardPhases.ADDCARDS }; + await boardService.updatePhase(boardPhaseDto); expect(boardRepositoryMock.updatePhase).toBeCalledTimes(1); }); - it('should throw badRequestException when boardRepository fails', async () => { + it('should throw the badRequestException when the boardRepository fails', async () => { + const boardPhaseDto = { boardId: '6405f9a04633b1668f71c068', phase: BoardPhases.ADDCARDS }; // Set up the board repository mock to reject with an error boardRepositoryMock.updatePhase.mockRejectedValueOnce(new Error('Some error')); - // Verify that the service method being tested throws a BadRequestException - expect(async () => await service.updatePhase(boardPhaseDto)).rejects.toThrowError( - BadRequestException + // Verify that the service method that is being tested throws a BadRequestException + expect(async () => await boardService.updatePhase(boardPhaseDto)).rejects.toThrowError( + UpdateFailedException ); }); - it('should call websocket with eventEmitter', async () => { + it('should call the websocket with eventEmitter', async () => { + const boardPhaseDto = { boardId: '6405f9a04633b1668f71c068', phase: BoardPhases.ADDCARDS }; // Call the service method being tested - await service.updatePhase(boardPhaseDto); + await boardService.updatePhase(boardPhaseDto); // Verify that the eventEmitterMock.emit method was called exactly once expect(eventEmitterMock.emit).toHaveBeenCalledTimes(1); }); - it('should call slackSendMessageService.execute once with slackMessageDto', async () => { + it('should call the slackSendMessageService.execute once with slackMessageDto', async () => { + const boardPhaseDto = { boardId: '6405f9a04633b1668f71c068', phase: BoardPhases.ADDCARDS }; // Create a fake board object with the specified properties const board = BoardFactory.create(); board.team = TeamFactory.create({ name: 'xgeeks' }); @@ -156,7 +636,17 @@ describe('UpdateBoardService', () => { }); // Call the service method being tested - await service.updatePhase(boardPhaseDto); + await boardService.updatePhase(boardPhaseDto); + + // Verify that the slackSendMessageService.execute method with correct data 1 time + expect(slackSendMessageServiceMock.execute).toHaveBeenNthCalledWith(1, { + slackChannelId: '6405f9a04633b1668f71c068', + message: expect.stringContaining('https://split.kigroup.de/') + }); + + board.phase = BoardPhases.VOTINGPHASE; + + await boardService.updatePhase(boardPhaseDto); // Verify that the slackSendMessageService.execute method with correct data 1 time expect(slackSendMessageServiceMock.execute).toHaveBeenNthCalledWith(1, { diff --git a/backend/src/modules/boards/services/update.board.service.ts b/backend/src/modules/boards/services/update.board.service.ts index 67281caac..73d34d5ef 100644 --- a/backend/src/modules/boards/services/update.board.service.ts +++ b/backend/src/modules/boards/services/update.board.service.ts @@ -1,20 +1,11 @@ import { UpdateBoardUserServiceInterface } from './../../boardusers/interfaces/services/update.board.user.service.interface'; import BoardUserDto from 'src/modules/boards/dto/board.user.dto'; -import { - BadRequestException, - Inject, - Injectable, - NotFoundException, - forwardRef -} from '@nestjs/common'; -import { ObjectId } from 'mongoose'; +import { Inject, Injectable, Logger, NotFoundException } from '@nestjs/common'; import { getIdFromObjectId } from 'src/libs/utils/getIdFromObjectId'; import isEmpty from 'src/libs/utils/isEmpty'; import { TeamDto } from 'src/modules/communication/dto/team.dto'; import { CommunicationServiceInterface } from 'src/modules/communication/interfaces/slack-communication.service.interface'; import * as CommunicationsType from 'src/modules/communication/interfaces/types'; -import { GetTeamServiceInterface } from 'src/modules/teams/interfaces/services/get.team.service.interface'; -import * as Teams from 'src/modules/teams/interfaces/types'; import * as Cards from 'src/modules/cards/interfaces/types'; import * as Boards from 'src/modules/boards/interfaces/types'; import * as BoardUsers from 'src/modules/boardusers/interfaces/types'; @@ -24,7 +15,6 @@ import { ResponsibleType } from '../interfaces/responsible.interface'; import { UpdateBoardServiceInterface } from '../interfaces/services/update.board.service.interface'; import Board from '../entities/board.schema'; import BoardUser from '../entities/board.user.schema'; -import { DELETE_FAILED, INSERT_FAILED, UPDATE_FAILED } from 'src/libs/exceptions/messages'; import SocketGateway from 'src/modules/socket/gateway/socket.gateway'; import Column from '../../columns/entities/column.schema'; import ColumnDto from '../../columns/dto/column.dto'; @@ -44,12 +34,15 @@ import Team from 'src/modules/teams/entities/teams.schema'; import { GetBoardUserServiceInterface } from 'src/modules/boardusers/interfaces/services/get.board.user.service.interface'; import { CreateBoardUserServiceInterface } from 'src/modules/boardusers/interfaces/services/create.board.user.service.interface'; import { DeleteBoardUserServiceInterface } from 'src/modules/boardusers/interfaces/services/delete.board.user.service.interface'; +import { generateNewSubColumns } from '../utils/generate-subcolumns'; +import { mergeCardsFromSubBoardColumnsIntoMainBoard } from '../utils/merge-cards-from-subboard'; +import { UpdateFailedException } from 'src/libs/exceptions/updateFailedBadRequestException'; @Injectable() export default class UpdateBoardService implements UpdateBoardServiceInterface { + private logger = new Logger(UpdateBoardService.name); + constructor( - @Inject(forwardRef(() => Teams.TYPES.services.GetTeamService)) - private getTeamService: GetTeamServiceInterface, @Inject(CommunicationsType.TYPES.services.SlackCommunicationService) private slackCommunicationService: CommunicationServiceInterface, @Inject(CommunicationsType.TYPES.services.SlackSendMessageService) @@ -72,6 +65,23 @@ export default class UpdateBoardService implements UpdateBoardServiceInterface { ) {} async update(boardId: string, boardData: UpdateBoardDto) { + /** + * Only can change the maxVotes if: + * - new maxVotes not empty + * - current highest votes equals to zero + * - or current highest votes lower than new maxVotes + */ + + if (!isEmpty(boardData.maxVotes)) { + const highestVotes = await this.getHighestVotesOnBoard(boardId); + + if (highestVotes > Number(boardData.maxVotes)) { + throw new UpdateFailedException( + `You can't set a lower value to max votes. Please insert a value higher or equals than ${highestVotes}!` + ); + } + } + const board = await this.boardRepository.getBoard(boardId); if (!board) { @@ -94,43 +104,14 @@ export default class UpdateBoardService implements UpdateBoardServiceInterface { * - and the current responsible isn't the new responsible */ if (boardData.users && String(currentResponsible?.id) !== String(newResponsible?.id)) { - if (isSubBoard) { - const promises = boardData.users - .filter((boardUser) => - [getIdFromObjectId(String(currentResponsible?.id)), String(newResponsible.id)].includes( - (boardUser.user as unknown as User)._id - ) - ) - .map((boardUser) => { - const typedBoardUser = boardUser.user as unknown as User; - - return this.updateBoardUserService.updateBoardUserRole( - boardId, - typedBoardUser._id, - boardUser.role - ); - }); - await Promise.all(promises); - } - - const mainBoardId = boardData.mainBoardId; - - const promises = boardData.users - .filter((boardUser) => - [getIdFromObjectId(String(currentResponsible?.id)), newResponsible.id].includes( - (boardUser.user as unknown as User)._id - ) - ) - .map((boardUser) => { - const typedBoardUser = boardUser.user as unknown as User; - - return this.updateBoardUserService.updateBoardUserRole( - mainBoardId, - typedBoardUser._id, - boardUser.role - ); - }); - await Promise.all(promises); + this.changeResponsibleOnBoard( + isSubBoard, + boardId, + boardData.mainBoardId, + boardData.users, + String(currentResponsible.id), + String(newResponsible.id) + ); } /** @@ -154,27 +135,9 @@ export default class UpdateBoardService implements UpdateBoardServiceInterface { board.columns = await this.updateRegularBoard(boardId, boardData, board); } - /** - * Only can change the maxVotes if: - * - new maxVotes not empty - * - current highest votes equals to zero - * - or current highest votes lower than new maxVotes - */ - - if (!isEmpty(boardData.maxVotes)) { - const highestVotes = await this.getHighestVotesOnBoard(boardId); - - // TODO: maxVotes as 'undefined' not undefined (so typeof returns string, but needs to be number or undefined) - if (highestVotes > Number(boardData.maxVotes)) { - throw new BadRequestException( - `You can't set a lower value to max votes. Please insert a value higher or equals than ${highestVotes}!` - ); - } - } - const updatedBoard = await this.boardRepository.updateBoard(boardId, board, true); - if (!updatedBoard) throw new BadRequestException(UPDATE_FAILED); + if (!updatedBoard) throw new UpdateFailedException(); if (boardData.socketId) { this.socketService.sendUpdatedBoard(boardId, boardData.socketId); @@ -201,39 +164,69 @@ export default class UpdateBoardService implements UpdateBoardServiceInterface { return updatedBoard; } - async mergeBoards(subBoardId: string, userId: string) { + async mergeBoards(subBoardId: string, userId: string, socketId?: string) { const [subBoard, board] = await Promise.all([ this.boardRepository.getBoard(subBoardId), this.boardRepository.getBoardByQuery({ dividedBoards: { $in: [subBoardId] } }) ]); - if (!subBoard || !board || subBoard.submitedByUser) return null; - const team = await this.getTeamService.getTeam((board.team as ObjectId).toString()); + if (!subBoard || !board || subBoard.submitedByUser) { + throw new NotFoundException('Board or subBoard not found'); + } - if (!team) return null; + const columnsWithMergedCards = this.getColumnsFromMainBoardWithMergedCards(subBoard, board); - const newSubColumns = this.generateNewSubColumns(subBoard as Board); + await this.boardRepository.startTransaction(); + try { + const updatedMergedSubBoard = await this.boardRepository.updateMergedSubBoard( + subBoardId, + userId, + true + ); - const newColumns = [...(board as Board).columns]; - for (let i = 0; i < newColumns.length; i++) { - newColumns[i].cards = [...newColumns[i].cards, ...newSubColumns[i].cards]; - } + if (!updatedMergedSubBoard) { + this.logger.error('Update of the subBoard to be merged failed'); + throw new UpdateFailedException(); + } - await this.boardRepository.updateMergedSubBoard(subBoardId, userId); + const mergedBoard = await this.boardRepository.updateMergedBoard( + board._id, + columnsWithMergedCards, + true + ); - const result = await this.boardRepository.updateMergedBoard(board._id, newColumns); + if (!mergedBoard) { + this.logger.error('Update of the merged board failed'); + throw new UpdateFailedException(); + } - if (board.slackChannelId && board.slackEnable) { - this.slackCommunicationService.executeMergeBoardNotification({ - responsiblesChannelId: board.slackChannelId, - teamNumber: subBoard.boardNumber, - isLastSubBoard: await this.checkIfIsLastBoardToMerge(result.dividedBoards as Board[]), - boardId: subBoardId, - mainBoardId: board._id - }); + if (board.slackChannelId && board.slackEnable) { + this.slackCommunicationService.executeMergeBoardNotification({ + responsiblesChannelId: board.slackChannelId, + teamNumber: subBoard.boardNumber, + isLastSubBoard: await this.checkIfIsLastBoardToMerge( + mergedBoard.dividedBoards as Board[] + ), + boardId: subBoardId, + mainBoardId: board._id + }); + } + + if (socketId) { + this.socketService.sendUpdatedAllBoard(subBoardId, socketId); + } + + await this.boardRepository.commitTransaction(); + await this.boardRepository.endSession(); + + return mergedBoard; + } catch (e) { + await this.boardRepository.abortTransaction(); + } finally { + await this.boardRepository.endSession(); } - return result; + throw new UpdateFailedException(); } updateChannelId(teams: TeamDto[]) { @@ -246,30 +239,31 @@ export default class UpdateBoardService implements UpdateBoardServiceInterface { try { let createdBoardUsers: BoardUser[] = []; - if (addUsers.length > 0) createdBoardUsers = await this.addBoardUsers(addUsers); + if (addUsers.length > 0) + createdBoardUsers = await this.createBoardUserService.saveBoardUsers(addUsers); if (removeUsers.length > 0) await this.deleteBoardUsers(removeUsers); return createdBoardUsers; } catch (error) { - throw new BadRequestException(UPDATE_FAILED); + throw new UpdateFailedException(); } } async updateBoardParticipantsRole(boardUserToUpdateRole: BoardUserDto) { - const user = boardUserToUpdateRole.user as unknown as User; + const user = boardUserToUpdateRole.user as User; - const updatedBoardUsers = await this.updateBoardUserService.updateBoardUserRole( + const updatedBoardUser = await this.updateBoardUserService.updateBoardUserRole( boardUserToUpdateRole.board, user._id, boardUserToUpdateRole.role ); - if (!updatedBoardUsers) { - throw new BadRequestException(UPDATE_FAILED); + if (!updatedBoardUser) { + throw new UpdateFailedException(); } - return updatedBoardUsers; + return updatedBoardUser; } async updatePhase(boardPhaseDto: BoardPhaseDto) { @@ -294,12 +288,26 @@ export default class UpdateBoardService implements UpdateBoardServiceInterface { this.slackSendMessageService.execute(slackMessageDto); } } catch (err) { - throw new BadRequestException(UPDATE_FAILED); + throw new UpdateFailedException(); } } /* --------------- HELPERS --------------- */ + /** + * Method to get the highest value of votesCount on Board Users + * @param boardId String + * @return number + */ + private async getHighestVotesOnBoard(boardId: string): Promise { + const votesCount = await this.getBoardUserService.getVotesCount(boardId); + + return votesCount.reduce( + (prev, current) => (current.votesCount > prev ? current.votesCount : prev), + 0 + ); + } + /** * Method to get current responsible to a specific board * @param boardId String @@ -319,17 +327,55 @@ export default class UpdateBoardService implements UpdateBoardServiceInterface { } /** - * Method to get the highest value of votesCount on Board Users + * Method to update all boardUsers role * @param boardId String - * @return number + * @param boardUsers BoardUserDto[] + * @param currentResponsibleId String + * @param newResponsibleId String + * @return void + * @private */ - private async getHighestVotesOnBoard(boardId: string): Promise { - const votesCount = await this.getBoardUserService.getVotesCount(boardId); + private async updateBoardUsersRole( + boardId: string, + boardUsers: BoardUserDto[], + currentResponsibleId: string, + newResponsibleId: string + ) { + const promises = boardUsers + .filter((boardUser) => + [getIdFromObjectId(currentResponsibleId), newResponsibleId].includes( + (boardUser.user as User)._id + ) + ) + .map((boardUser) => { + const typedBoardUser = boardUser.user as User; + + return this.updateBoardUserService.updateBoardUserRole( + boardId, + typedBoardUser._id, + boardUser.role + ); + }); + await Promise.all(promises); + } - return votesCount.reduce( - (prev, current) => (current.votesCount > prev ? current.votesCount : prev), - 0 - ); + /** + * Method to change board responsible + * @return void + */ + private changeResponsibleOnBoard( + isSubBoard: boolean, + boardId: string, + mainBoardId: string, + users: BoardUserDto[], + currentResponsibleId: string, + newResponsibleId: string + ) { + if (isSubBoard) { + this.updateBoardUsersRole(boardId, users, currentResponsibleId, newResponsibleId); + } + + this.updateBoardUsersRole(mainBoardId, users, currentResponsibleId, newResponsibleId); } private async updateRegularBoard(boardId: string, boardData: UpdateBoardDto, board: Board) { @@ -350,17 +396,12 @@ export default class UpdateBoardService implements UpdateBoardServiceInterface { * Updates the columns * * */ - const columns = boardData.columns.flatMap((col: Column | ColumnDto) => { if (col._id) { const columnBoard = board.columns.find( (colBoard) => colBoard._id.toString() === col._id.toString() ); - if (columnBoard) { - return [{ ...columnBoard, title: col.title }]; - } - if (boardData.deletedColumns) { const columnToDelete = boardData.deletedColumns.some( (colId) => colId === col._id.toString() @@ -370,6 +411,10 @@ export default class UpdateBoardService implements UpdateBoardServiceInterface { return []; } } + + if (columnBoard) { + return [{ ...columnBoard, title: col.title }]; + } } return [{ ...col }]; @@ -408,51 +453,16 @@ export default class UpdateBoardService implements UpdateBoardServiceInterface { return count === dividedBoards.length; } - private generateNewSubColumns(subBoard: Board) { - return [...subBoard.columns].map((column) => { - const newColumn = { - title: column.title, - color: column.color, - cards: column.cards.map((card) => { - const newCard = { - text: card.text, - createdBy: card.createdBy, - votes: card.votes, - anonymous: card.anonymous, - createdByTeam: subBoard.title.replace('board', ''), - comments: card.comments.map((comment) => { - return { - text: comment.text, - createdBy: comment.createdBy, - anonymous: comment.anonymous - }; - }), - items: card.items.map((cardItem) => { - return { - text: cardItem.text, - votes: cardItem.votes, - createdByTeam: subBoard.title.replace('board ', ''), - createdBy: cardItem.createdBy, - anonymous: cardItem.anonymous, - comments: cardItem.comments.map((comment) => { - return { - text: comment.text, - createdBy: comment.createdBy, - anonymous: comment.anonymous - }; - }), - createdAt: card.createdAt - }; - }), - createdAt: card.createdAt - }; - - return newCard; - }) - }; - - return newColumn; - }); + /** + * Method to return columns with cards merged cards a from sub-board + * @param subBoard: Board + * @param board: Board + * @return Column[] + */ + private getColumnsFromMainBoardWithMergedCards(subBoard: Board, board: Board) { + const newSubColumns = generateNewSubColumns(subBoard); + + return mergeCardsFromSubBoardColumnsIntoMainBoard([...board.columns], newSubColumns); } private generateMessage(phase: string, boardId: string, date: string, columns): string { @@ -488,17 +498,9 @@ export default class UpdateBoardService implements UpdateBoardServiceInterface { } } - private async addBoardUsers(boardUsers: BoardUserDto[]) { - const createdBoardUsers = await this.createBoardUserService.saveBoardUsers(boardUsers); - - if (createdBoardUsers.length < 1) throw new Error(INSERT_FAILED); - - return createdBoardUsers; - } - private async deleteBoardUsers(boardUsers: string[]) { const deletedCount = await this.deleteBoardUserService.deleteBoardUsers(boardUsers); - if (deletedCount <= 0) throw new Error(DELETE_FAILED); + if (deletedCount <= 0) throw new UpdateFailedException(); } } diff --git a/backend/src/modules/boards/utils/generate-subcolumns.ts b/backend/src/modules/boards/utils/generate-subcolumns.ts new file mode 100644 index 000000000..86b42eb60 --- /dev/null +++ b/backend/src/modules/boards/utils/generate-subcolumns.ts @@ -0,0 +1,56 @@ +import Column from 'src/modules/columns/entities/column.schema'; +import Board from '../entities/board.schema'; + +/** + * Method to generate columns from sub-board + * @param subBoard: Board + * @return Column[] + */ +export const generateNewSubColumns = (subBoard: Board) => { + return [...subBoard.columns].map((column) => { + const newColumn = { + title: column.title, + color: column.color, + cardText: column.cardText, + isDefaultText: column.isDefaultText, + cards: column.cards.map((card) => { + const newCard = { + text: card.text, + createdBy: card.createdBy, + votes: card.votes, + anonymous: card.anonymous, + createdByTeam: subBoard.title.replace('board', ''), + comments: card.comments.map((comment) => { + return { + text: comment.text, + createdBy: comment.createdBy, + anonymous: comment.anonymous + }; + }), + items: card.items.map((cardItem) => { + return { + text: cardItem.text, + votes: cardItem.votes, + createdByTeam: subBoard.title.replace('board ', ''), + createdBy: cardItem.createdBy, + anonymous: cardItem.anonymous, + comments: cardItem.comments.map((comment) => { + return { + text: comment.text, + createdBy: comment.createdBy, + anonymous: comment.anonymous + }; + }), + createdAt: card.createdAt + }; + }), + createdAt: card.createdAt + }; + + return newCard; + }) + }; + + return newColumn as Column; + }); +}; diff --git a/backend/src/modules/boards/utils/merge-cards-from-subboard.ts b/backend/src/modules/boards/utils/merge-cards-from-subboard.ts new file mode 100644 index 000000000..e7d8441d3 --- /dev/null +++ b/backend/src/modules/boards/utils/merge-cards-from-subboard.ts @@ -0,0 +1,18 @@ +import Column from 'src/modules/columns/entities/column.schema'; + +/** + * Method to merge cards from sub-board into a main board + * @param columns: Column[] + * @param subColumns: Column[] + * @return Column[] + */ +export const mergeCardsFromSubBoardColumnsIntoMainBoard = ( + columns: Column[], + subColumns: Column[] +) => { + for (let i = 0; i < columns.length; i++) { + columns[i].cards = [...columns[i].cards, ...subColumns[i].cards]; + } + + return columns; +}; diff --git a/backend/src/modules/communication/applications/slack-communication.application.ts b/backend/src/modules/communication/applications/slack-communication.application.ts index d470925a9..84ea816a7 100644 --- a/backend/src/modules/communication/applications/slack-communication.application.ts +++ b/backend/src/modules/communication/applications/slack-communication.application.ts @@ -1,6 +1,6 @@ import { Logger } from '@nestjs/common'; import { get_nth_suffix } from 'src/libs/utils/ordinal-date'; -import { TeamDto } from 'src/modules/communication/dto/team.dto'; +import { ForTeamDtoEnum, TeamDto } from 'src/modules/communication/dto/team.dto'; import { BoardRoles, BoardType, ConfigurationType } from 'src/modules/communication/dto/types'; import { UserDto } from 'src/modules/communication/dto/user.dto'; import { BoardNotValidError } from 'src/modules/communication/errors/board-not-valid.error'; @@ -195,7 +195,7 @@ export class SlackCommunicationApplication implements CommunicationApplicationIn name: board.title, normalName: normalizeName(board.team.name), boardId: board.id, - type: board.isSubBoard ? 'sub-team' : 'team', + type: board.isSubBoard ? ForTeamDtoEnum.SUBTEAM : ForTeamDtoEnum.TEAM, for: BoardRoles.RESPONSIBLE, teamNumber: 0, participants: board.isSubBoard @@ -219,7 +219,7 @@ export class SlackCommunicationApplication implements CommunicationApplicationIn name: subBoard.title, normalName: normalizeName(board.team.name + '-' + subBoard.title.replace(' board', '')), boardId: subBoard.id, - type: 'sub-team', + type: ForTeamDtoEnum.SUBTEAM, for: BoardRoles.MEMBER, participants, teamNumber: subBoard.boardNumber diff --git a/backend/src/modules/communication/dto/team.dto.ts b/backend/src/modules/communication/dto/team.dto.ts index 009e5b6a1..f19f82f84 100644 --- a/backend/src/modules/communication/dto/team.dto.ts +++ b/backend/src/modules/communication/dto/team.dto.ts @@ -1,6 +1,11 @@ import { BoardRoles } from 'src/modules/communication/dto/types'; import { UserDto } from 'src/modules/communication/dto/user.dto'; +export enum ForTeamDtoEnum { + TEAM = 'team', + SUBTEAM = 'sub-team' +} + export class TeamDto { name!: string; @@ -10,7 +15,7 @@ export class TeamDto { channelId?: string; - type!: 'team' | 'sub-team'; + type!: ForTeamDtoEnum; for!: BoardRoles;