diff --git a/src/main/java/com/study/realworld/domain/follow/api/FollowApiAdvice.java b/src/main/java/com/study/realworld/domain/follow/api/FollowApiAdvice.java new file mode 100644 index 00000000..5dab7cdc --- /dev/null +++ b/src/main/java/com/study/realworld/domain/follow/api/FollowApiAdvice.java @@ -0,0 +1,27 @@ +package com.study.realworld.domain.follow.api; + +import com.study.realworld.domain.follow.error.FollowErrorCode; +import com.study.realworld.domain.follow.error.FollowErrorResponse; +import com.study.realworld.domain.follow.error.exception.FollowBusinessException; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; + + +@RestControllerAdvice +public class FollowApiAdvice { + + private final Logger log = LogManager.getLogger(getClass()); + + @ExceptionHandler(FollowBusinessException.class) + protected ResponseEntity handleUserBusinessException(final FollowBusinessException followBusinessException) { + + log.error("FollowBusinessException: {}", followBusinessException.getMessage()); + + final FollowErrorCode followErrorCode = FollowErrorCode.values(followBusinessException); + final FollowErrorResponse followErrorResponse = FollowErrorResponse.from(followErrorCode); + return followErrorResponse.toResponseEntity(); + } +} diff --git a/src/main/java/com/study/realworld/domain/follow/application/FollowCommandService.java b/src/main/java/com/study/realworld/domain/follow/application/FollowCommandService.java index 9aa1723c..e682c3b7 100644 --- a/src/main/java/com/study/realworld/domain/follow/application/FollowCommandService.java +++ b/src/main/java/com/study/realworld/domain/follow/application/FollowCommandService.java @@ -4,6 +4,7 @@ import com.study.realworld.domain.follow.domain.FollowRepository; import com.study.realworld.domain.follow.dto.FollowResponse; import com.study.realworld.domain.follow.dto.UnFollowResponse; +import com.study.realworld.domain.follow.error.exception.DuplicatedFollowException; import com.study.realworld.domain.user.application.UserQueryService; import com.study.realworld.domain.user.domain.persist.User; import com.study.realworld.domain.user.domain.vo.UserName; @@ -23,9 +24,7 @@ public class FollowCommandService { public FollowResponse follow(final Long userId, final UserName userName) { final User followee = userQueryService.findByUserName(userName); final User follower = userQueryService.findById(userId); - if (followQueryService.existsByFolloweeAndFollower(followee, follower)) { - return FollowResponse.from(followee); - } + validateDuplicatedFollow(followee, follower); final Follow follow = followBuilder(followee, follower); return FollowResponse.from(followRepository.save(follow).followee()); } @@ -33,14 +32,17 @@ public FollowResponse follow(final Long userId, final UserName userName) { public UnFollowResponse unfollow(final Long suerId, final UserName userName) { final User followee = userQueryService.findByUserName(userName); final User follower = userQueryService.findById(suerId); - if (!followQueryService.existsByFolloweeAndFollower(followee, follower)) { - return UnFollowResponse.from(followee); - } final Follow follow = followQueryService.findByFolloweeAndFollower(followee, follower); followRepository.delete(follow); return UnFollowResponse.from(followee); } + private void validateDuplicatedFollow(final User followee, final User follower) { + if (followQueryService.existsByFolloweeAndFollower(followee, follower)) { + throw new DuplicatedFollowException(); + } + } + private Follow followBuilder(final User followee, final User follower) { return Follow.builder() .followee(followee) diff --git a/src/main/java/com/study/realworld/domain/follow/application/FollowQueryService.java b/src/main/java/com/study/realworld/domain/follow/application/FollowQueryService.java index bc4396a7..6912ea8b 100644 --- a/src/main/java/com/study/realworld/domain/follow/application/FollowQueryService.java +++ b/src/main/java/com/study/realworld/domain/follow/application/FollowQueryService.java @@ -4,6 +4,7 @@ import com.study.realworld.domain.follow.domain.FollowQueryDSLRepository; import com.study.realworld.domain.follow.domain.FollowRepository; import com.study.realworld.domain.follow.dto.ProfileResponse; +import com.study.realworld.domain.follow.error.exception.FollowNotFoundException; import com.study.realworld.domain.user.application.UserQueryService; import com.study.realworld.domain.user.domain.persist.User; import com.study.realworld.domain.user.domain.vo.UserName; @@ -21,20 +22,20 @@ public class FollowQueryService { private final FollowRepository followRepository; public ProfileResponse profile(final Long userId, final UserName username) { - final User user = userQueryService.findById(userId); + final User me = userQueryService.findById(userId); final User target = userQueryService.findByUserName(username); - final boolean isFollow = existsByFolloweeAndFollower(user, target); + final boolean isFollow = existsByFolloweeAndFollower(target, me); return new ProfileResponse(target, isFollow); } - public boolean existsByFolloweeAndFollower(final User user, final User target) { - return followRepository.existsByFolloweeAndFollower(target, user); + public boolean existsByFolloweeAndFollower(final User followee, final User follower) { + return followRepository.existsByFolloweeAndFollower(followee, follower); } public Follow findByFolloweeAndFollower(final User followee, final User follower) { return followRepository .findByFolloweeAndFollower(followee, follower) - .orElseThrow(IllegalArgumentException::new); + .orElseThrow(FollowNotFoundException::new); } public ProfileResponse profile2(final Long userId, final UserName username) { diff --git a/src/main/java/com/study/realworld/domain/follow/error/FollowErrorCode.java b/src/main/java/com/study/realworld/domain/follow/error/FollowErrorCode.java new file mode 100644 index 00000000..80b7d649 --- /dev/null +++ b/src/main/java/com/study/realworld/domain/follow/error/FollowErrorCode.java @@ -0,0 +1,44 @@ +package com.study.realworld.domain.follow.error; + +import com.study.realworld.domain.follow.error.exception.DuplicatedFollowException; +import com.study.realworld.domain.follow.error.exception.FollowBusinessException; +import com.study.realworld.domain.follow.error.exception.FollowNotFoundException; +import com.study.realworld.global.error.ErrorCode; +import org.springframework.http.HttpStatus; + +import java.util.Arrays; + +public enum FollowErrorCode implements ErrorCode { + + FOLLOW_NOT_FOUND(FollowNotFoundException.class, HttpStatus.BAD_REQUEST, "Follow is not exist"), + DUPLICATED_FOLLOW(DuplicatedFollowException.class, HttpStatus.BAD_REQUEST, "Follow is already exist"); + + private final Class exceptionClass; + private final HttpStatus httpStatus; + private final String message; + + FollowErrorCode(final Class exceptionClass, final HttpStatus httpStatus, final String message) { + this.exceptionClass = exceptionClass; + this.httpStatus = httpStatus; + this.message = message; + } + + public static FollowErrorCode values(final FollowBusinessException exception) { + final Class exceptionClass = exception.getClass(); + return Arrays.stream(values()) + .filter(userErrorCode -> userErrorCode.exceptionClass.equals(exceptionClass)) + .findFirst() + .orElseThrow(IllegalArgumentException::new); + } + + @Override + public HttpStatus httpStatus() { + return httpStatus; + } + + @Override + public String message() { + return message; + } + +} diff --git a/src/main/java/com/study/realworld/domain/follow/error/FollowErrorResponse.java b/src/main/java/com/study/realworld/domain/follow/error/FollowErrorResponse.java new file mode 100644 index 00000000..d876687b --- /dev/null +++ b/src/main/java/com/study/realworld/domain/follow/error/FollowErrorResponse.java @@ -0,0 +1,39 @@ +package com.study.realworld.domain.follow.error; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import com.fasterxml.jackson.annotation.JsonTypeName; +import com.study.realworld.global.error.ErrorCode; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.NoArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; + +import java.util.List; + +@AllArgsConstructor(access = AccessLevel.PRIVATE) +@NoArgsConstructor(access = AccessLevel.PACKAGE) +@JsonTypeName("errors") +@JsonTypeInfo(include = JsonTypeInfo.As.WRAPPER_OBJECT, use = JsonTypeInfo.Id.NAME) +public class FollowErrorResponse { + + @JsonIgnore + private HttpStatus httpStatus; + + @JsonProperty("body") + private List body; + + public static FollowErrorResponse from(final ErrorCode errorCode) { + return new FollowErrorResponse(errorCode.httpStatus(), List.of(errorCode.message())); + } + + public ResponseEntity toResponseEntity() { + return ResponseEntity.status(this.httpStatus).body(this); + } + + public List body() { + return body; + } +} diff --git a/src/main/java/com/study/realworld/domain/follow/error/exception/DuplicatedFollowException.java b/src/main/java/com/study/realworld/domain/follow/error/exception/DuplicatedFollowException.java new file mode 100644 index 00000000..0bfa3545 --- /dev/null +++ b/src/main/java/com/study/realworld/domain/follow/error/exception/DuplicatedFollowException.java @@ -0,0 +1,10 @@ +package com.study.realworld.domain.follow.error.exception; + +public class DuplicatedFollowException extends FollowBusinessException { + + private static final String DUPLICATED_FOLLOW_MESSAGE = "팔로우 정보가 이미 있습니다"; + + public DuplicatedFollowException() { + super(DUPLICATED_FOLLOW_MESSAGE); + } +} diff --git a/src/main/java/com/study/realworld/domain/follow/error/exception/FollowBusinessException.java b/src/main/java/com/study/realworld/domain/follow/error/exception/FollowBusinessException.java new file mode 100644 index 00000000..7b8b756b --- /dev/null +++ b/src/main/java/com/study/realworld/domain/follow/error/exception/FollowBusinessException.java @@ -0,0 +1,9 @@ +package com.study.realworld.domain.follow.error.exception; + +public class FollowBusinessException extends RuntimeException { + + public FollowBusinessException(final String message) { + super(message); + } + +} diff --git a/src/main/java/com/study/realworld/domain/follow/error/exception/FollowNotFoundException.java b/src/main/java/com/study/realworld/domain/follow/error/exception/FollowNotFoundException.java new file mode 100644 index 00000000..ca137f41 --- /dev/null +++ b/src/main/java/com/study/realworld/domain/follow/error/exception/FollowNotFoundException.java @@ -0,0 +1,10 @@ +package com.study.realworld.domain.follow.error.exception; + +public class FollowNotFoundException extends FollowBusinessException { + + private static final String FOLLOW_NOT_FOUND_MESSAGE = "팔로우 정보를 찾을 수 없습니다"; + + public FollowNotFoundException() { + super(FOLLOW_NOT_FOUND_MESSAGE); + } +} diff --git a/src/main/java/com/study/realworld/http/login.http b/src/main/java/com/study/realworld/http/login.http index 4b4ae8c1..24a822b9 100644 --- a/src/main/java/com/study/realworld/http/login.http +++ b/src/main/java/com/study/realworld/http/login.http @@ -1,4 +1,4 @@ -### JOIN +### JOIN, 삭제한 데이터에 대해서 배치나 스케줄링 써서 삭제하는 작업해야겠다. POST localhost:8080/api/users Content-Type: application/json @@ -38,3 +38,7 @@ Authorization: {{Authorization}} "image": "https://i.stack.imgur.com/xHWG8.jpg" } } + +### DELETE +DELETE localhost:8080/api/users +Authorization: {{Authorization}} diff --git a/src/test/java/com/study/realworld/domain/follow/application/FollowCommandServiceTest.java b/src/test/java/com/study/realworld/domain/follow/application/FollowCommandServiceTest.java new file mode 100644 index 00000000..ec242874 --- /dev/null +++ b/src/test/java/com/study/realworld/domain/follow/application/FollowCommandServiceTest.java @@ -0,0 +1,57 @@ +package com.study.realworld.domain.follow.application; + +import com.study.realworld.domain.follow.domain.Follow; +import com.study.realworld.domain.follow.domain.FollowRepository; +import com.study.realworld.domain.follow.dto.FollowResponse; +import com.study.realworld.domain.user.application.UserQueryService; +import com.study.realworld.domain.user.domain.persist.User; +import com.study.realworld.domain.user.domain.vo.UserBio; +import com.study.realworld.domain.user.domain.vo.UserImage; +import com.study.realworld.domain.user.domain.vo.UserName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import static com.study.realworld.domain.follow.domain.FollowTest.testFollower; +import static com.study.realworld.domain.user.domain.persist.UserTest.testUser; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.willReturn; + +@ExtendWith(MockitoExtension.class) +class FollowCommandServiceTest { + + @Mock + private UserQueryService userQueryService; + + @Mock + private FollowQueryService followQueryService; + + @Mock + private FollowRepository followRepository; + + @InjectMocks + private FollowCommandService followCommandService; + + @Test + void 팔로워_아이덴티티와_팔로위_이름을_입력하면_팔로우_할_수_있다() { + final User follower = testUser("user1@gmail.com", "user1", "password1", "bio1", "image1"); + final User followee = testUser("user2@gmail.com", "user2", "password2", "bio2", "image2"); + final Follow follow = testFollower(followee, follower); + willReturn(follower).given(userQueryService).findById(any()); + willReturn(followee).given(userQueryService).findByUserName(any()); + willReturn(false).given(followQueryService).existsByFolloweeAndFollower(any(), any()); + willReturn(follow).given(followRepository).save(any()); + + final FollowResponse followResponse = followCommandService.follow(1L, UserName.from("user2")); + assertAll( + () -> assertThat(followResponse.userName()).isEqualTo(UserName.from("user2")), + () -> assertThat(followResponse.userBio()).isEqualTo(UserBio.from("bio2")), + () -> assertThat(followResponse.userImage()).isEqualTo(UserImage.from("image2")), + () -> assertThat(followResponse.isFollowing()).isTrue() + ); + } +} diff --git a/src/test/java/com/study/realworld/domain/follow/application/FollowQueryServiceTest.java b/src/test/java/com/study/realworld/domain/follow/application/FollowQueryServiceTest.java new file mode 100644 index 00000000..8905c15a --- /dev/null +++ b/src/test/java/com/study/realworld/domain/follow/application/FollowQueryServiceTest.java @@ -0,0 +1,7 @@ +package com.study.realworld.domain.follow.application; + +import static org.junit.jupiter.api.Assertions.*; + +class FollowQueryServiceTest { + +} \ No newline at end of file diff --git a/src/test/java/com/study/realworld/domain/follow/domain/FollowTest.java b/src/test/java/com/study/realworld/domain/follow/domain/FollowTest.java index a09e17b0..6854c485 100644 --- a/src/test/java/com/study/realworld/domain/follow/domain/FollowTest.java +++ b/src/test/java/com/study/realworld/domain/follow/domain/FollowTest.java @@ -11,7 +11,7 @@ import static org.junit.jupiter.api.Assertions.assertAll; @DisplayName("팔로우 (Follow)") -class FollowTest { +public class FollowTest { @Test void 두명의_유저들을_토대로_객체를_생성할_수_있다() { diff --git a/src/test/java/com/study/realworld/domain/user/api/UserApiAdviceTest.java b/src/test/java/com/study/realworld/domain/user/api/UserApiAdviceTest.java deleted file mode 100644 index 8f52307a..00000000 --- a/src/test/java/com/study/realworld/domain/user/api/UserApiAdviceTest.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.study.realworld.domain.user.api; - -import static org.junit.jupiter.api.Assertions.*; - -class UserApiAdviceTest { - -} \ No newline at end of file diff --git a/src/test/java/com/study/realworld/domain/user/api/UserCommandApiTest.java b/src/test/java/com/study/realworld/domain/user/api/UserCommandApiTest.java deleted file mode 100644 index 73be39bf..00000000 --- a/src/test/java/com/study/realworld/domain/user/api/UserCommandApiTest.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.study.realworld.domain.user.api; - -import static org.junit.jupiter.api.Assertions.*; - -class UserCommandApiTest { - -} \ No newline at end of file