Skip to content

Commit

Permalink
Merge branch 'develop' into BE/feature/#100
Browse files Browse the repository at this point in the history
  • Loading branch information
Dltmd202 authored Dec 30, 2023
2 parents 997b8ac + 67e9985 commit 41aa580
Show file tree
Hide file tree
Showing 43 changed files with 1,018 additions and 75 deletions.
167 changes: 128 additions & 39 deletions BE/src/category/application/category.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,17 @@ import { transactionTest } from '../../../test/common/transaction-test';
import { NotFoundCategoryException } from '../exception/not-found-category.exception';
import { CategoryRelocateRequest } from '../dto/category-relocate.request';
import { InvalidCategoryRelocateException } from '../exception/Invalid-Category-Relocate.exception';
import { CategoryMetaData } from '../dto/category-metadata';
import { CategoryNotFoundException } from '../exception/category-not-found.exception';
import { UserRepository } from '../../users/entities/user.repository';

describe('CategoryService', () => {
let categoryService: CategoryService;
let dataSource: DataSource;
let usersFixture: UsersFixture;
let categoryFixture: CategoryFixture;
let achievementFixture: AchievementFixture;
let userRepository: UserRepository;

beforeAll(async () => {
const app: TestingModule = await Test.createTestingModule({
Expand All @@ -40,6 +44,7 @@ describe('CategoryService', () => {
providers: [],
}).compile();

userRepository = app.get<UserRepository>(UserRepository);
categoryFixture = app.get<CategoryFixture>(CategoryFixture);
achievementFixture = app.get<AchievementFixture>(AchievementFixture);
usersFixture = app.get<UsersFixture>(UsersFixture);
Expand Down Expand Up @@ -107,52 +112,56 @@ describe('CategoryService', () => {

describe('getCategoriesByUsers는 카테고리를 조회할 수 있다', () => {
it('user에 대한 카테고리가 없을 때 빈 배열을 반환한다.', async () => {
// given
const user = await usersFixture.getUser(1);
await transactionTest(dataSource, async () => {
// given
const user = await usersFixture.getUser(1);

// when
const retrievedCategories =
await categoryService.getCategoriesByUser(user);
// when
const retrievedCategories =
await categoryService.getCategoriesByUser(user);

// then
expect(retrievedCategories).toBeDefined();
expect(retrievedCategories.length).toBe(1);
expect(retrievedCategories[0].categoryId).toEqual(-1);
// then
expect(retrievedCategories).toBeDefined();
expect(retrievedCategories.length).toBe(1);
expect(retrievedCategories[0].categoryId).toEqual(-1);
});
});

it('user에 대한 카테고리가 있을 때 카테고리를 반환한다.', async () => {
// given
const user = await usersFixture.getUser(1);
const categories: Category[] = await categoryFixture.getCategories(
4,
user,
);
await achievementFixture.getAchievements(4, user, categories[0]);
await achievementFixture.getAchievements(5, user, categories[1]);
await achievementFixture.getAchievements(7, user, categories[2]);
await achievementFixture.getAchievements(10, user, categories[3]);
await transactionTest(dataSource, async () => {
// given
const user = await usersFixture.getUser(1);
const categories: Category[] = await categoryFixture.getCategories(
4,
user,
);
await achievementFixture.getAchievements(4, user, categories[0]);
await achievementFixture.getAchievements(5, user, categories[1]);
await achievementFixture.getAchievements(7, user, categories[2]);
await achievementFixture.getAchievements(10, user, categories[3]);

// when
const retrievedCategories =
await categoryService.getCategoriesByUser(user);
// when
const retrievedCategories =
await categoryService.getCategoriesByUser(user);

// then
expect(retrievedCategories.length).toBe(5);
expect(retrievedCategories[0].categoryId).toEqual(-1);
expect(retrievedCategories[0].insertedAt).toBeNull();
expect(retrievedCategories[0].achievementCount).toBe(0);
expect(retrievedCategories[1].categoryId).toEqual(categories[0].id);
expect(retrievedCategories[1].insertedAt).toBeInstanceOf(Date);
expect(retrievedCategories[1].achievementCount).toBe(4);
expect(retrievedCategories[2].categoryId).toEqual(categories[1].id);
expect(retrievedCategories[2].insertedAt).toBeInstanceOf(Date);
expect(retrievedCategories[2].achievementCount).toBe(5);
expect(retrievedCategories[3].categoryId).toEqual(categories[2].id);
expect(retrievedCategories[3].insertedAt).toBeInstanceOf(Date);
expect(retrievedCategories[3].achievementCount).toBe(7);
expect(retrievedCategories[4].categoryId).toEqual(categories[3].id);
expect(retrievedCategories[4].insertedAt).toBeInstanceOf(Date);
expect(retrievedCategories[4].achievementCount).toBe(10);
// then
expect(retrievedCategories.length).toBe(5);
expect(retrievedCategories[0].categoryId).toEqual(-1);
expect(retrievedCategories[0].insertedAt).toBeNull();
expect(retrievedCategories[0].achievementCount).toBe(0);
expect(retrievedCategories[1].categoryId).toEqual(categories[0].id);
expect(retrievedCategories[1].insertedAt).toBeInstanceOf(Date);
expect(retrievedCategories[1].achievementCount).toBe(4);
expect(retrievedCategories[2].categoryId).toEqual(categories[1].id);
expect(retrievedCategories[2].insertedAt).toBeInstanceOf(Date);
expect(retrievedCategories[2].achievementCount).toBe(5);
expect(retrievedCategories[3].categoryId).toEqual(categories[2].id);
expect(retrievedCategories[3].insertedAt).toBeInstanceOf(Date);
expect(retrievedCategories[3].achievementCount).toBe(7);
expect(retrievedCategories[4].categoryId).toEqual(categories[3].id);
expect(retrievedCategories[4].insertedAt).toBeInstanceOf(Date);
expect(retrievedCategories[4].achievementCount).toBe(10);
});
});
});

Expand Down Expand Up @@ -373,4 +382,84 @@ describe('CategoryService', () => {
});
});
});

describe('deleteCategory는 카테고리를 삭제할 수 있다.', () => {
it('본인의 카테고리를 삭제할 수 있다.', async () => {
await transactionTest(dataSource, async () => {
// given
const user = await usersFixture.getUser('ABCD');
const category = await categoryFixture.getCategory(user, '카테고리1');

// when
await categoryService.deleteCategory(user, category.id);
const categoryMetaData: CategoryMetaData[] =
await categoryService.getCategoriesByUser(user);

// then
expect(categoryMetaData.length).toBe(1);
expect(categoryMetaData[0].categoryId).toBe(-1);
});
});

it('본인의 카테고리가 아닌 카테고리는 삭제할 수 없다.', async () => {
await transactionTest(dataSource, async () => {
// given
const otherUser = await usersFixture.getUser('ABC');
const category = await categoryFixture.getCategory(
otherUser,
'카테고리1',
);

const user = await usersFixture.getUser('ABCD');

// when
await expect(
categoryService.deleteCategory(user, category.id),
).rejects.toThrow(new CategoryNotFoundException());
});
});

it('카테고리를 삭제해도 기존 순서가 유지된다.', async () => {
await transactionTest(dataSource, async () => {
// given
const user = await usersFixture.getUser('ABCD');
const category1 = await categoryFixture.getCategory(user, '카테고리1');
const category2 = await categoryFixture.getCategory(user, '카테고리2');
const category3 = await categoryFixture.getCategory(user, '카테고리3');

// when
await categoryService.deleteCategory(user, category2.id);
const categoryMetaData: CategoryMetaData[] =
await categoryService.getCategoriesByUser(user);

// then
expect(categoryMetaData.length).toBe(3);
expect(categoryMetaData[0].categoryId).toBe(-1);
expect(categoryMetaData[1].categoryId).toBe(category1.id);
expect(categoryMetaData[2].categoryId).toBe(category3.id);
});
});

it('카테고리를 삭제하면 유저의 카테고리 카운트가 반영된다.', async () => {
await transactionTest(dataSource, async () => {
// given
const user = await usersFixture.getUser('ABCD');
await categoryFixture.getCategory(user, '카테고리1');
const category2 = await categoryFixture.getCategory(user, '카테고리2');
await categoryFixture.getCategory(user, '카테고리3');

// when
await categoryService.deleteCategory(user, category2.id);
const userEntity = await userRepository.repository.findOne({
where: {
id: user.id,
},
});

// then
expect(userEntity.categoryCount).toBe(2);
expect(userEntity.categorySequence).toBe(3);
});
});
});
});
13 changes: 13 additions & 0 deletions BE/src/category/application/category.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { NotFoundCategoryException } from '../exception/not-found-category.excep
import { CategoryRelocateRequest } from '../dto/category-relocate.request';
import { UserRepository } from '../../users/entities/user.repository';
import { InvalidCategoryRelocateException } from '../exception/Invalid-Category-Relocate.exception';
import { CategoryNotFoundException } from '../exception/category-not-found.exception';

@Injectable()
export class CategoryService {
Expand Down Expand Up @@ -61,4 +62,16 @@ export class CategoryService {
await this.categoryRepository.saveCategory(category);
}
}

@Transactional()
async deleteCategory(user: User, categoryId: number) {
const category = await this.categoryRepository.findByIdAndUser(
user.id,
categoryId,
);
if (!category) throw new CategoryNotFoundException();
user.deleteCategory();
await this.userRepository.updateUser(user);
await this.categoryRepository.deleteCategory(category);
}
}
72 changes: 72 additions & 0 deletions BE/src/category/controller/category.controller.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { GroupCategoryMetadata } from '../../group/category/dto/group-category-m
import { NotFoundCategoryException } from '../exception/not-found-category.exception';
import { CategoryRelocateRequest } from '../dto/category-relocate.request';
import { InvalidCategoryRelocateException } from '../exception/Invalid-Category-Relocate.exception';
import { CategoryNotFoundException } from '../exception/category-not-found.exception';

describe('CategoryController Test', () => {
let app: INestApplication;
Expand Down Expand Up @@ -414,4 +415,75 @@ describe('CategoryController Test', () => {
.expect(400);
});
});

describe('deleteCategory는 카테고리를 삭제할 수 있다.', () => {
it('카테고리 삭제에 성공하면 204을 반환한다.', async () => {
// given
const { accessToken } = await authFixture.getAuthenticatedUser('ABC');

when(mockCategoryService.deleteCategory(anyOfClass(User), 1)).thenResolve(
undefined,
);

// when
// then
return request(app.getHttpServer())
.delete(`/api/v1/categories/1`)
.set('Authorization', `Bearer ${accessToken}`)
.expect(204);
});

it('카테고리 삭제에 실패하면 400을 반환한다.', async () => {
// given
const { accessToken } = await authFixture.getAuthenticatedUser('ABC');

when(mockCategoryService.deleteCategory(anyOfClass(User), 1)).thenThrow(
new CategoryNotFoundException(),
);

// when
// then
return request(app.getHttpServer())
.delete(`/api/v1/categories/1`)
.set('Authorization', `Bearer ${accessToken}`)
.expect(404)
.expect((res: request.Response) => {
expect(res.body.success).toBe(false);
expect(res.body.message).toBe('카테고리를 찾을 수 없습니다.');
});
});

it('잘못된 인증시 401을 반환한다.', async () => {
// given
const accessToken = 'abcd.abcd.efgh';

// when
// then
return request(app.getHttpServer())
.get(`/api/v1/categories/1006`)
.set('Authorization', `Bearer ${accessToken}`)
.expect(401)
.expect((res: request.Response) => {
expect(res.body.success).toBe(false);
expect(res.body.message).toBe('잘못된 토큰입니다.');
});
});

it('만료된 인증정보에 401을 반환한다.', async () => {
// given
const { accessToken } =
await authFixture.getExpiredAccessTokenUser('ABC');

// when
// then
return request(app.getHttpServer())
.delete(`/api/v1/categories/1`)
.set('Authorization', `Bearer ${accessToken}`)
.expect(401)
.expect((res: request.Response) => {
expect(res.body.success).toBe(false);
expect(res.body.message).toBe('만료된 토큰입니다.');
});
});
});
});
17 changes: 17 additions & 0 deletions BE/src/category/controller/category.controller.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {
Body,
Controller,
Delete,
Get,
HttpCode,
HttpStatus,
Expand Down Expand Up @@ -109,4 +110,20 @@ export class CategoryController {
) {
return this.categoryService.relocateCategory(user, categoryRelocateRequest);
}

@ApiBearerAuth('accessToken')
@ApiOperation({
summary: '카테고리 순서 변경 API',
description:
'변경될 카테고리 순서로 카테고리 아이디를 배열의 형태로 요청한다.',
})
@Delete('/:categoryId')
@UseGuards(AccessTokenGuard)
@HttpCode(HttpStatus.NO_CONTENT)
async deleteCategory(
@Param('categoryId', ParseIntPipe) categoryId: number,
@AuthenticatedUser() user: User,
) {
return this.categoryService.deleteCategory(user, categoryId);
}
}
5 changes: 5 additions & 0 deletions BE/src/category/entities/category.repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ export class CategoryRepository extends TransactionalRepository<CategoryEntity>
.addSelect('COUNT(achievement.id)', 'achievementCount')
.leftJoin('category.achievements', 'achievement')
.where('category.user_id = :user', { user: user.id })
.andWhere('category.deletedAt is NULL')
.orderBy('category.seq', 'ASC')
.groupBy('category.id')
.getRawMany<ICategoryMetaData>();
Expand Down Expand Up @@ -127,4 +128,8 @@ export class CategoryRepository extends TransactionalRepository<CategoryEntity>

return categories.map((c) => c.toModel());
}

async deleteCategory(category: Category) {
await this.repository.softDelete(category.id);
}
}
8 changes: 8 additions & 0 deletions BE/src/category/exception/category-not-found.exception.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { MotimateException } from '../../common/exception/motimate.excpetion';
import { ERROR_INFO } from '../../common/exception/error-code';

export class CategoryNotFoundException extends MotimateException {
constructor() {
super(ERROR_INFO.CATEGORY_NOT_FOUND);
}
}
4 changes: 4 additions & 0 deletions BE/src/common/exception/error-code.ts
Original file line number Diff line number Diff line change
Expand Up @@ -155,4 +155,8 @@ export const ERROR_INFO = {
statusCode: 403,
message: '접근할 수 없는 요청입니다.',
},
CATEGORY_NOT_FOUND: {
statusCode: 404,
message: '카테고리를 찾을 수 없습니다.',
},
} as const;
4 changes: 4 additions & 0 deletions BE/src/users/domain/user.domain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,4 +38,8 @@ export class User {
++this.categoryCount;
return new Category(this, categoryName, ++this.categorySequence);
}

deleteCategory() {
this.categoryCount--;
}
}
Loading

0 comments on commit 41aa580

Please sign in to comment.