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

[NDD-172]: 카테고리 삭제 기능 구현 (2h / 1h) #54

Merged
merged 6 commits into from
Nov 17, 2023
136 changes: 113 additions & 23 deletions BE/src/category/controller/category.controller.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,10 @@ import {
oauthRequestFixture,
} from '../../member/fixture/member.fixture';
import { CreateCategoryRequest } from '../dto/createCategoryRequest';
import { CategoryNameEmptyException } from '../exception/category.exception';
import {
CategoryNameEmptyException,
CategoryNotFoundException,
} from '../exception/category.exception';
import { ManipulatedTokenNotFiltered } from '../../token/exception/token.exception';
import { Request } from 'express';
import * as request from 'supertest';
Expand All @@ -23,6 +26,7 @@ import { TokenModule } from '../../token/token.module';
import { AuthService } from '../../auth/service/auth.service';
import { Token } from '../../token/entity/token';
import {
beCategoryFixture,
categoryListFixture,
categoryListResponseFixture,
defaultCategoryListFixture,
Expand All @@ -31,13 +35,15 @@ import {
import { CategoryListResponse } from '../dto/categoryListResponse';
import { TokenService } from '../../token/service/token.service';
import { CategoryRepository } from '../repository/category.repository';
import { UnauthorizedException } from '@nestjs/common';

describe('CategoryController', () => {
let controller: CategoryController;

const mockCategoryService = {
createCategory: jest.fn(),
findUsingCategories: jest.fn(),
deleteCategoryById: jest.fn(),
};

const mockTokenService = {
Expand Down Expand Up @@ -142,6 +148,73 @@ describe('CategoryController', () => {
CategoryListResponse.of(defaultCategoryListResponseFixture),
);
});

it('Member객체가 있고, 존재하는 id의 삭제를 요청하면, Undefined가 반환된다.', async () => {
//given
const category = new Category(1, 'CS', memberFixture, new Date());

//when
mockCategoryService.deleteCategoryById.mockResolvedValue(undefined);
//then
await expect(
controller.deleteCategoryById(mockReqWithMemberFixture, category.id),
).resolves.toBeUndefined();
});

it('Member객체가 없이 id만을 요청하면 ManipulatedToken을 반환한다. => 미들웨어 통과지만 Repository에서 찾지 못한 경우', async () => {
//given
const category = new Category(1, 'CS', memberFixture, new Date());

//when
mockCategoryService.deleteCategoryById.mockRejectedValue(
new ManipulatedTokenNotFiltered(),
);
//then
await expect(
controller.deleteCategoryById(
{ user: undefined } as unknown as Request,
category.id,
),
).rejects.toThrow(new ManipulatedTokenNotFiltered());
});

it('Member객체가 있지만, id로 조회한 Category의 Member와 다르면 UnauthorizedException을 반환한다.', async () => {
//given
const category = new Category(
1,
'CS',
new Member(3, 'ja@ja.com', 'ja', 'http://www.gomterview.com', new Date()),
new Date(),
);

//when
mockCategoryService.deleteCategoryById.mockRejectedValue(
new UnauthorizedException(),
);
//then
await expect(
controller.deleteCategoryById(mockReqWithMemberFixture, category.id),
).rejects.toThrow(new UnauthorizedException());
});

it('Member객체가 있지만 id로 조회한 Category가 없을 경우 CategoryNotFoundException을 반환한다.', async () => {
//given
const category = new Category(
1,
'CS',
new Member(3, 'ja@ja.com', 'ja', 'http://www.gomterview.com', new Date()),
new Date(),
);

//when
mockCategoryService.deleteCategoryById.mockRejectedValue(
new CategoryNotFoundException(),
);
//then
await expect(
controller.deleteCategoryById(mockReqWithMemberFixture, category.id + 1),
).rejects.toThrow(new CategoryNotFoundException());
});
});

describe('CategoryController 통합테스트', () => {
Expand All @@ -164,25 +237,24 @@ describe('CategoryController 통합테스트', () => {
moduleFixture.get<CategoryRepository>(CategoryRepository);
});

beforeEach(async () => {
await categoryRepository.query('delete from MemberQuestion');
await categoryRepository.query('delete from Question');
await categoryRepository.query('delete from Category');
await categoryRepository.query('delete from Member');
await categoryRepository.query('delete from token');
});

it('카테고리 저장을 성공시 201상태코드가 반환된다.', (done) => {
memberRepository
.save(memberFixture)
.then(() => {
return authService.login(oauthRequestFixture);
})
.then((token) => {
const agent = request.agent(app.getHttpServer());
agent
.post(`/api/category`)
.set('Cookie', [`accessToken=${token}`])
.send(new CreateCategoryRequest('tester'))
.expect(201)
.then((response) => {
expect(response).toBeUndefined();
return;
});
})
.then(done);
authService.login(oauthRequestFixture).then((validToken) => {
const agent = request.agent(app.getHttpServer());
agent
.post('/api/category')
.set('Cookie', [`accessToken=${validToken}`])
.send(new CreateCategoryRequest('tester'))
.expect(201)
.then(() => done());
});
});

it('회원이 카테고리 조회시 200코드와 CategoryListResponse가 반환된다.', (done) => {
Expand Down Expand Up @@ -237,9 +309,27 @@ describe('CategoryController 통합테스트', () => {
.then(done);
});

afterEach(async () => {
await categoryRepository.query('delete from Category');
await categoryRepository.query('delete from Member');
await categoryRepository.query('delete from token');
it('회원의 카테고리를 삭제한다.', (done) => {
//given
const category = Category.from(beCategoryFixture, memberFixture);
const oauthRequest = {
name: memberFixture.nickname,
email: memberFixture.email,
img: memberFixture.profileImg,
};
//when

//then
categoryRepository
.save(category)
.then(() => authService.login(oauthRequest))
.then((token) => {
const agent = request.agent(app.getHttpServer());
agent
.delete(`/api/category?id=1`)
.set('Cookie', [`accessToken=${token}`])
.expect(200)
.then(done);
});
});
});
26 changes: 25 additions & 1 deletion BE/src/category/controller/category.controller.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,13 @@
import { Body, Controller, Get, Post, Req, UseGuards } from '@nestjs/common';
import {
Body,
Controller,
Delete,
Get,
Post,
Query,
Req,
UseGuards,
} from '@nestjs/common';
import {
ApiBody,
ApiCookieAuth,
Expand Down Expand Up @@ -61,4 +70,19 @@ export class CategoryController {

return CategoryListResponse.of(categories);
}

@Delete()
@UseGuards(AuthGuard('jwt'))
@ApiCookieAuth()
@ApiOperation({
summary: '카테고리를 삭제한다.',
})
@ApiResponse(createApiResponseOption(204, '카테고리 삭제', null))
async deleteCategoryById(
@Req() req: Request,
@Query('categoryId') categoryId: number,
) {
const member = req.user as Member;
await this.categoryService.deleteCategoryById(member, categoryId);
}
}
20 changes: 6 additions & 14 deletions BE/src/category/entity/category.ts
Original file line number Diff line number Diff line change
@@ -1,29 +1,17 @@
import {
Column,
Entity,
JoinColumn,
JoinTable,
ManyToMany,
ManyToOne,
} from 'typeorm';
import { Column, Entity, JoinColumn, ManyToOne } from 'typeorm';
import { DefaultEntity } from '../../app.entity';
import { Member } from '../../member/entity/member';
import { Question } from '../../question/entity/question';
import { CreateCategoryRequest } from '../dto/createCategoryRequest';

@Entity({ name: 'Category' })
export class Category extends DefaultEntity {
@Column()
name: string;

@ManyToOne(() => Member, { nullable: true, onDelete: 'CASCADE' })
@ManyToOne(() => Member, { nullable: true, onDelete: 'CASCADE', eager: true })
@JoinColumn({ name: 'memberId' })
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

카테고리를 불러올 때 거의 대부분의 경우에 Member도 같이 불러오나요??
eager를 여기에서 왜 사용하셨는지 이유가 궁금합니다!

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

eager를 통해서 Member를 가져옵니다. 등록되지 않았을 때 Member가 조회되지 않는 문제가 있었습니다.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

넵 알겠습니다! 감사합니다:)

member: Member;

@ManyToMany(() => Question)
@JoinTable({ name: 'CategoryQuestion' })
questions: Question[];

constructor(id: number, name: string, member: Member, createdAt: Date) {
super(id, createdAt);
this.member = member;
Expand All @@ -34,6 +22,10 @@ export class Category extends DefaultEntity {
return new Category(null, inputObj.name, member, new Date());
}

isOwnedBy(member: Member) {
return this.member.equals(member);
}

getName() {
return this.name;
}
Expand Down
8 changes: 7 additions & 1 deletion BE/src/category/exception/category.exception.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,10 @@ class CategoryNameEmptyException extends HttpException {
}
}

export { CategoryNameEmptyException };
class CategoryNotFoundException extends HttpException {
constructor() {
super('카테고리자 존재하지 않습니다.', 404);
}
}

export { CategoryNameEmptyException, CategoryNotFoundException };
4 changes: 4 additions & 0 deletions BE/src/category/repository/category.repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@ export class CategoryRepository {
await this.repository.remove(category);
}

async findByCategoryId(categoryId) {
return await this.repository.findOneBy({ id: categoryId });
}

async query(query: string) {
return await this.repository.query(query);
}
Expand Down
Loading
Loading