From 7229b8890caabd4960c77195e4102e035e9a2263 Mon Sep 17 00:00:00 2001 From: lsh23 Date: Sat, 9 Dec 2023 21:29:14 +0900 Subject: [PATCH] =?UTF-8?q?[BE]=20feat:=20Manager=20=EA=B6=8C=ED=95=9C=20?= =?UTF-8?q?=EC=9D=B4=EC=83=81=EC=9D=B8=20=EA=B2=BD=EC=9A=B0=EC=97=90?= =?UTF-8?q?=EB=A7=8C=20=EA=B7=B8=EB=A3=B9=EB=82=B4=20=EB=8B=A4=EB=A5=B8=20?= =?UTF-8?q?=EB=A9=A4=EB=B2=84=EC=9D=98=20=EA=B8=B0=EB=A1=9D=EC=9D=84=20?= =?UTF-8?q?=EC=82=AD=EC=A0=9C=ED=95=A0=20=EC=88=98=20=EC=9E=88=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../group-achievement.service.spec.ts | 42 +++++++++++++++++-- .../application/group-achievement.service.ts | 37 ++++++++++++---- .../group-achievement.controller.spec.ts | 24 +++++++++++ .../entities/group-achievement.repository.ts | 12 +++--- .../achievement/group-achievement.module.ts | 2 + 5 files changed, 100 insertions(+), 17 deletions(-) diff --git a/BE/src/group/achievement/application/group-achievement.service.spec.ts b/BE/src/group/achievement/application/group-achievement.service.spec.ts index fe8c61d1..dfd8beee 100644 --- a/BE/src/group/achievement/application/group-achievement.service.spec.ts +++ b/BE/src/group/achievement/application/group-achievement.service.spec.ts @@ -30,6 +30,7 @@ import { UsersService } from '../../../users/application/users.service'; import { UsersModule } from '../../../users/users.module'; import { GroupAchievementRepository } from '../entities/group-achievement.repository'; import { GroupAchievementUpdateRequest } from '../dto/group-achievement-update-request'; +import { NoSuchUserGroupException } from '../../group/exception/no-such-user-group.exception'; describe('GroupAchievementService Test', () => { let groupAchievementService: GroupAchievementService; @@ -774,7 +775,40 @@ describe('GroupAchievementService Test', () => { }); }); - test('남의 달성기록을 삭제하려하면 NoSuchGroupAchievementException를 던진다.', async () => { + test('관리자나 매니저는 다른 사람이 작성한 달성기록을 삭제할 수 있다.', async () => { + await transactionTest(dataSource, async () => { + // given + const leader = await usersFixture.getUser('ABC'); + const member = await usersFixture.getUser('DEF'); + const group = await groupFixture.createGroup('GROUP1', leader); + await groupFixture.addMember(group, member, UserGroupGrade.PARTICIPANT); + const groupAchievement = + await groupAchievementFixture.createGroupAchievement( + member, + group, + null, + 'title', + ); + + // when + await groupAchievementService.delete( + leader.id, + group.id, + groupAchievement.id, + ); + const findOne = + await groupAchievementRepository.findOneByIdAndUserAndGroup( + member.id, + group.id, + groupAchievement.id, + ); + + // then + expect(findOne).toBeUndefined(); + }); + }); + + test('관리자나 매니저가 아닌 사람이 남의 달성기록을 삭제하려하면 NoSuchGroupAchievementException를 던진다.', async () => { await transactionTest(dataSource, async () => { // given const user1 = await usersFixture.getUser('ABC'); @@ -793,10 +827,10 @@ describe('GroupAchievementService Test', () => { // then await expect( groupAchievementService.delete(user2.id, group.id, groupAchievement.id), - ).rejects.toThrow(NoSuchGroupAchievementException); + ).rejects.toThrow(UnauthorizedAchievementException); }); }); - test('다른 그룹의 달성을 삭제하려하면 NoSuchGroupAchievementException를 던진다.', async () => { + test('다른 그룹의 달성을 삭제하려하면 NoSuchUserGroupException 던진다.', async () => { await transactionTest(dataSource, async () => { // given const user1 = await usersFixture.getUser('ABC'); @@ -819,7 +853,7 @@ describe('GroupAchievementService Test', () => { group1.id, groupAchievement.id, ), - ).rejects.toThrow(NoSuchGroupAchievementException); + ).rejects.toThrow(NoSuchUserGroupException); }); }); diff --git a/BE/src/group/achievement/application/group-achievement.service.ts b/BE/src/group/achievement/application/group-achievement.service.ts index 4763bacc..cd546555 100644 --- a/BE/src/group/achievement/application/group-achievement.service.ts +++ b/BE/src/group/achievement/application/group-achievement.service.ts @@ -18,10 +18,12 @@ import { NoSuchAchievementException } from '../../../achievement/exception/no-su import { UnauthorizedAchievementException } from '../../../achievement/exception/unauthorized-achievement.exception'; import { PaginateGroupAchievementRequest } from '../dto/paginate-group-achievement-request'; import { PaginateGroupAchievementResponse } from '../dto/paginate-group-achievement-response'; -import { GroupAchievementResponse } from '../dto/group-achievement-response'; import { GroupAchievementDeleteResponse } from '../dto/group-achievement-delete-response'; import { GroupAchievementUpdateRequest } from '../dto/group-achievement-update-request'; import { GroupAchievementUpdateResponse } from '../dto/group-achievement-update-response'; +import { UserGroupRepository } from '../../group/entities/user-group.repository'; +import { NoSuchUserGroupException } from '../../group/exception/no-such-user-group.exception'; +import { UserGroupGrade } from '../../group/domain/user-group-grade'; @Injectable() export class GroupAchievementService { @@ -31,6 +33,7 @@ export class GroupAchievementService { private readonly groupRepository: GroupRepository, private readonly imageRepository: ImageRepository, private readonly userBlockedGroupAchievementRepository: UserBlockedGroupAchievementRepository, + private readonly userGroupRepository: UserGroupRepository, ) {} @Transactional() @@ -166,12 +169,23 @@ export class GroupAchievementService { return achievement; } - async delete(userId: number, groupId: number, achievementId: number) { - const achievement = await this.getAchievement( - achievementId, - userId, - groupId, - ); + async delete(requesterId: number, groupId: number, achievementId: number) { + const achievement = + await this.groupAchievementRepository.findOneByIdAndGroupId( + achievementId, + groupId, + ); + if (!achievement) throw new NoSuchGroupAchievementException(); + + if (achievement.user.id != requesterId) { + const userGroup = await this.getUserGroup(requesterId, groupId); + if ( + userGroup.grade !== UserGroupGrade.LEADER && + userGroup.grade !== UserGroupGrade.MANAGER + ) + throw new UnauthorizedAchievementException(); + } + await this.groupAchievementRepository.repository.softDelete(achievement.id); return GroupAchievementDeleteResponse.from(achievement); } @@ -190,4 +204,13 @@ export class GroupAchievementService { if (!achievement) throw new NoSuchGroupAchievementException(); return achievement; } + + private async getUserGroup(userId: number, groupId: number) { + const userGroup = await this.userGroupRepository.findOneByUserIdAndGroupId( + userId, + groupId, + ); + if (!userGroup) throw new NoSuchUserGroupException(); + return userGroup; + } } diff --git a/BE/src/group/achievement/controller/group-achievement.controller.spec.ts b/BE/src/group/achievement/controller/group-achievement.controller.spec.ts index 1ae335d6..ae91aa3c 100644 --- a/BE/src/group/achievement/controller/group-achievement.controller.spec.ts +++ b/BE/src/group/achievement/controller/group-achievement.controller.spec.ts @@ -614,6 +614,30 @@ describe('GroupAchievementController', () => { }); }); + it('삭제 권한이 없는 경우에는 403을 반환한다.', async () => { + // given + const { accessToken } = await authFixture.getAuthenticatedUser('ABC'); + + when( + mockGroupAchievementService.delete( + anyNumber(), + anyNumber(), + anyNumber(), + ), + ).thenThrow(new UnauthorizedAchievementException()); + + // when + // then + return request(app.getHttpServer()) + .delete('/api/v1/groups/1/achievements/1') + .set('Authorization', `Bearer ${accessToken}`) + .expect(403) + .expect((res: request.Response) => { + expect(res.body.success).toBe(false); + expect(res.body.message).toBe('달성기록에 접근할 수 없습니다.'); + }); + }); + it('잘못된 인증시 401을 반환한다.', async () => { // given const accessToken = 'abcd.abcd.efgh'; diff --git a/BE/src/group/achievement/entities/group-achievement.repository.ts b/BE/src/group/achievement/entities/group-achievement.repository.ts index c261ece8..c48a3b20 100644 --- a/BE/src/group/achievement/entities/group-achievement.repository.ts +++ b/BE/src/group/achievement/entities/group-achievement.repository.ts @@ -164,12 +164,12 @@ export class GroupAchievementRepository extends TransactionalRepository