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

Space 삭제 API 작업 #200

Closed
wants to merge 8 commits into from
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,15 @@ export const ERROR_MESSAGES = {
UPDATE_FAILED: '스페이스 업데이트에 실패하였습니다.',
INITIALIZE_FAILED: '스페이스가 초기화에 실패하였습니다.',
PARENT_NOT_FOUND: '부모 스페이스가 존재하지 않습니다.',
DELETE_FAILED: '노트 삭제에 실패하였습니다.',
},
NOTE: {
BAD_REQUEST: '잘못된 요청입니다.',
NOT_FOUND: '노트가 존재하지 않습니다.',
CREATION_FAILED: '노트 생성에 실패하였습니다.',
UPDATE_FAILED: '노트 업데이트에 실패하였습니다.',
INITIALIZE_FAILED: '노트가 초기화에 실패하였습니다.',
DELETE_FAILED: '노트 삭제에 실패하였습니다.',
},
SOCKET: {
INVALID_URL: '유효하지 않은 URL 주소입니다.',
Expand Down
18 changes: 18 additions & 0 deletions packages/backend/src/note/note.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,4 +80,22 @@ export class NoteService {
throw new BadRequestException(ERROR_MESSAGES.NOTE.UPDATE_FAILED);
}
}
async deleteById(id: string) {
this.logger.log(`ID가 ${id}인 노트를 삭제하는 중입니다.`);

try {
const result = await this.noteModel.deleteOne({ id }).exec();

if (result.deletedCount === 0) {
this.logger.warn(`삭제 실패: ID가 ${id}인 노트를 찾을 수 없습니다.`);
throw new BadRequestException(ERROR_MESSAGES.NOTE.NOT_FOUND);
}

this.logger.log(`ID가 ${id}인 노트 삭제 완료.`);
return { success: true, message: '노트가 성공적으로 삭제되었습니다.' };
} catch (error) {
this.logger.error(`ID가 ${id}인 노트 삭제 중 오류 발생.`, error.stack);
throw new BadRequestException(ERROR_MESSAGES.NOTE.DELETE_FAILED);
}
}
}
145 changes: 145 additions & 0 deletions packages/backend/src/space/space.controller.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
import { Test, TestingModule } from '@nestjs/testing';
import { SpaceController } from './space.controller';
import { SpaceService } from './space.service';
import { CreateSpaceDto } from './dto/create.space.dto';
import { HttpException } from '@nestjs/common';
import { GUEST_USER_ID } from '../common/constants/space.constants';

describe('SpaceController', () => {
let spaceController: SpaceController;
let spaceService: Partial<SpaceService>;

beforeEach(async () => {
spaceService = {
existsById: jest.fn(),
getBreadcrumb: jest.fn(),
create: jest.fn(),
};

const module: TestingModule = await Test.createTestingModule({
controllers: [SpaceController],
providers: [
{
provide: SpaceService,
useValue: spaceService,
},
],
}).compile();

spaceController = module.get<SpaceController>(SpaceController);
});

describe('existsBySpace', () => {
it('스페이스가 존재할 경우 true를 반환해야 한다', async () => {
const spaceId = '123';
(spaceService.existsById as jest.Mock).mockResolvedValue(true);

const result = await spaceController.existsBySpace(spaceId);

expect(spaceService.existsById).toHaveBeenCalledWith(spaceId);
expect(result).toBe(true);
});

it('예외가 발생하면 오류를 던져야 한다', async () => {
const spaceId = '123';
(spaceService.existsById as jest.Mock).mockRejectedValue(
new Error('Unexpected Error'),
);

await expect(spaceController.existsBySpace(spaceId)).rejects.toThrow(
'Unexpected Error',
);
});
});

describe('getBreadcrumb', () => {
it('주어진 스페이스 ID에 대한 경로를 반환해야 한다', async () => {
const spaceId = '123';
const breadcrumb = ['Home', 'Space'];
(spaceService.getBreadcrumb as jest.Mock).mockResolvedValue(breadcrumb);

const result = await spaceController.getBreadcrumb(spaceId);

expect(spaceService.getBreadcrumb).toHaveBeenCalledWith(spaceId);
expect(result).toEqual(breadcrumb);
});
});

describe('createSpace', () => {
it('스페이스를 생성하고 URL 경로를 반환해야 한다', async () => {
const createSpaceDto: CreateSpaceDto = {
userId: GUEST_USER_ID,
spaceName: 'New Space',
parentContextNodeId: '123',
};

const mockSpace = { toObject: () => ({ id: 'space123' }) };
(spaceService.create as jest.Mock).mockResolvedValue(mockSpace);

const result = await spaceController.createSpace(createSpaceDto);

expect(spaceService.create).toHaveBeenCalledWith(
GUEST_USER_ID,
'New Space',
'123',
);
expect(result).toEqual({ urlPath: 'space123' });
});

it('잘못된 요청인 경우 400 오류를 던져야 한다', async () => {
const createSpaceDto: CreateSpaceDto = {
userId: 'invalidUser',
spaceName: '',
parentContextNodeId: '123',
};

await expect(spaceController.createSpace(createSpaceDto)).rejects.toThrow(
HttpException,
);

expect(spaceService.create).not.toHaveBeenCalled();
});

it('스페이스 생성에 실패한 경우 404 오류를 던져야 한다', async () => {
const createSpaceDto: CreateSpaceDto = {
userId: GUEST_USER_ID,
spaceName: 'New Space',
parentContextNodeId: '123',
};

(spaceService.create as jest.Mock).mockResolvedValue(null);

await expect(spaceController.createSpace(createSpaceDto)).rejects.toThrow(
HttpException,
);

expect(spaceService.create).toHaveBeenCalledWith(
GUEST_USER_ID,
'New Space',
'123',
);
});
});

describe('updateSpace', () => {
it('스페이스가 존재하지 않을 경우 404 오류를 던져야 한다', async () => {
const spaceId = '123';
(spaceService.existsById as jest.Mock).mockResolvedValue(false);

await expect(spaceController.updateSpace(spaceId)).rejects.toThrow(
HttpException,
);

expect(spaceService.existsById).toHaveBeenCalledWith(spaceId);
});

it('스페이스가 존재할 경우 오류를 던지지 않아야 한다', async () => {
const spaceId = '123';
(spaceService.existsById as jest.Mock).mockResolvedValue(true);

await expect(spaceController.updateSpace(spaceId)).resolves.not.toThrow();

expect(spaceService.existsById).toHaveBeenCalledWith(spaceId);
});
});
});
24 changes: 24 additions & 0 deletions packages/backend/src/space/space.controller.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {
Body,
Controller,
Delete,
Get,
HttpException,
HttpStatus,
Expand Down Expand Up @@ -157,4 +158,27 @@ export class SpaceController {
);
}
}
@Version('1')
@Delete('/:id')
@ApiOperation({ summary: '스페이스 삭제' })
@ApiResponse({ status: 201, description: '스페이스 삭제 성공' })
@ApiResponse({ status: 400, description: '잘못된 요청' })
async deleteSpace(@Param('id') id: string) {
const result = await this.spaceService.deleteById(id);

if (!result) {
this.logger.error(
'스페이스 삭제 실패 - 스페이스 삭제에 실패하였습니다.',
{
method: 'deleteSpace',
error: ERROR_MESSAGES.SPACE.DELETE_FAILED,
id,
},
);
throw new HttpException(
ERROR_MESSAGES.SPACE.DELETE_FAILED,
HttpStatus.NOT_FOUND,
);
}
}
}
2 changes: 2 additions & 0 deletions packages/backend/src/space/space.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@ import { SpaceController } from './space.controller';
import { SpaceDocument, SpaceSchema } from './space.schema';
import { SpaceService } from './space.service';
import { SpaceValidationService } from './space.validation.service';
import { NoteModule } from 'src/note/note.module';

@Module({
imports: [
NoteModule,
MongooseModule.forFeature([
{ name: SpaceDocument.name, schema: SpaceSchema },
]),
Expand Down
82 changes: 82 additions & 0 deletions packages/backend/src/space/space.service.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { Test, TestingModule } from '@nestjs/testing';
import { SpaceService } from './space.service';
import { getModelToken } from '@nestjs/mongoose';
import { SpaceDocument } from './space.schema';
import { SpaceValidationService } from './space.validation.service';
import { Model } from 'mongoose';

jest.mock('uuid', () => ({
v4: jest.fn(() => 'mock-uuid'),
}));

describe('SpaceService', () => {
let spaceService: SpaceService;
let spaceModel: Model<SpaceDocument>;
let spaceValidationService: SpaceValidationService;

beforeEach(async () => {
const mockSpaceModel = {
findOne: jest.fn().mockReturnValue({
exec: jest.fn(),
}),
findOneAndUpdate: jest.fn().mockReturnValue({
exec: jest.fn(),
}),
countDocuments: jest.fn(),
create: jest.fn(),
};

const mockSpaceValidationService = {
validateSpaceLimit: jest.fn().mockResolvedValue(undefined),
validateParentNodeExists: jest.fn().mockResolvedValue(undefined),
};

const module: TestingModule = await Test.createTestingModule({
providers: [
SpaceService,
{
provide: getModelToken(SpaceDocument.name),
useValue: mockSpaceModel,
},
{
provide: SpaceValidationService,
useValue: mockSpaceValidationService,
},
],
}).compile();

spaceService = module.get<SpaceService>(SpaceService);
spaceModel = module.get<Model<SpaceDocument>>(
getModelToken(SpaceDocument.name),
);
spaceValidationService = module.get<SpaceValidationService>(
SpaceValidationService,
);
});

describe('getBreadcrumb', () => {
it('스페이스의 경로를 반환해야 한다', async () => {
const mockSpaces = [
{ id: 'parent-id', name: 'Parent Space', parentSpaceId: null },
{ id: '123', name: 'Child Space', parentSpaceId: 'parent-id' },
];

(spaceModel.findOne as jest.Mock)
.mockReturnValueOnce({
exec: jest.fn().mockResolvedValue(mockSpaces[1]),
})
.mockReturnValueOnce({
exec: jest.fn().mockResolvedValue(mockSpaces[0]),
});

const result = await spaceService.getBreadcrumb('123');

expect(spaceModel.findOne).toHaveBeenCalledWith({ id: '123' });
expect(spaceModel.findOne).toHaveBeenCalledWith({ id: 'parent-id' });
expect(result).toEqual([
{ name: 'Parent Space', url: 'parent-id' },
{ name: 'Child Space', url: '123' },
]);
});
});
});
Loading
Loading