diff --git a/nestjs-BE/server/src/boards/boards.controller.spec.ts b/nestjs-BE/server/src/boards/boards.controller.spec.ts index b0b99a68..21341ba6 100644 --- a/nestjs-BE/server/src/boards/boards.controller.spec.ts +++ b/nestjs-BE/server/src/boards/boards.controller.spec.ts @@ -1,17 +1,13 @@ import { Test, TestingModule } from '@nestjs/testing'; +import { ConfigModule, ConfigService } from '@nestjs/config'; +import { HttpStatus, NotFoundException } from '@nestjs/common'; import { BoardsController } from './boards.controller'; import { BoardsService } from './boards.service'; -import { UploadService } from '../upload/upload.service'; -import { Board } from './schemas/board.schema'; import { CreateBoardDto } from './dto/create-board.dto'; -import { HttpStatus, NotFoundException } from '@nestjs/common'; -import { UpdateWriteOpResult } from 'mongoose'; -import { ConfigModule, ConfigService } from '@nestjs/config'; describe('BoardsController', () => { let controller: BoardsController; let boardsService: BoardsService; - let uploadService: UploadService; let configService: ConfigService; beforeEach(async () => { @@ -22,126 +18,120 @@ describe('BoardsController', () => { { provide: BoardsService, useValue: { - create: jest.fn(), + createBoard: jest.fn(), deleteBoard: jest.fn(), restoreBoard: jest.fn(), }, }, - { - provide: UploadService, - useValue: { - uploadFile: jest.fn(), - }, - }, ], }).compile(); controller = module.get(BoardsController); boardsService = module.get(BoardsService); - uploadService = module.get(UploadService); configService = module.get(ConfigService); }); - it('createBoard created board', async () => { + describe('createBoard', () => { const bodyMock = { boardName: 'board name', spaceId: 'space uuid', } as CreateBoardDto; const imageMock = { filename: 'image' } as Express.Multer.File; - jest.spyOn(uploadService, 'uploadFile').mockResolvedValue('image url'); - jest.spyOn(boardsService, 'create').mockResolvedValue({ - uuid: 'board uuid', - createdAt: 'created date' as unknown as Date, - } as Board); - - const board = controller.createBoard(bodyMock, imageMock); - - await expect(board).resolves.toEqual({ - statusCode: HttpStatus.CREATED, - message: 'Board created.', - data: { - boardId: 'board uuid', - date: 'created date', + + it('created board', async () => { + (boardsService.createBoard as jest.Mock).mockResolvedValue({ + uuid: 'board uuid', + createdAt: 'created date', imageUrl: 'image url', - }, + }); + + const board = controller.createBoard(bodyMock, imageMock); + + await expect(board).resolves.toEqual({ + statusCode: HttpStatus.CREATED, + message: 'Board created.', + data: { + boardId: 'board uuid', + date: 'created date', + imageUrl: 'image url', + }, + }); }); - expect(uploadService.uploadFile).toHaveBeenCalled(); - }); - it('createBoard request does not have image file', async () => { - const bodyMock = { - boardName: 'board name', - spaceId: 'space uuid', - } as CreateBoardDto; - jest.spyOn(boardsService, 'create').mockResolvedValue({ - uuid: 'board uuid', - createdAt: 'created date' as unknown as Date, - } as Board); - - const response = controller.createBoard( - bodyMock, - null as unknown as Express.Multer.File, - ); - - await expect(response).resolves.toEqual({ - statusCode: HttpStatus.CREATED, - message: 'Board created.', - data: { - boardId: 'board uuid', - date: 'created date', - imageUrl: configService.get('APP_ICON_URL'), - }, + it('request does not have image file', async () => { + (boardsService.createBoard as jest.Mock).mockResolvedValue({ + uuid: 'board uuid', + createdAt: 'created date', + }); + + const response = controller.createBoard( + bodyMock, + undefined as Express.Multer.File, + ); + + await expect(response).resolves.toEqual({ + statusCode: HttpStatus.CREATED, + message: 'Board created.', + data: { + boardId: 'board uuid', + date: 'created date', + imageUrl: configService.get('APP_ICON_URL'), + }, + }); }); - expect(uploadService.uploadFile).not.toHaveBeenCalled(); }); - it('deleteBoard success', async () => { + describe('deleteBoard', () => { const bodyMock = { boardId: 'board uuid' }; - jest.spyOn(boardsService, 'deleteBoard').mockResolvedValue({ - matchedCount: 1, - } as UpdateWriteOpResult); - const response = controller.deleteBoard(bodyMock); + it('success', async () => { + (boardsService.deleteBoard as jest.Mock).mockResolvedValue({ + matchedCount: 1, + }); + + const response = controller.deleteBoard(bodyMock); - await expect(response).resolves.toEqual({ - statusCode: HttpStatus.OK, - message: 'Board deleted.', + await expect(response).resolves.toEqual({ + statusCode: HttpStatus.OK, + message: 'Board deleted.', + }); }); - }); - it('deleteBoard fail', async () => { - const bodyMock = { boardId: 'board uuid' }; - jest.spyOn(boardsService, 'deleteBoard').mockResolvedValue({ - matchedCount: 0, - } as UpdateWriteOpResult); + it('fail', async () => { + (boardsService.deleteBoard as jest.Mock).mockRejectedValue( + new NotFoundException(), + ); - const response = controller.deleteBoard(bodyMock); + const response = controller.deleteBoard(bodyMock); - await expect(response).rejects.toThrow(NotFoundException); + await expect(response).rejects.toThrow(NotFoundException); + }); }); - it('restoreBoard success', async () => { + describe('restoreBoard', () => { const bodyMock = { boardId: 'board uuid' }; - jest.spyOn(boardsService, 'restoreBoard').mockResolvedValue({ - matchedCount: 1, - } as UpdateWriteOpResult); - const response = controller.restoreBoard(bodyMock); + it('success', async () => { + (boardsService.restoreBoard as jest.Mock).mockResolvedValue({ + matchedCount: 1, + }); + + const response = controller.restoreBoard(bodyMock); - await expect(response).resolves.toEqual({ - statusCode: HttpStatus.OK, - message: 'Board restored.', + await expect(response).resolves.toEqual({ + statusCode: HttpStatus.OK, + message: 'Board restored.', + }); }); - }); - it('restoreBoard fail', async () => { - const bodyMock = { boardId: 'board uuid' }; - jest.spyOn(boardsService, 'restoreBoard').mockResolvedValue({ - matchedCount: 0, - } as UpdateWriteOpResult); + it('fail', async () => { + (boardsService.restoreBoard as jest.Mock).mockRejectedValue( + new NotFoundException(), + ); - const response = controller.restoreBoard(bodyMock); + const response = controller.restoreBoard(bodyMock); - await expect(response).rejects.toThrow(NotFoundException); + await expect(response).rejects.toThrow(NotFoundException); + }); }); }); diff --git a/nestjs-BE/server/src/boards/boards.controller.ts b/nestjs-BE/server/src/boards/boards.controller.ts index 3e859772..044f6b72 100644 --- a/nestjs-BE/server/src/boards/boards.controller.ts +++ b/nestjs-BE/server/src/boards/boards.controller.ts @@ -7,12 +7,9 @@ import { HttpStatus, Query, Patch, - NotFoundException, UseInterceptors, UploadedFile, } from '@nestjs/common'; -import { BoardsService } from './boards.service'; -import { CreateBoardDto } from './dto/create-board.dto'; import { ApiBody, ApiConsumes, @@ -23,7 +20,9 @@ import { ApiQuery, ApiTags, } from '@nestjs/swagger'; -import { Public } from '../auth/decorators/public.decorator'; +import { FileInterceptor } from '@nestjs/platform-express'; +import { BoardsService } from './boards.service'; +import { CreateBoardDto } from './dto/create-board.dto'; import { DeleteBoardDto } from './dto/delete-board.dto'; import { RestoreBoardDto } from './dto/restore-board.dto'; import { @@ -34,20 +33,12 @@ import { RestoreBoardFailure, RestoreBoardSuccess, } from './swagger/boards.type'; -import { FileInterceptor } from '@nestjs/platform-express'; -import { UploadService } from '../upload/upload.service'; -import { ConfigService } from '@nestjs/config'; - -const BOARD_EXPIRE_DAY = 7; +import { Public } from '../auth/decorators/public.decorator'; @Controller('boards') @ApiTags('boards') export class BoardsController { - constructor( - private boardsService: BoardsService, - private uploadService: UploadService, - private configService: ConfigService, - ) {} + constructor(private boardsService: BoardsService) {} @ApiOperation({ summary: '보드 생성', @@ -67,14 +58,14 @@ export class BoardsController { @Body() createBoardDto: CreateBoardDto, @UploadedFile() image: Express.Multer.File, ) { - const imageUrl = image - ? await this.uploadService.uploadFile(image) - : this.configService.get('APP_ICON_URL'); - const document = await this.boardsService.create(createBoardDto, imageUrl); + const document = await this.boardsService.createBoard( + createBoardDto, + image, + ); const responseData = { boardId: document.uuid, date: document.createdAt, - imageUrl, + imageUrl: document.imageUrl, }; return { statusCode: HttpStatus.CREATED, @@ -100,32 +91,10 @@ export class BoardsController { @Get('list') async findBySpaceId(@Query('spaceId') spaceId: string) { const boardList = await this.boardsService.findBySpaceId(spaceId); - const responseData = boardList.reduce>((list, board) => { - let isDeleted = false; - - if (board.deletedAt && board.deletedAt > board.restoredAt) { - const expireDate = new Date(board.deletedAt); - expireDate.setDate(board.deletedAt.getDate() + BOARD_EXPIRE_DAY); - if (new Date() > expireDate) { - this.boardsService.deleteExpiredBoard(board.uuid); - return list; - } - isDeleted = true; - } - - list.push({ - boardId: board.uuid, - boardName: board.boardName, - createdAt: board.createdAt, - imageUrl: board.imageUrl, - isDeleted, - }); - return list; - }, []); return { statusCode: HttpStatus.OK, message: 'Retrieved board list.', - data: responseData, + data: boardList, }; } @@ -142,12 +111,7 @@ export class BoardsController { @Public() @Patch('delete') async deleteBoard(@Body() deleteBoardDto: DeleteBoardDto) { - const updateResult = await this.boardsService.deleteBoard( - deleteBoardDto.boardId, - ); - if (!updateResult.matchedCount) { - throw new NotFoundException('Target board not found.'); - } + await this.boardsService.deleteBoard(deleteBoardDto.boardId); return { statusCode: HttpStatus.OK, message: 'Board deleted.' }; } @@ -164,12 +128,7 @@ export class BoardsController { @Public() @Patch('restore') async restoreBoard(@Body() resotreBoardDto: RestoreBoardDto) { - const updateResult = await this.boardsService.restoreBoard( - resotreBoardDto.boardId, - ); - if (!updateResult.matchedCount) { - throw new NotFoundException('Target board not found.'); - } + await this.boardsService.restoreBoard(resotreBoardDto.boardId); return { statusCode: HttpStatus.OK, message: 'Board restored.' }; } } diff --git a/nestjs-BE/server/src/boards/boards.module.ts b/nestjs-BE/server/src/boards/boards.module.ts index 91c082ed..3c943a67 100644 --- a/nestjs-BE/server/src/boards/boards.module.ts +++ b/nestjs-BE/server/src/boards/boards.module.ts @@ -1,7 +1,7 @@ import { Module } from '@nestjs/common'; +import { MongooseModule } from '@nestjs/mongoose'; import { BoardsService } from './boards.service'; import { BoardsController } from './boards.controller'; -import { MongooseModule } from '@nestjs/mongoose'; import { Board, BoardSchema } from './schemas/board.schema'; import { UploadModule } from '../upload/upload.module'; diff --git a/nestjs-BE/server/src/boards/boards.service.spec.ts b/nestjs-BE/server/src/boards/boards.service.spec.ts index f70dc9ef..f5e69f8f 100644 --- a/nestjs-BE/server/src/boards/boards.service.spec.ts +++ b/nestjs-BE/server/src/boards/boards.service.spec.ts @@ -1,9 +1,11 @@ import { Test, TestingModule } from '@nestjs/testing'; -import { BoardsService } from './boards.service'; import { getModelToken } from '@nestjs/mongoose'; -import { Board, BoardDocument } from './schemas/board.schema'; -import { Model, Query } from 'mongoose'; +import { ConfigModule } from '@nestjs/config'; +import { Model } from 'mongoose'; +import { BoardsService } from './boards.service'; +import { Board } from './schemas/board.schema'; import { CreateBoardDto } from './dto/create-board.dto'; +import { UploadService } from '../upload/upload.service'; describe('BoardsService', () => { let service: BoardsService; @@ -11,12 +13,17 @@ describe('BoardsService', () => { beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ + imports: [ConfigModule], providers: [ BoardsService, { provide: getModelToken(Board.name), useValue: { create: jest.fn(), find: jest.fn() }, }, + { + provide: UploadService, + useValue: { uploadFile: jest.fn() }, + }, ], }).compile(); @@ -30,25 +37,12 @@ describe('BoardsService', () => { spaceId: 'space uuid', } as CreateBoardDto; const imageMock = 'www.test.com/image'; - jest - .spyOn(model, 'create') - .mockResolvedValue('created board' as unknown as BoardDocument[]); + + (model.create as jest.Mock).mockResolvedValue('created board'); const board = service.create(data, imageMock); await expect(board).resolves.toBe('created board'); expect(model.create).toHaveBeenCalled(); }); - - it('findBySpaceId', async () => { - jest.spyOn(model, 'find').mockReturnValue({ - exec: async () => { - return 'board list' as unknown as Board[]; - }, - } as Query); - - const boards = service.findBySpaceId('space uuid'); - - await expect(boards).resolves.toBe('board list'); - }); }); diff --git a/nestjs-BE/server/src/boards/boards.service.ts b/nestjs-BE/server/src/boards/boards.service.ts index 7d134f36..27dc480e 100644 --- a/nestjs-BE/server/src/boards/boards.service.ts +++ b/nestjs-BE/server/src/boards/boards.service.ts @@ -1,13 +1,21 @@ -import { Injectable } from '@nestjs/common'; +import { Injectable, NotFoundException } from '@nestjs/common'; import { InjectModel } from '@nestjs/mongoose'; -import { Board } from './schemas/board.schema'; +import { ConfigService } from '@nestjs/config'; import { Model } from 'mongoose'; -import { CreateBoardDto } from './dto/create-board.dto'; import { v4 } from 'uuid'; +import { Board } from './schemas/board.schema'; +import { CreateBoardDto } from './dto/create-board.dto'; +import { UploadService } from '../upload/upload.service'; + +const BOARD_EXPIRE_DAY = 7; @Injectable() export class BoardsService { - constructor(@InjectModel(Board.name) private boardModel: Model) {} + constructor( + @InjectModel(Board.name) private boardModel: Model, + private uploadService: UploadService, + private configService: ConfigService, + ) {} async create( createBoardDto: CreateBoardDto, @@ -27,13 +35,53 @@ export class BoardsService { return board; } + async createBoard( + createBoardDto: CreateBoardDto, + image: Express.Multer.File | undefined, + ) { + const imageUrl = image + ? await this.uploadService.uploadFile(image) + : this.configService.get('APP_ICON_URL'); + return this.create(createBoardDto, imageUrl); + } + async findBySpaceId(spaceId: string): Promise { - return this.boardModel.find({ spaceId }).exec(); + const boardList = await this.boardModel.find({ spaceId }).exec(); + const filteredList = boardList.reduce>((list, board) => { + let isDeleted = false; + + if (board.deletedAt && board.deletedAt > board.restoredAt) { + const expireDate = new Date(board.deletedAt); + expireDate.setDate(board.deletedAt.getDate() + BOARD_EXPIRE_DAY); + if (new Date() > expireDate) { + this.deleteExpiredBoard(board.uuid); + return list; + } + isDeleted = true; + } + + list.push({ + boardId: board.uuid, + boardName: board.boardName, + createdAt: board.createdAt, + imageUrl: board.imageUrl, + isDeleted, + }); + return list; + }, []); + return filteredList; } async deleteBoard(boardId: string) { const now = new Date(); - return this.boardModel.updateOne({ uuid: boardId }, { deletedAt: now }); + const board = await this.boardModel.updateOne( + { uuid: boardId }, + { deletedAt: now }, + ); + if (!board.matchedCount) { + throw new NotFoundException('Target board not found.'); + } + return board; } async deleteExpiredBoard(boardId: string) { @@ -42,6 +90,13 @@ export class BoardsService { async restoreBoard(boardId: string) { const now = new Date(); - return this.boardModel.updateOne({ uuid: boardId }, { restoredAt: now }); + const board = await this.boardModel.updateOne( + { uuid: boardId }, + { restoredAt: now }, + ); + if (!board.matchedCount) { + throw new NotFoundException('Target board not found.'); + } + return board; } }