diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/domain/capsule/group_capsule/repository/GroupCapsuleQueryRepository.java b/backend/core/src/main/java/site/timecapsulearchive/core/domain/capsule/group_capsule/repository/GroupCapsuleQueryRepository.java index 047d86fff..b305e80ac 100644 --- a/backend/core/src/main/java/site/timecapsulearchive/core/domain/capsule/group_capsule/repository/GroupCapsuleQueryRepository.java +++ b/backend/core/src/main/java/site/timecapsulearchive/core/domain/capsule/group_capsule/repository/GroupCapsuleQueryRepository.java @@ -166,4 +166,14 @@ public Slice findMyGroupCapsuleSlice( return new SliceImpl<>(groupCapsules, Pageable.ofSize(size), hasNext); } + + public boolean findGroupCapsuleExistByGroupId(Long groupId) { + Integer count = jpaQueryFactory + .selectOne() + .from(capsule) + .where(capsule.group.id.eq(groupId)) + .fetchFirst(); + + return count != null; + } } diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/api/GroupApi.java b/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/api/GroupApi.java index d90e76ef5..e79948b4c 100644 --- a/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/api/GroupApi.java +++ b/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/api/GroupApi.java @@ -3,6 +3,7 @@ import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.enums.ParameterIn; +import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.responses.ApiResponses; @@ -19,6 +20,7 @@ import site.timecapsulearchive.core.domain.group.data.response.GroupDetailResponse; import site.timecapsulearchive.core.domain.group.data.response.GroupsSliceResponse; import site.timecapsulearchive.core.global.common.response.ApiSpec; +import site.timecapsulearchive.core.global.error.ErrorResponse; public interface GroupApi { @@ -62,20 +64,46 @@ ResponseEntity> createGroup( @Operation( summary = "그룹 삭제", - description = "그룹장인 경우에 특정 그룹을 삭제한다.", + description = """ + 그룹 삭제를 요청한 사용자가 해당 그룹의 그룹장인 경우 그룹을 삭제한다.
+ 주의 + + """, security = {@SecurityRequirement(name = "user_token")}, tags = {"group"} ) @ApiResponses(value = { @ApiResponse( - responseCode = "204", - description = "처리완료" - ) + responseCode = "202", + description = "처리 시작" + ), + @ApiResponse( + responseCode = "400", + description = """ + 다음의 경우 예외가 발생한다. + + """, + content = @Content(schema = @Schema(implementation = ErrorResponse.class)) + ), + @ApiResponse( + responseCode = "404", + description = "그룹이 존재하지 않으면 발생한다.", + content = @Content(schema = @Schema(implementation = ErrorResponse.class)) + ), }) - @DeleteMapping(value = "/groups/{group_id}") - ResponseEntity deleteGroupById( - @Parameter(in = ParameterIn.PATH, description = "수정할 그룹 아이디", required = true, schema = @Schema()) - @PathVariable("group_id") Long groupId + ResponseEntity> deleteGroupById( + Long memberId, + + @Parameter(in = ParameterIn.PATH, description = "삭제할 그룹 아이디", required = true) + Long groupId ); @Operation( diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/api/GroupApiController.java b/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/api/GroupApiController.java index e8a067e35..aade3b1af 100644 --- a/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/api/GroupApiController.java +++ b/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/api/GroupApiController.java @@ -6,6 +6,7 @@ import org.springframework.data.domain.Slice; import org.springframework.http.ResponseEntity; import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; @@ -60,9 +61,15 @@ public ResponseEntity> createGroup( ); } + @DeleteMapping(value = "/{group_id}") @Override - public ResponseEntity deleteGroupById(Long groupId) { - return null; + public ResponseEntity> deleteGroupById( + @AuthenticationPrincipal final Long memberId, + @PathVariable("group_id") final Long groupId + ) { + groupService.deleteGroup(memberId, groupId); + + return ResponseEntity.ok(ApiSpec.empty(SuccessCode.SUCCESS)); } @Override diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/entity/Group.java b/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/entity/Group.java index 08f7765bf..9f8e0eacd 100644 --- a/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/entity/Group.java +++ b/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/entity/Group.java @@ -38,12 +38,6 @@ public class Group extends BaseEntity { @Column(name = "group_profile_url", nullable = false) private String groupProfileUrl; - @OneToMany(mappedBy = "group", cascade = CascadeType.ALL, orphanRemoval = true) - private List capsules; - - @OneToMany(mappedBy = "group", cascade = CascadeType.ALL, orphanRemoval = true) - private List members; - @Builder private Group(String groupName, String groupDescription, String groupProfileUrl) { if (Objects.isNull(groupName) diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/exception/GroupDeleteFailException.java b/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/exception/GroupDeleteFailException.java new file mode 100644 index 000000000..663b76e2a --- /dev/null +++ b/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/exception/GroupDeleteFailException.java @@ -0,0 +1,11 @@ +package site.timecapsulearchive.core.domain.group.exception; + +import site.timecapsulearchive.core.global.error.ErrorCode; +import site.timecapsulearchive.core.global.error.exception.BusinessException; + +public class GroupDeleteFailException extends BusinessException { + + public GroupDeleteFailException(ErrorCode errorCode) { + super(errorCode); + } +} diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/repository/GroupRepository.java b/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/repository/GroupRepository.java index 383258106..cd596d4ef 100644 --- a/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/repository/GroupRepository.java +++ b/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/repository/GroupRepository.java @@ -10,4 +10,5 @@ public interface GroupRepository extends Repository { Optional findGroupById(Long groupId); + void delete(Group group); } \ No newline at end of file diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/repository/MemberGroupRepository.java b/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/repository/MemberGroupRepository.java index b5e46ade4..910146376 100644 --- a/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/repository/MemberGroupRepository.java +++ b/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/repository/MemberGroupRepository.java @@ -1,9 +1,14 @@ package site.timecapsulearchive.core.domain.group.repository; +import java.util.List; import org.springframework.data.repository.Repository; import site.timecapsulearchive.core.domain.group.entity.MemberGroup; public interface MemberGroupRepository extends Repository { void save(MemberGroup memberGroup); + + List findMemberGroupsByGroupId(Long groupId); + + void delete(MemberGroup mg); } diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/service/GroupService.java b/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/service/GroupService.java index 98b3b1603..f279e3fdf 100644 --- a/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/service/GroupService.java +++ b/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/service/GroupService.java @@ -1,7 +1,7 @@ package site.timecapsulearchive.core.domain.group.service; -import lombok.RequiredArgsConstructor; import java.time.ZonedDateTime; +import java.util.List; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Slice; import org.springframework.stereotype.Service; @@ -9,11 +9,13 @@ import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.support.TransactionCallbackWithoutResult; import org.springframework.transaction.support.TransactionTemplate; +import site.timecapsulearchive.core.domain.capsule.group_capsule.repository.GroupCapsuleQueryRepository; import site.timecapsulearchive.core.domain.group.data.dto.GroupCreateDto; import site.timecapsulearchive.core.domain.group.data.dto.GroupDetailDto; import site.timecapsulearchive.core.domain.group.data.dto.GroupSummaryDto; import site.timecapsulearchive.core.domain.group.entity.Group; import site.timecapsulearchive.core.domain.group.entity.MemberGroup; +import site.timecapsulearchive.core.domain.group.exception.GroupDeleteFailException; import site.timecapsulearchive.core.domain.group.exception.GroupNotFoundException; import site.timecapsulearchive.core.domain.group.repository.GroupQueryRepository; import site.timecapsulearchive.core.domain.group.repository.GroupRepository; @@ -21,7 +23,9 @@ import site.timecapsulearchive.core.domain.member.entity.Member; import site.timecapsulearchive.core.domain.member.exception.MemberNotFoundException; import site.timecapsulearchive.core.domain.member.repository.MemberRepository; +import site.timecapsulearchive.core.global.error.ErrorCode; import site.timecapsulearchive.core.infra.queue.manager.SocialNotificationManager; +import site.timecapsulearchive.core.infra.s3.manager.S3ObjectManager; @Service @RequiredArgsConstructor @@ -33,6 +37,8 @@ public class GroupService { private final TransactionTemplate transactionTemplate; private final SocialNotificationManager socialNotificationManager; private final GroupQueryRepository groupQueryRepository; + private final GroupCapsuleQueryRepository groupCapsuleQueryRepository; + private final S3ObjectManager s3ObjectManager; public void createGroup(final Long memberId, final GroupCreateDto dto) { final Member member = memberRepository.findMemberById(memberId) @@ -84,4 +90,48 @@ public GroupDetailDto findGroupDetailByGroupId(final Long memberId, final Long g return groupDetailDto; } + + /** + * 사용자가 그룹장인 그룹을 삭제한다. + *
주의 - 그룹 삭제 시 아래 조건에 해당하면 예외가 발생한다. + *
1. 그룹에 멤버가 존재하는 경우 + *
2. 그룹 삭제를 요청한 사용자가 그룹장이 아닌 경우 + *
3. 그룹에 그룹 캡슐이 남아있는 경우 + * + * @param memberId 그룹에 속한 그룹장 아이디 + * @param groupId 그룹 아이디 + */ + public void deleteGroup(final Long memberId, final Long groupId) { + final String[] groupProfilePath = new String[1]; + + transactionTemplate.executeWithoutResult(transactionStatus -> { + final Group group = groupRepository.findGroupById(groupId) + .orElseThrow(GroupNotFoundException::new); + groupProfilePath[0] = group.getGroupProfileUrl(); + + final List groupMembers = memberGroupRepository.findMemberGroupsByGroupId( + groupId); + final boolean isGroupOwner = groupMembers.stream() + .anyMatch(mg -> mg.getMember().getId().equals(memberId) && mg.getIsOwner()); + if (!isGroupOwner) { + throw new GroupDeleteFailException(ErrorCode.NO_GROUP_AUTHORITY_ERROR); + } + + final boolean groupMemberExist = groupMembers.size() > 1; + if (groupMemberExist) { + throw new GroupDeleteFailException(ErrorCode.GROUP_MEMBER_EXIST_ERROR); + } + groupMembers.forEach(memberGroupRepository::delete); + + final boolean groupCapsuleExist = groupCapsuleQueryRepository.findGroupCapsuleExistByGroupId( + groupId); + if (groupCapsuleExist) { + throw new GroupDeleteFailException(ErrorCode.GROUP_CAPSULE_EXIST_ERROR); + } + + groupRepository.delete(group); + }); + + s3ObjectManager.deleteObject(groupProfilePath[0]); + } } diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/global/error/ErrorCode.java b/backend/core/src/main/java/site/timecapsulearchive/core/global/error/ErrorCode.java index d1f5dffa2..f4276f17a 100644 --- a/backend/core/src/main/java/site/timecapsulearchive/core/global/error/ErrorCode.java +++ b/backend/core/src/main/java/site/timecapsulearchive/core/global/error/ErrorCode.java @@ -60,6 +60,9 @@ public enum ErrorCode { //group GROUP_CREATE_ERROR(400, "GROUP-001", "그룹 생성에 실패하였습니다."), GROUP_NOT_FOUND_ERROR(404, "GROUP-002", "그룹을 찾을 수 없습니다"), + GROUP_MEMBER_EXIST_ERROR(400, "GROUP-003", "다른 그룹 멤버가 존재해 그룹을 삭제할 수 없습니다."), + NO_GROUP_AUTHORITY_ERROR(403, "GROUP-004", "그룹에 대한 권한이 존재하지 않습니다."), + GROUP_CAPSULE_EXIST_ERROR(400, "GROUP-005", "그룹 캡슐이 존재해 그룹을 삭제할 수 없습니다."), //friend invite FRIEND_INVITE_NOT_FOUND_ERROR(404, "FRIEND-INVITE-001", "친구 요청을 찾지 못하였습니다."), diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/infra/s3/config/S3Config.java b/backend/core/src/main/java/site/timecapsulearchive/core/infra/s3/config/S3Config.java index 5d18172e2..8378fdee0 100644 --- a/backend/core/src/main/java/site/timecapsulearchive/core/infra/s3/config/S3Config.java +++ b/backend/core/src/main/java/site/timecapsulearchive/core/infra/s3/config/S3Config.java @@ -6,6 +6,7 @@ import software.amazon.awssdk.auth.credentials.AwsBasicCredentials; import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider; import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.services.s3.S3Client; import software.amazon.awssdk.services.s3.presigner.S3Presigner; @Configuration @@ -23,6 +24,14 @@ public S3Presigner s3Presigner() { .build(); } + @Bean + public S3Client s3Client() { + return S3Client.builder() + .credentialsProvider(StaticCredentialsProvider.create(awsBasicCredentials())) + .region(Region.of(s3Properties.region())) + .build(); + } + private AwsBasicCredentials awsBasicCredentials() { return AwsBasicCredentials.create(s3Properties.accessKey(), s3Properties.secretKey()); } diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/infra/s3/manager/S3ObjectManager.java b/backend/core/src/main/java/site/timecapsulearchive/core/infra/s3/manager/S3ObjectManager.java new file mode 100644 index 000000000..c8974b286 --- /dev/null +++ b/backend/core/src/main/java/site/timecapsulearchive/core/infra/s3/manager/S3ObjectManager.java @@ -0,0 +1,42 @@ +package site.timecapsulearchive.core.infra.s3.manager; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; +import site.timecapsulearchive.core.infra.s3.config.S3Config; +import software.amazon.awssdk.awscore.exception.AwsServiceException; +import software.amazon.awssdk.core.exception.SdkClientException; +import software.amazon.awssdk.services.s3.S3Client; +import software.amazon.awssdk.services.s3.model.DeleteObjectRequest; + +@Slf4j +@Component +public class S3ObjectManager { + + private final S3Client s3Client; + private final String bucketName; + + public S3ObjectManager(S3Client s3Client, S3Config s3config) { + this.s3Client = s3Client; + this.bucketName = s3config.getBucketName(); + } + + /** + * s3 상의 경로를 입력한다. (버킷명 제외) + *
ex) capsule/1/test.jpg + * @param path s3 상에서 경로 + */ + public void deleteObject(String path) { + try { + s3Client.deleteObject( + DeleteObjectRequest.builder() + .bucket(bucketName) + .key(path) + .build() + ); + } catch (AwsServiceException e) { + log.error("Aws S3에서 삭제 요청 처리 실패 - {}", path, e); + } catch (SdkClientException e) { + log.error("Aws S3 삭제 요청 클라이언트 처리 실패 - {}", path, e); + } + } +} diff --git a/backend/core/src/test/java/site/timecapsulearchive/core/common/fixture/domain/MemberFixture.java b/backend/core/src/test/java/site/timecapsulearchive/core/common/fixture/domain/MemberFixture.java index 9126a5f9a..275977da0 100644 --- a/backend/core/src/test/java/site/timecapsulearchive/core/common/fixture/domain/MemberFixture.java +++ b/backend/core/src/test/java/site/timecapsulearchive/core/common/fixture/domain/MemberFixture.java @@ -1,5 +1,6 @@ package site.timecapsulearchive.core.common.fixture.domain; +import java.lang.reflect.Field; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.List; @@ -14,6 +15,28 @@ public class MemberFixture { private static final HashEncryptionManager hashEncryptionManager = UnitTestDependency.hashEncryptionManager(); + /** + * 테스트 픽스처 - {@code Member}의 {@code id}가 주어진 {@code memberId}로 설정된 멤버 엔티티를 생성한다. + * @param memberId 설정할 멤버 아이디 + * @return {@code Member}의 {@code id}가 {@code memberId}로 설정된 {@code Member} 테스트 픽스처 + */ + public static Member memberWithMemberId(long memberId) { + try { + Member member = member((int) memberId); + setFieldValue(member, "id", memberId); + return member; + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + private static void setFieldValue(Object instance, String fieldName, Object value) + throws NoSuchFieldException, IllegalAccessException { + Field field = instance.getClass().getDeclaredField(fieldName); + field.setAccessible(true); + field.set(instance, value); + } + /** * 테스트 픽스처 - 멤버 마다 상이한 값을 위한 dataPrefix를 주면 멤버를 생성한다. *
주의 - 테스트에서 같은 prefix를 사용하면 오류가 발생하므로 서로 다른 prefix를 쓰도록 해야함. @@ -77,4 +100,19 @@ public static List getPhoneBytesList(int start, int count) { return result; } + + /** + * 테스트 픽스처 - 리스트의 {@code Member}의 {@code id}가 주어진 {@code memberId}로 설정된 멤버 엔티티 리스트를 생성한다. + * @param startDataPrefix 시작 id + * @param count 크기 + * @return 리스트의 각 {@code Member}의 {@code id}가 {@code memberId}로 설정된 {@code List} 테스트 픽스처 + */ + public static List membersWithMemberId(int startDataPrefix, int count) { + List result = new ArrayList<>(); + for (int index = startDataPrefix; index < startDataPrefix + count; index++) { + result.add(memberWithMemberId(index)); + } + + return result; + } } \ No newline at end of file diff --git a/backend/core/src/test/java/site/timecapsulearchive/core/common/fixture/domain/MemberGroupFixture.java b/backend/core/src/test/java/site/timecapsulearchive/core/common/fixture/domain/MemberGroupFixture.java index 6ec83101e..0237e4212 100644 --- a/backend/core/src/test/java/site/timecapsulearchive/core/common/fixture/domain/MemberGroupFixture.java +++ b/backend/core/src/test/java/site/timecapsulearchive/core/common/fixture/domain/MemberGroupFixture.java @@ -16,7 +16,7 @@ public class MemberGroupFixture { * @param group 대상 그룹 * @return 그룹 멤버 테스트 픽스처 */ - public static MemberGroup memberGroup(Member member, Group group) { + public static MemberGroup groupOwner(Member member, Group group) { return MemberGroup.createGroupOwner(member, group); } diff --git a/backend/core/src/test/java/site/timecapsulearchive/core/common/spy/FakeTransactionTemplate.java b/backend/core/src/test/java/site/timecapsulearchive/core/common/spy/FakeTransactionTemplate.java new file mode 100644 index 000000000..ac11780f0 --- /dev/null +++ b/backend/core/src/test/java/site/timecapsulearchive/core/common/spy/FakeTransactionTemplate.java @@ -0,0 +1,22 @@ +package site.timecapsulearchive.core.common.spy; + +import static org.mockito.Mockito.spy; + +import java.util.function.Consumer; +import org.springframework.transaction.TransactionException; +import org.springframework.transaction.TransactionStatus; +import org.springframework.transaction.support.SimpleTransactionStatus; +import org.springframework.transaction.support.TransactionTemplate; + +public class FakeTransactionTemplate extends TransactionTemplate { + + public static FakeTransactionTemplate spied() { + return spy(new FakeTransactionTemplate()); + } + + @Override + public void executeWithoutResult(Consumer action) + throws TransactionException { + action.accept(new SimpleTransactionStatus()); + } +} diff --git a/backend/core/src/test/java/site/timecapsulearchive/core/domain/capsule/repository/GroupCapsuleOpenQueryRepositoryTest.java b/backend/core/src/test/java/site/timecapsulearchive/core/domain/capsule/group_capsule/repository/GroupCapsuleOpenQueryRepositoryTest.java similarity index 95% rename from backend/core/src/test/java/site/timecapsulearchive/core/domain/capsule/repository/GroupCapsuleOpenQueryRepositoryTest.java rename to backend/core/src/test/java/site/timecapsulearchive/core/domain/capsule/group_capsule/repository/GroupCapsuleOpenQueryRepositoryTest.java index ccb33429c..fd7707cd2 100644 --- a/backend/core/src/test/java/site/timecapsulearchive/core/domain/capsule/repository/GroupCapsuleOpenQueryRepositoryTest.java +++ b/backend/core/src/test/java/site/timecapsulearchive/core/domain/capsule/group_capsule/repository/GroupCapsuleOpenQueryRepositoryTest.java @@ -1,4 +1,4 @@ -package site.timecapsulearchive.core.domain.capsule.repository; +package site.timecapsulearchive.core.domain.capsule.group_capsule.repository; import static org.assertj.core.api.Assertions.assertThat; @@ -22,7 +22,6 @@ import site.timecapsulearchive.core.domain.capsule.entity.Capsule; import site.timecapsulearchive.core.domain.capsule.entity.CapsuleType; import site.timecapsulearchive.core.domain.capsule.entity.GroupCapsuleOpen; -import site.timecapsulearchive.core.domain.capsule.group_capsule.repository.GroupCapsuleOpenQueryRepository; import site.timecapsulearchive.core.domain.capsuleskin.entity.CapsuleSkin; import site.timecapsulearchive.core.domain.member.entity.Member; diff --git a/backend/core/src/test/java/site/timecapsulearchive/core/domain/group/service/GroupServiceTest.java b/backend/core/src/test/java/site/timecapsulearchive/core/domain/group/service/GroupServiceTest.java new file mode 100644 index 000000000..4ffff305b --- /dev/null +++ b/backend/core/src/test/java/site/timecapsulearchive/core/domain/group/service/GroupServiceTest.java @@ -0,0 +1,192 @@ +package site.timecapsulearchive.core.domain.group.service; + +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import org.junit.jupiter.api.Test; +import org.springframework.transaction.support.TransactionTemplate; +import site.timecapsulearchive.core.common.fixture.domain.GroupFixture; +import site.timecapsulearchive.core.common.fixture.domain.MemberFixture; +import site.timecapsulearchive.core.common.fixture.domain.MemberGroupFixture; +import site.timecapsulearchive.core.common.spy.FakeTransactionTemplate; +import site.timecapsulearchive.core.domain.capsule.group_capsule.repository.GroupCapsuleQueryRepository; +import site.timecapsulearchive.core.domain.group.entity.Group; +import site.timecapsulearchive.core.domain.group.entity.MemberGroup; +import site.timecapsulearchive.core.domain.group.exception.GroupDeleteFailException; +import site.timecapsulearchive.core.domain.group.exception.GroupNotFoundException; +import site.timecapsulearchive.core.domain.group.repository.GroupQueryRepository; +import site.timecapsulearchive.core.domain.group.repository.GroupRepository; +import site.timecapsulearchive.core.domain.group.repository.MemberGroupRepository; +import site.timecapsulearchive.core.domain.member.repository.MemberRepository; +import site.timecapsulearchive.core.global.error.ErrorCode; +import site.timecapsulearchive.core.infra.queue.manager.SocialNotificationManager; +import site.timecapsulearchive.core.infra.s3.manager.S3ObjectManager; + +/** + * 테스트 케이스 + *
    + *
  1. 모든 분기 테스트
  2. + *
      + *
    • 그룹이 없는 경우
    • + *
    • 그룹장이 아닌 경우
    • + *
    • 그룹원이 있는 경우
    • + *
    • 그룹 캡슐이 존재하는 경우
    • + *
    • 그룹을 삭제할 수 있는 모든 조건(그룹 존재, 그룹에 속한 그룹원 없음, 요청한 사용자가 그룹장, 그룹 캡슐 없음)을 만족한 경우
    • + *
    + *
+ */ +class GroupServiceTest { + + private final GroupRepository groupRepository = mock(GroupRepository.class); + private final MemberRepository memberRepository = mock(MemberRepository.class); + private final MemberGroupRepository memberGroupRepository = mock(MemberGroupRepository.class); + private final TransactionTemplate transactionTemplate = FakeTransactionTemplate.spied(); + private final SocialNotificationManager socialNotificationManager = mock( + SocialNotificationManager.class); + private final GroupQueryRepository groupQueryRepository = mock(GroupQueryRepository.class); + private final GroupCapsuleQueryRepository groupCapsuleQueryRepository = mock( + GroupCapsuleQueryRepository.class); + private final S3ObjectManager s3ObjectManager = mock(S3ObjectManager.class); + + private final GroupService groupService = new GroupService( + groupRepository, + memberRepository, + memberGroupRepository, + transactionTemplate, + socialNotificationManager, + groupQueryRepository, + groupCapsuleQueryRepository, + s3ObjectManager + ); + + @Test + void 존재하지_않는_그룹_아이디로_삭제를_시도하면_예외가_발생한다() { + //given + Long memberId = 1L; + Long notExistGroupId = 1L; + + given(groupRepository.findGroupById(anyLong())).willReturn(Optional.empty()); + + //when + //then + assertThatThrownBy(() -> groupService.deleteGroup(memberId, notExistGroupId)) + .isExactlyInstanceOf(GroupNotFoundException.class) + .hasMessageContaining(ErrorCode.GROUP_NOT_FOUND_ERROR.getMessage()); + } + + @Test + void 그룹장이_아닌_사용자가_그룹_아이디로_삭제를_시도하면_예외가_발생한다() { + //given + Long groupMemberId = 1L; + Long groupId = 1L; + + given(groupRepository.findGroupById(anyLong())).willReturn(group()); + given(memberGroupRepository.findMemberGroupsByGroupId(groupId)).willReturn(notOwnerGroupMember()); + + //when + //then + assertThatThrownBy(() -> groupService.deleteGroup(groupMemberId, groupId)) + .isExactlyInstanceOf(GroupDeleteFailException.class) + .hasMessageContaining(ErrorCode.NO_GROUP_AUTHORITY_ERROR.getMessage()); + } + + private List notOwnerGroupMember() { + return List.of( + MemberGroupFixture.memberGroup( + MemberFixture.memberWithMemberId(1), + GroupFixture.group(), + false + ) + ); + } + + @Test + void 그룹원이_존재하는_그룹_아이디로_삭제를_시도하면_예외가_발생한다() { + //given + Long groupOwnerId = 1L; + Long groupMemberExistGroupId = 1L; + + given(groupRepository.findGroupById(anyLong())).willReturn(group()); + given(memberGroupRepository.findMemberGroupsByGroupId(groupMemberExistGroupId)).willReturn( + groupMembers()); + + //when + //then + assertThatThrownBy(() -> groupService.deleteGroup(groupOwnerId, groupMemberExistGroupId)) + .isExactlyInstanceOf(GroupDeleteFailException.class) + .hasMessageContaining(ErrorCode.GROUP_MEMBER_EXIST_ERROR.getMessage()); + } + + private Optional group() { + return Optional.of( + GroupFixture.group() + ); + } + + private List groupMembers() { + Group group = GroupFixture.group(); + MemberGroup groupOwner = MemberGroupFixture.groupOwner(MemberFixture.memberWithMemberId(1L), group); + + List memberGroups = MemberGroupFixture.memberGroups( + MemberFixture.membersWithMemberId(2, 2), + group + ); + + List result = new ArrayList<>(memberGroups); + result.add(groupOwner); + return result; + } + + @Test + void 그룹_캡슐이_존재하는_그룹_아이디로_삭제를_시도하면_예외가_발생한다() { + //given + Long groupOwnerId = 1L; + Long groupCapsuleExistGroupId = 1L; + + given(groupRepository.findGroupById(anyLong())).willReturn(group()); + given(memberGroupRepository.findMemberGroupsByGroupId(groupCapsuleExistGroupId)).willReturn( + ownerGroupMember()); + given(groupCapsuleQueryRepository.findGroupCapsuleExistByGroupId(anyLong())).willReturn(true); + + //when + //then + assertThatThrownBy(() -> groupService.deleteGroup(groupOwnerId, groupCapsuleExistGroupId)) + .isExactlyInstanceOf(GroupDeleteFailException.class) + .hasMessageContaining(ErrorCode.GROUP_CAPSULE_EXIST_ERROR.getMessage()); + } + + private List ownerGroupMember() { + return List.of( + MemberGroupFixture.groupOwner( + MemberFixture.memberWithMemberId(1), + GroupFixture.group() + ) + ); + } + + @Test + void 그룹을_삭제할_조건을_만족한_그룹_아이디로_삭제를_시도하면_그룹이_삭제된다() { + //given + Long groupOwnerId = 1L; + Long groupId = 1L; + + given(groupRepository.findGroupById(anyLong())).willReturn(group()); + given(memberGroupRepository.findMemberGroupsByGroupId(groupId)).willReturn( + ownerGroupMember()); + given(groupCapsuleQueryRepository.findGroupCapsuleExistByGroupId(anyLong())).willReturn(false); + + //when + //then + assertThatCode(() -> groupService.deleteGroup(groupOwnerId, groupId)).doesNotThrowAnyException(); + verify(groupRepository, times(1)).delete(any(Group.class)); + } +} \ No newline at end of file