From 08dfa720b84e21999631e9b145c84dbfd228caaf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=B5=9C=EC=9D=80=EB=B9=84?= <59335077+hikarigin99@users.noreply.github.com> Date: Fri, 27 Jan 2023 17:42:06 +0900 Subject: [PATCH] =?UTF-8?q?[#63]=20=EC=A2=8B=EC=95=84=EC=9A=94=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1/=EC=B7=A8=EC=86=8C=20=EA=B8=B0=EB=8A=A5=20=EB=B0=8F?= =?UTF-8?q?=20=ED=95=B4=EB=8B=B9=20=EA=B2=8C=EC=8B=9C=EB=AC=BC=EC=97=90=20?= =?UTF-8?q?=EB=8C=80=ED=95=9C=20=EC=A2=8B=EC=95=84=EC=9A=94=20=EA=B0=9C?= =?UTF-8?q?=EC=88=98=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80=20(#87)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * add: 좋아요 도메인 - likes 테이블 생성 - userId와 postId FK * add: 좋아요 생성 및 취소 기능 * add: 게시물 좋아요 개수 증가/감소 기능 - JPQL UPDATE 사용을 위해 @Modifying * add: Like 관련 에러 메시지 및 테스트용 entity 생성 * add: 좋아요 생성 및 취소 API, LikeRequestDto * add: testUtils 수정으로 인한 Post 데이터 값 변경 * add: 좋아요 리포지토리와 서비스 테스트 - repository : 추가한 findByUserAndPost 메서드만 진행 - service : BDDMockito 테스트 * add: 좋아요 컨트롤러 테스트 및 Rest docs 생성 * add: build를 위한 test Lombok 제거 * add: 좋아요 생성 API 수정 및 LikeDto userId 변경 - 컨트롤러 API에 postId 추가 - @AuthenticationPrincipal 처리 --- .../domain/like/api/LikeController.java | 47 +++++++ .../prolog/domain/like/dto/LikeDto.java | 10 ++ .../prgrms/prolog/domain/like/model/Like.java | 44 ++++++ .../like/repository/LikeRepository.java | 15 ++ .../domain/like/service/LikeService.java | 9 ++ .../domain/like/service/LikeServiceImpl.java | 73 ++++++++++ .../prgrms/prolog/domain/post/model/Post.java | 10 +- .../post/repository/PostRepository.java | 9 ++ .../domain/post/service/PostService.java | 4 + .../domain/post/service/PostServiceImpl.java | 13 +- .../prolog/domain/user/dto/UserDto.java | 22 ++- .../db/migration/V2.2__add_like_table.sql | 12 ++ .../messages/exceptions/exception.properties | 7 +- .../domain/like/api/LikeControllerTest.java | 122 ++++++++++++++++ .../like/repository/LikeRepositoryTest.java | 63 +++++++++ .../domain/like/service/LikeServiceTest.java | 133 ++++++++++++++++++ .../domain/post/api/PostControllerTest.java | 19 +-- .../post/repository/PostRepositoryTest.java | 13 +- .../com/prgrms/prolog/utils/TestUtils.java | 37 +++-- src/test/resources/schema.sql | 46 +++--- 20 files changed, 650 insertions(+), 58 deletions(-) create mode 100644 src/main/java/com/prgrms/prolog/domain/like/api/LikeController.java create mode 100644 src/main/java/com/prgrms/prolog/domain/like/dto/LikeDto.java create mode 100644 src/main/java/com/prgrms/prolog/domain/like/model/Like.java create mode 100644 src/main/java/com/prgrms/prolog/domain/like/repository/LikeRepository.java create mode 100644 src/main/java/com/prgrms/prolog/domain/like/service/LikeService.java create mode 100644 src/main/java/com/prgrms/prolog/domain/like/service/LikeServiceImpl.java create mode 100644 src/main/resources/db/migration/V2.2__add_like_table.sql create mode 100644 src/test/java/com/prgrms/prolog/domain/like/api/LikeControllerTest.java create mode 100644 src/test/java/com/prgrms/prolog/domain/like/repository/LikeRepositoryTest.java create mode 100644 src/test/java/com/prgrms/prolog/domain/like/service/LikeServiceTest.java diff --git a/src/main/java/com/prgrms/prolog/domain/like/api/LikeController.java b/src/main/java/com/prgrms/prolog/domain/like/api/LikeController.java new file mode 100644 index 0000000..ee3e3ad --- /dev/null +++ b/src/main/java/com/prgrms/prolog/domain/like/api/LikeController.java @@ -0,0 +1,47 @@ +package com.prgrms.prolog.domain.like.api; + +import java.net.URI; + +import javax.validation.Valid; + +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.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.util.UriComponentsBuilder; + +import com.prgrms.prolog.domain.like.dto.LikeDto; +import com.prgrms.prolog.domain.like.dto.LikeDto.likeRequest; +import com.prgrms.prolog.domain.like.service.LikeServiceImpl; +import com.prgrms.prolog.global.jwt.JwtAuthentication; + +import lombok.RequiredArgsConstructor; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/v1/like") +public class LikeController { + + private final LikeServiceImpl likeService; + + @PostMapping(value = "/{postId}") + public ResponseEntity insert( + @PathVariable Long postId, + @AuthenticationPrincipal JwtAuthentication user + ) { + LikeDto.likeRequest request = new likeRequest(user.id(), postId); + Long likeId = likeService.save(request); + URI location = UriComponentsBuilder.fromUriString("/api/v1/like/" + likeId).build().toUri(); + return ResponseEntity.created(location).build(); + } + + @DeleteMapping + public ResponseEntity delete(@RequestBody @Valid likeRequest likeRequest) { + likeService.cancel(likeRequest); + return ResponseEntity.noContent().build(); + } +} diff --git a/src/main/java/com/prgrms/prolog/domain/like/dto/LikeDto.java b/src/main/java/com/prgrms/prolog/domain/like/dto/LikeDto.java new file mode 100644 index 0000000..86a0807 --- /dev/null +++ b/src/main/java/com/prgrms/prolog/domain/like/dto/LikeDto.java @@ -0,0 +1,10 @@ +package com.prgrms.prolog.domain.like.dto; + +import javax.validation.constraints.NotNull; + +public class LikeDto { + + public record likeRequest(@NotNull Long userId, + @NotNull Long postId) { + } +} diff --git a/src/main/java/com/prgrms/prolog/domain/like/model/Like.java b/src/main/java/com/prgrms/prolog/domain/like/model/Like.java new file mode 100644 index 0000000..0d3aaa8 --- /dev/null +++ b/src/main/java/com/prgrms/prolog/domain/like/model/Like.java @@ -0,0 +1,44 @@ +package com.prgrms.prolog.domain.like.model; + +import static javax.persistence.FetchType.*; +import static javax.persistence.GenerationType.*; +import static lombok.AccessLevel.*; + +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.Id; +import javax.persistence.JoinColumn; +import javax.persistence.ManyToOne; +import javax.persistence.Table; + +import com.prgrms.prolog.domain.post.model.Post; +import com.prgrms.prolog.domain.user.model.User; + +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Table(name = "likes") +@Entity +@Getter +@NoArgsConstructor(access = PROTECTED) +public class Like { + + @Id + @GeneratedValue(strategy = IDENTITY) + private Long id; + + @ManyToOne(fetch = LAZY) + @JoinColumn(name = "user_id") + private User user; + + @ManyToOne(fetch = LAZY) + @JoinColumn(name = "post_id") + private Post post; + + @Builder + public Like(User user, Post post) { + this.user = user; + this.post = post; + } +} diff --git a/src/main/java/com/prgrms/prolog/domain/like/repository/LikeRepository.java b/src/main/java/com/prgrms/prolog/domain/like/repository/LikeRepository.java new file mode 100644 index 0000000..9d4bfc2 --- /dev/null +++ b/src/main/java/com/prgrms/prolog/domain/like/repository/LikeRepository.java @@ -0,0 +1,15 @@ +package com.prgrms.prolog.domain.like.repository; + +import java.util.Optional; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import com.prgrms.prolog.domain.like.model.Like; +import com.prgrms.prolog.domain.post.model.Post; +import com.prgrms.prolog.domain.user.model.User; + +@Repository +public interface LikeRepository extends JpaRepository { + Optional findByUserAndPost(User user, Post post); +} diff --git a/src/main/java/com/prgrms/prolog/domain/like/service/LikeService.java b/src/main/java/com/prgrms/prolog/domain/like/service/LikeService.java new file mode 100644 index 0000000..5f6503a --- /dev/null +++ b/src/main/java/com/prgrms/prolog/domain/like/service/LikeService.java @@ -0,0 +1,9 @@ +package com.prgrms.prolog.domain.like.service; + +import com.prgrms.prolog.domain.like.dto.LikeDto; + +public interface LikeService { + Long save(LikeDto.likeRequest likeRequest); + + void cancel(LikeDto.likeRequest likeRequest); +} diff --git a/src/main/java/com/prgrms/prolog/domain/like/service/LikeServiceImpl.java b/src/main/java/com/prgrms/prolog/domain/like/service/LikeServiceImpl.java new file mode 100644 index 0000000..04e8244 --- /dev/null +++ b/src/main/java/com/prgrms/prolog/domain/like/service/LikeServiceImpl.java @@ -0,0 +1,73 @@ +package com.prgrms.prolog.domain.like.service; + +import javax.persistence.EntityNotFoundException; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import com.prgrms.prolog.domain.like.dto.LikeDto.likeRequest; +import com.prgrms.prolog.domain.like.model.Like; +import com.prgrms.prolog.domain.like.repository.LikeRepository; +import com.prgrms.prolog.domain.post.model.Post; +import com.prgrms.prolog.domain.post.repository.PostRepository; +import com.prgrms.prolog.domain.user.model.User; +import com.prgrms.prolog.domain.user.repository.UserRepository; + +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +@Transactional +@Service +public class LikeServiceImpl implements LikeService { + + private final LikeRepository likeRepository; + private final UserRepository userRepository; + private final PostRepository postRepository; + + @Override + public Long save(likeRequest likeRequest) { + + User user = getFindUserBy(likeRequest.userId()); + Post post = getFindPostBy(likeRequest.postId()); + + //TODO 이미 좋아요 되어있으면 에러 반환 -> 409 Conflict 오류로 변환 + if (likeRepository.findByUserAndPost(user, post).isPresent()) { + throw new EntityNotFoundException("exception.like.alreadyExist"); + } + + Like like = likeRepository.save(saveLike(user, post)); + + postRepository.addLikeCount(post.getId()); + return like.getId(); + } + + @Override + public void cancel(likeRequest likeRequest) { + + User user = getFindUserBy(likeRequest.userId()); + Post post = getFindPostBy(likeRequest.postId()); + + Like like = likeRepository.findByUserAndPost(user, post) + .orElseThrow(() -> new EntityNotFoundException("exception.like.notExist")); + + likeRepository.delete(like); + postRepository.subLikeCount(post.getId()); + } + + private Like saveLike(User user, Post post) { + return Like.builder() + .user(user) + .post(post) + .build(); + } + + private User getFindUserBy(Long userId) { + return userRepository.findById(userId) + .orElseThrow(() -> new IllegalArgumentException("exception.user.notExists")); + } + + private Post getFindPostBy(Long postId) { + return postRepository.findById(postId) + .orElseThrow(() -> new IllegalArgumentException("exception.post.notExists")); + } +} diff --git a/src/main/java/com/prgrms/prolog/domain/post/model/Post.java b/src/main/java/com/prgrms/prolog/domain/post/model/Post.java index 3baeb4a..7c617cf 100644 --- a/src/main/java/com/prgrms/prolog/domain/post/model/Post.java +++ b/src/main/java/com/prgrms/prolog/domain/post/model/Post.java @@ -9,6 +9,7 @@ import java.util.Objects; import java.util.Set; +import javax.persistence.Column; import javax.persistence.Entity; import javax.persistence.GeneratedValue; import javax.persistence.Id; @@ -17,6 +18,7 @@ import javax.persistence.ManyToOne; import javax.persistence.OneToMany; +import org.hibernate.annotations.ColumnDefault; import org.springframework.util.Assert; import com.prgrms.prolog.domain.comment.model.Comment; @@ -39,10 +41,6 @@ public class Post extends BaseEntity { private static final int TITLE_MAX_SIZE = 50; private static final int CONTENT_MAX_SIZE = 65535; - private static final String USER_INFO_NEED_MESSAGE = "게시글은 작성자 정보가 필요합니다."; - private static final String NOT_NULL_DATA_MESSAGE = "빈 값일 수 없는 데이터입니다."; - private static final String OVER_LENGTH_MESSAGE = "입력할 수 있는 범위를 초과하였습니다."; - @Id @GeneratedValue(strategy = IDENTITY) private Long id; @@ -68,6 +66,10 @@ public class Post extends BaseEntity { @JoinColumn(name = "series_id") private Series series; + @ColumnDefault("0") + @Column(name = "like_count") + private int likeCount; + @Builder public Post(String title, String content, boolean openStatus, User user, Series series) { this.title = validateTitle(title); diff --git a/src/main/java/com/prgrms/prolog/domain/post/repository/PostRepository.java b/src/main/java/com/prgrms/prolog/domain/post/repository/PostRepository.java index 0778c93..70c54fb 100644 --- a/src/main/java/com/prgrms/prolog/domain/post/repository/PostRepository.java +++ b/src/main/java/com/prgrms/prolog/domain/post/repository/PostRepository.java @@ -3,6 +3,7 @@ import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; @@ -27,4 +28,12 @@ public interface PostRepository extends JpaRepository { WHERE p.id = :postId """) Optional joinUserFindById(@Param(value = "postId") Long postId); + + @Modifying + @Query("UPDATE Post p SET p.likeCount = p.likeCount + 1 WHERE p.id = :postId") + int addLikeCount(@Param(value = "postId") Long postId); + + @Modifying + @Query("UPDATE Post p SET p.likeCount = p.likeCount - 1 WHERE p.id = :postId") + int subLikeCount(@Param(value = "postId") Long postId); } diff --git a/src/main/java/com/prgrms/prolog/domain/post/service/PostService.java b/src/main/java/com/prgrms/prolog/domain/post/service/PostService.java index c7f9ff9..33f36e7 100644 --- a/src/main/java/com/prgrms/prolog/domain/post/service/PostService.java +++ b/src/main/java/com/prgrms/prolog/domain/post/service/PostService.java @@ -8,8 +8,12 @@ public interface PostService { Long save(PostRequest.CreateRequest request, Long userId); + PostResponse findById(Long postId); + Page findAll(Pageable pageable); + PostResponse update(PostRequest.UpdateRequest update, Long userId, Long postId); + void delete(Long id); } diff --git a/src/main/java/com/prgrms/prolog/domain/post/service/PostServiceImpl.java b/src/main/java/com/prgrms/prolog/domain/post/service/PostServiceImpl.java index b7f8bb6..790a1f6 100644 --- a/src/main/java/com/prgrms/prolog/domain/post/service/PostServiceImpl.java +++ b/src/main/java/com/prgrms/prolog/domain/post/service/PostServiceImpl.java @@ -46,7 +46,6 @@ public class PostServiceImpl implements PostService { private final PostTagRepository postTagRepository; private final UserTagRepository userTagRepository; - @Override @Transactional public Long save(CreateRequest request, Long userId) { @@ -66,10 +65,10 @@ private void registerSeries(CreateRequest request, Post post, User owner) { Series series = seriesRepository .findByIdAndTitle(owner.getId(), seriesTitle) .orElseGet(() -> seriesRepository.save( - Series.builder() - .title(seriesTitle) - .user(owner) - .build() + Series.builder() + .title(seriesTitle) + .user(owner) + .build() ) ); post.setSeries(series); @@ -78,7 +77,7 @@ private void registerSeries(CreateRequest request, Post post, User owner) { @Override public PostResponse findById(Long postId) { Post post = postRepository.joinCommentFindById(postId) - .orElseThrow(() -> new IllegalArgumentException(POST_NOT_EXIST_MESSAGE)); + .orElseThrow(() -> new IllegalArgumentException("exception.post.notExists")); Set findPostTags = postTagRepository.joinRootTagFindByPostId(postId); post.addPostTagsFrom(findPostTags); return PostResponse.toPostResponse(post); @@ -94,7 +93,7 @@ public Page findAll(Pageable pageable) { @Transactional public PostResponse update(UpdateRequest update, Long userId, Long postId) { Post findPost = postRepository.joinUserFindById(postId) - .orElseThrow(() -> new IllegalArgumentException(POST_NOT_EXIST_MESSAGE)); + .orElseThrow(() -> new IllegalArgumentException("exception.post.notExists")); if (!findPost.getUser().checkSameUserId(userId)) { throw new IllegalArgumentException("exception.post.not.owner"); diff --git a/src/main/java/com/prgrms/prolog/domain/user/dto/UserDto.java b/src/main/java/com/prgrms/prolog/domain/user/dto/UserDto.java index 97641e1..09753bd 100644 --- a/src/main/java/com/prgrms/prolog/domain/user/dto/UserDto.java +++ b/src/main/java/com/prgrms/prolog/domain/user/dto/UserDto.java @@ -6,7 +6,6 @@ public class UserDto { - @Builder public record UserProfile( Long id, String email, @@ -15,6 +14,17 @@ public record UserProfile( String prologName, String profileImgUrl ) { + @Builder + public UserProfile(Long id, String email, String nickName, String introduce, String prologName, + String profileImgUrl) { + this.id = id; + this.email = email; + this.nickName = nickName; + this.introduce = introduce; + this.prologName = prologName; + this.profileImgUrl = profileImgUrl; + } + public static UserProfile toUserProfile(User user) { return new UserProfile( user.getId(), @@ -27,7 +37,6 @@ public static UserProfile toUserProfile(User user) { } } - @Builder public record UserInfo( String email, String nickName, @@ -35,7 +44,14 @@ public record UserInfo( String oauthId, String profileImgUrl ) { - + @Builder + public UserInfo(String email, String nickName, String provider, String oauthId, String profileImgUrl) { + this.email = email; + this.nickName = nickName; + this.provider = provider; + this.oauthId = oauthId; + this.profileImgUrl = profileImgUrl; + } } public record IdResponse(Long id) { diff --git a/src/main/resources/db/migration/V2.2__add_like_table.sql b/src/main/resources/db/migration/V2.2__add_like_table.sql new file mode 100644 index 0000000..dd768c7 --- /dev/null +++ b/src/main/resources/db/migration/V2.2__add_like_table.sql @@ -0,0 +1,12 @@ +DROP TABLE IF EXISTS likes; + +CREATE TABLE likes +( + id bigint NOT NULL PRIMARY KEY AUTO_INCREMENT, + user_id bigint NOT NULL, + post_id bigint NOT NULL, + FOREIGN KEY fk_likes_user_id (user_id) REFERENCES users (id), + FOREIGN KEY fk_likes_post_id (post_id) REFERENCES post (id) +); + +ALTER TABLE post ADD like_count INT DEFAULT 0; \ No newline at end of file diff --git a/src/main/resources/messages/exceptions/exception.properties b/src/main/resources/messages/exceptions/exception.properties index a35f3cf..4090df9 100644 --- a/src/main/resources/messages/exceptions/exception.properties +++ b/src/main/resources/messages/exceptions/exception.properties @@ -2,20 +2,17 @@ exception.user.notExists=유저가 존재하지 않습니다. exception.user.email.notSame=해당 유저의 이메일과 일치하지 않습니다. exception.user.require=해당하는 유저가 필요합니다. - ## POST ## exception.post.notExists=존재하지 않는 포스트입니다. exception.post.content.overLength=게시글 내용의 최대 글자 수를 초과하였습나다. exception.post.require=해당하는 게시글이 필요합니다. exception.post.text=데이터는 빈 값일 수 없습니다. exception.post.text.overLength=입력된 문자열이 최대 범위를 초과하였습니다. - ## COMMENT ## exception.comment.content.overLength=댓글 내용의 최대 글자 수를 초과하였습나다. exception.comment.content.empty=댓글 내용은 빈 값일 수 없습니다. exception.comment.notExists=존재하지 않는 댓글입니다. exception.comment.user.require=게시글은 작성자 정보가 필요합니다. - ## VALIDATION ## javax.validation.constraints.AssertFalse.message={}는 false 이어야만 합니다. javax.validation.constraints.AssertTrue.message={}는 true 이어야만 합니다. @@ -32,8 +29,10 @@ javax.validation.constraints.Pattern.message={regexp} 정규 표현식에 일치 javax.validation.constraints.Positive.message=0 보다 커야합니다. javax.validation.constraints.PositiveOrZero.message=0 보다 크거나 같아야 합니다. javax.validation.constraints.Size.message={min} 과 {max} 사이의 값이어야 합니다. - ## SECURITY ## exception.jwtAuthentication.token.notExists=토큰이 존재하지 않습니다. exception.jwtAuthentication.user.email.notExists=유저 이메일이 존재하지 않습니다. exception.jwtAuthenticationToken.isAuthenticated=인증 정보를 확인할 수 없는 메서드 주입은 지원하지 않습니다. 생성자를 통해 생성해야 합니다. +## LIKE ## +exception.like.notExist=좋아요를 누르지않아 취소할 수 없습니다. +exception.like.alreadyExist=이미 좋아요를 누른 게시물에는 좋아요를 할 수 없습니다. \ No newline at end of file diff --git a/src/test/java/com/prgrms/prolog/domain/like/api/LikeControllerTest.java b/src/test/java/com/prgrms/prolog/domain/like/api/LikeControllerTest.java new file mode 100644 index 0000000..0660115 --- /dev/null +++ b/src/test/java/com/prgrms/prolog/domain/like/api/LikeControllerTest.java @@ -0,0 +1,122 @@ +package com.prgrms.prolog.domain.like.api; + +import static com.prgrms.prolog.utils.TestUtils.*; +import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.*; +import static org.springframework.restdocs.payload.PayloadDocumentation.*; +import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.annotation.Import; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.restdocs.RestDocumentationContextProvider; +import org.springframework.restdocs.RestDocumentationExtension; +import org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders; +import org.springframework.restdocs.mockmvc.RestDocumentationResultHandler; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.context.WebApplicationContext; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.prgrms.prolog.config.RestDocsConfig; +import com.prgrms.prolog.domain.like.dto.LikeDto; +import com.prgrms.prolog.domain.like.service.LikeService; +import com.prgrms.prolog.domain.post.model.Post; +import com.prgrms.prolog.domain.post.repository.PostRepository; +import com.prgrms.prolog.domain.user.model.User; +import com.prgrms.prolog.domain.user.repository.UserRepository; +import com.prgrms.prolog.global.jwt.JwtTokenProvider; +import com.prgrms.prolog.global.jwt.JwtTokenProvider.Claims; + +@SpringBootTest +@ExtendWith(RestDocumentationExtension.class) +@Import(RestDocsConfig.class) +@Transactional +class LikeControllerTest { + + @Autowired + RestDocumentationResultHandler restDocs; + + @Autowired + LikeService likeService; + + @Autowired + ObjectMapper objectMapper; + + @Autowired + private JwtTokenProvider jwtTokenProvider; + + @Autowired + UserRepository userRepository; + + @Autowired + PostRepository postRepository; + + MockMvc mockMvc; + + @BeforeEach + void setUpRestDocs(WebApplicationContext webApplicationContext, + RestDocumentationContextProvider restDocumentation) { + this.mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext) + .apply(documentationConfiguration(restDocumentation)) + .alwaysDo(restDocs) + .apply(springSecurity()) + .build(); + } + + @Test + void likeSaveApiTest() throws Exception { + User savedUser = userRepository.save(USER); + Post post = getPost(); + post.setUser(savedUser); + Post savedPost = postRepository.save(post); + Claims claims = Claims.from(savedUser.getId(), USER_ROLE); + + LikeDto.likeRequest likeRequest = new LikeDto.likeRequest(savedUser.getId(), savedPost.getId()); + + mockMvc.perform(RestDocumentationRequestBuilders.post("/api/v1/like/{postId}", savedPost.getId()) + .header(HttpHeaders.AUTHORIZATION, BEARER_TYPE + jwtTokenProvider.createAccessToken(claims)) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(likeRequest))) + .andExpect(status().isCreated()) + .andDo(restDocs.document( + requestFields( + fieldWithPath("userId").description("사용자 아이디"), + fieldWithPath("postId").description("게시물 아이디") + ), + responseBody() + )); + } + + @Test + void likeCancelApiTest() throws Exception { + User savedUser = userRepository.save(USER); + Post post = getPost(); + post.setUser(savedUser); + Post savedPost = postRepository.save(post); + + Claims claims = Claims.from(savedUser.getId(), USER_ROLE); + + LikeDto.likeRequest likeRequest = new LikeDto.likeRequest(savedUser.getId(), savedPost.getId()); + likeService.save(likeRequest); + + mockMvc.perform(RestDocumentationRequestBuilders.delete("/api/v1/like") + .header(HttpHeaders.AUTHORIZATION, BEARER_TYPE + jwtTokenProvider.createAccessToken(claims)) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(likeRequest))) + .andExpect(status().isNoContent()) + .andDo(restDocs.document( + requestFields( + fieldWithPath("userId").description("사용자 아이디"), + fieldWithPath("postId").description("게시물 아이디") + ), + responseBody() + )); + } +} \ No newline at end of file diff --git a/src/test/java/com/prgrms/prolog/domain/like/repository/LikeRepositoryTest.java b/src/test/java/com/prgrms/prolog/domain/like/repository/LikeRepositoryTest.java new file mode 100644 index 0000000..bf6e2b3 --- /dev/null +++ b/src/test/java/com/prgrms/prolog/domain/like/repository/LikeRepositoryTest.java @@ -0,0 +1,63 @@ +package com.prgrms.prolog.domain.like.repository; + +import static com.prgrms.prolog.utils.TestUtils.*; +import static org.assertj.core.api.AssertionsForClassTypes.*; +import static org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase.Replace.*; + +import java.util.Optional; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.context.annotation.Import; + +import com.prgrms.prolog.domain.like.model.Like; +import com.prgrms.prolog.domain.post.model.Post; +import com.prgrms.prolog.domain.post.repository.PostRepository; +import com.prgrms.prolog.domain.user.model.User; +import com.prgrms.prolog.domain.user.repository.UserRepository; +import com.prgrms.prolog.global.config.JpaConfig; + +@DataJpaTest +@AutoConfigureTestDatabase(replace = NONE) +@Import({JpaConfig.class}) +class LikeRepositoryTest { + + @Autowired + LikeRepository likeRepository; + + @Autowired + PostRepository postRepository; + + @Autowired + UserRepository userRepository; + + @Test + @DisplayName("존재하는 사용자가 존재하는 게시물을 좋아요를 할 때 좋아요가 생긴다.") + void findByUserAndPostTest() { + // given + User savedUser = userRepository.save(USER); + Post post = Post.builder() + .title(TITLE) + .content(CONTENT) + .openStatus(true) + .user(savedUser) + .build(); + Post savedPost = postRepository.save(post); + + Like like = Like.builder() + .user(savedUser) + .post(savedPost) + .build(); + Like savedLike = likeRepository.save(like); + + // when + Optional actual = likeRepository.findByUserAndPost(savedUser, savedPost); + + // then + assertThat(actual) + .hasValueSatisfying(l -> assertThat(l.getId()).isEqualTo(savedLike.getId())); + } +} \ No newline at end of file diff --git a/src/test/java/com/prgrms/prolog/domain/like/service/LikeServiceTest.java b/src/test/java/com/prgrms/prolog/domain/like/service/LikeServiceTest.java new file mode 100644 index 0000000..d070300 --- /dev/null +++ b/src/test/java/com/prgrms/prolog/domain/like/service/LikeServiceTest.java @@ -0,0 +1,133 @@ +package com.prgrms.prolog.domain.like.service; + +import static com.prgrms.prolog.utils.TestUtils.*; +import static org.assertj.core.api.Assertions.*; +import static org.mockito.BDDMockito.*; + +import java.util.Optional; + +import javax.persistence.EntityNotFoundException; + +import org.junit.jupiter.api.DisplayName; +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 com.prgrms.prolog.domain.like.dto.LikeDto.likeRequest; +import com.prgrms.prolog.domain.like.model.Like; +import com.prgrms.prolog.domain.like.repository.LikeRepository; +import com.prgrms.prolog.domain.post.repository.PostRepository; +import com.prgrms.prolog.domain.user.repository.UserRepository; + +@ExtendWith(MockitoExtension.class) +class LikeServiceTest { + + @InjectMocks + private LikeServiceImpl likeService; + + @Mock + private LikeRepository likeRepository; + + @Mock + private PostRepository postRepository; + + @Mock + private UserRepository userRepository; + + @Mock + private Like like; + + likeRequest likeRequest = new likeRequest(USER_ID, POST_ID); + + @Test + @DisplayName("게시물에 좋아요를 누를 수 있다.") + void insertLikeTest() { + // given + given(userRepository.findById(USER_ID)).willReturn(Optional.of(USER)); + given(postRepository.findById(POST_ID)).willReturn(Optional.of(POST)); + given(likeRepository.findByUserAndPost(USER, POST)).willReturn(Optional.empty()); + given(likeRepository.save(any(Like.class))).willReturn(like); + given(like.getId()).willReturn(1L); + + // when + Long likeId = likeService.save(likeRequest); + + // then + then(likeRepository).should().save(any(Like.class)); // 행위 검증 + assertThat(likeId).isEqualTo(1L); // 상태 검증 + } + + @Test + @DisplayName("좋아요한 게시물을 좋아요 취소할 수 있다.") + void cancelLikeTest() { + // given + given(userRepository.findById(USER_ID)).willReturn(Optional.of(USER)); + given(postRepository.findById(POST_ID)).willReturn(Optional.of(POST)); + given(likeRepository.findByUserAndPost(USER, POST)).willReturn(Optional.of(like)); + + // when + likeService.cancel(likeRequest); + + // then + then(likeRepository).should().delete(any(Like.class)); + } + + @Test + @DisplayName("좋아요한 게시물에 또 좋아요를 할 수 없다.") + void insertDuplicateLikeTest() { + given(userRepository.findById(USER_ID)).willReturn(Optional.of(USER)); + given(postRepository.findById(POST_ID)).willReturn(Optional.of(POST)); + given(likeRepository.findByUserAndPost(USER, POST)).willReturn(Optional.of(LIKE)); + + assertThatThrownBy(() -> likeService.save(likeRequest)).isInstanceOf(EntityNotFoundException.class); + } + + @Test + @DisplayName("좋아요를 하지 않은 게시물에는 좋아요를 취소할 수 없다.") + void cancelDuplicateLikeTest() { + given(userRepository.findById(USER_ID)).willReturn(Optional.of(USER)); + given(postRepository.findById(POST_ID)).willReturn(Optional.of(POST)); + given(likeRepository.findByUserAndPost(USER, POST)).willThrow(EntityNotFoundException.class); + + assertThatThrownBy(() -> likeService.save(likeRequest)).isInstanceOf(EntityNotFoundException.class); + } + + @Test + @DisplayName("좋아요를 누르면 게시물의 총 좋아요의 개수가 1씩 증가한다.") + void addLikeCountTest() { + // given + given(userRepository.findById(USER_ID)).willReturn(Optional.of(USER)); + given(postRepository.findById(POST_ID)).willReturn(Optional.of(POST)); + given(likeRepository.findByUserAndPost(USER, POST)).willReturn(Optional.empty()); + given(likeRepository.save(any(Like.class))).willReturn(like); + given(postRepository.addLikeCount(any())).willReturn(1); + given(like.getId()).willReturn(1L); + + // when + likeService.save(likeRequest); + + // then + then(postRepository).should().addLikeCount(any()); // 행위 검증 + assertThat(postRepository.addLikeCount(POST_ID)).isEqualTo(1); + } + + @Test + @DisplayName("좋아요를 게시물의 총 좋아요의 개수가 1씩 증가한다.") + void cancelLikeCountTest() { + // given + given(userRepository.findById(USER_ID)).willReturn(Optional.of(USER)); + given(postRepository.findById(POST_ID)).willReturn(Optional.of(POST)); + given(likeRepository.findByUserAndPost(USER, POST)).willReturn(Optional.of(LIKE)); + willDoNothing().given(likeRepository).delete(any(Like.class)); + given(postRepository.subLikeCount(any())).willReturn(1); + + // when + likeService.cancel(likeRequest); + + // then + then(postRepository).should().subLikeCount(any()); + assertThat(postRepository.subLikeCount(POST_ID)).isEqualTo(1); + } +} \ No newline at end of file diff --git a/src/test/java/com/prgrms/prolog/domain/post/api/PostControllerTest.java b/src/test/java/com/prgrms/prolog/domain/post/api/PostControllerTest.java index 8bf7cba..964def7 100644 --- a/src/test/java/com/prgrms/prolog/domain/post/api/PostControllerTest.java +++ b/src/test/java/com/prgrms/prolog/domain/post/api/PostControllerTest.java @@ -31,7 +31,7 @@ import com.prgrms.prolog.config.TestContainerConfig; import com.prgrms.prolog.domain.post.dto.PostRequest.CreateRequest; import com.prgrms.prolog.domain.post.dto.PostRequest.UpdateRequest; -import com.prgrms.prolog.domain.post.service.PostServiceImpl; +import com.prgrms.prolog.domain.post.service.PostService; import com.prgrms.prolog.domain.series.repository.SeriesRepository; import com.prgrms.prolog.domain.user.repository.UserRepository; import com.prgrms.prolog.global.jwt.JwtTokenProvider; @@ -50,7 +50,7 @@ class PostControllerTest { @Autowired private ObjectMapper objectMapper; @Autowired - private PostServiceImpl postService; + private PostService postService; @Autowired private UserRepository userRepository; @Autowired @@ -64,9 +64,10 @@ class PostControllerTest { @BeforeEach void setUp(WebApplicationContext webApplicationContext, RestDocumentationContextProvider restDocumentation) { + userId = userRepository.save(USER).getId(); claims = Claims.from(userId, "ROLE_USER"); - CreateRequest createRequest = new CreateRequest("테스트 제목", "테스트 내용", "#tag", false, SERIES_TITLE); + CreateRequest createRequest = new CreateRequest("테스트 제목", "테스트 내용", "#tag", true, SERIES_TITLE); postId = postService.save(createRequest, userId); this.mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext) .apply(documentationConfiguration(restDocumentation)) @@ -163,12 +164,13 @@ void findById() throws Exception { @Test @DisplayName("게시물 아이디로 게시물을 수정할 수 있다.") void update() throws Exception { - UpdateRequest request = new UpdateRequest("수정된 테스트 제목", "수정된 테스트 내용", "", true); + UpdateRequest update = new UpdateRequest(UPDATE_TITLE, UPDATE_CONTENT, "", false); + postService.update(update, userId, postId); mockMvc.perform(RestDocumentationRequestBuilders.patch("/api/v1/posts/{id}", postId) .header(HttpHeaders.AUTHORIZATION, BEARER_TYPE + jwtTokenProvider.createAccessToken(claims)) .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(request)) + .content(objectMapper.writeValueAsString(update)) ).andExpect(status().isOk()) .andDo(restDocs.document( requestFields( @@ -212,7 +214,7 @@ void remove() throws Exception { @Test @DisplayName("게시물 작성 중 제목이 공백인 경우 에러가 발생해야한다.") - void isValidateTitleNull() throws Exception { + void validateTitleNull() throws Exception { CreateRequest createRequest = new CreateRequest("", "테스트 게시물 내용", "#tag", true, SERIES_TITLE); String requestJsonString = objectMapper.writeValueAsString(createRequest); @@ -226,9 +228,8 @@ void isValidateTitleNull() throws Exception { @Test @DisplayName("게시물 작성 중 내용이 빈칸인 경우 에러가 발생해야한다.") - void isValidateContentEmpty() throws Exception { + void validateContentEmpty() throws Exception { CreateRequest createRequest = new CreateRequest("테스트 게시물 제목", " ", "#tag", true, SERIES_TITLE); - String requestJsonString = objectMapper.writeValueAsString(createRequest); mockMvc.perform(RestDocumentationRequestBuilders.post("/api/v1/posts") @@ -240,7 +241,7 @@ void isValidateContentEmpty() throws Exception { @Test @DisplayName("게시물 작성 중 게시물 제목이 50이상인 경우 에러가 발생해야한다.") - void isValidateTitleSizeOver() throws Exception { + void validateTitleSizeOver() throws Exception { CreateRequest createRequest = new CreateRequest( "안녕하세요. 여기는 프로그래머스 기술 블로그 prolog입니다. 이곳에 글을 작성하기 위해서는 제목은 50글자 미만이어야합니다.", "null 게시물 내용", "#tag", diff --git a/src/test/java/com/prgrms/prolog/domain/post/repository/PostRepositoryTest.java b/src/test/java/com/prgrms/prolog/domain/post/repository/PostRepositoryTest.java index 2a97cc3..3e308c8 100644 --- a/src/test/java/com/prgrms/prolog/domain/post/repository/PostRepositoryTest.java +++ b/src/test/java/com/prgrms/prolog/domain/post/repository/PostRepositoryTest.java @@ -38,22 +38,21 @@ class PostRepositoryTest { @BeforeEach void setUp() { user = userRepository.save(USER); - post = Post.builder() - .title("테스트 제목") - .content("테스트 내용") + Post p = Post.builder() + .title(TITLE) + .content(CONTENT) .openStatus(true) .user(user) .build(); - - postRepository.save(post); + post = postRepository.save(p); } @Test @DisplayName("게시물을 등록할 수 있다.") void save() { Post newPost = Post.builder() - .title("새로운 테스트 제목") - .content("새로운 테스트 내용") + .title("새로 저장한 제목") + .content("새로 저장한 내용") .openStatus(false) .user(user) .build(); diff --git a/src/test/java/com/prgrms/prolog/utils/TestUtils.java b/src/test/java/com/prgrms/prolog/utils/TestUtils.java index 8f1f27d..c4ce30e 100644 --- a/src/test/java/com/prgrms/prolog/utils/TestUtils.java +++ b/src/test/java/com/prgrms/prolog/utils/TestUtils.java @@ -1,6 +1,7 @@ package com.prgrms.prolog.utils; import com.prgrms.prolog.domain.comment.model.Comment; +import com.prgrms.prolog.domain.like.model.Like; import com.prgrms.prolog.domain.post.model.Post; import com.prgrms.prolog.domain.posttag.model.PostTag; import com.prgrms.prolog.domain.roottag.model.RootTag; @@ -13,6 +14,7 @@ public class TestUtils { // User Data public static final Long USER_ID = 1L; + public static final User USER = getUser(); public static final Long UNSAVED_USER_ID = 0L; public static final String USER_EMAIL = "dev@programmers.com"; public static final String USER_NICK_NAME = "머쓱이"; @@ -21,34 +23,47 @@ public class TestUtils { public static final String PROVIDER = "kakao"; public static final String OAUTH_ID = "kakao@123456789"; public static final String USER_PROFILE_IMG_URL = "http://kakao/defaultImg.jpg"; - public static final User USER = getUser(); - public static final Post POST = getPost(); public static final UserInfo USER_INFO = getUserInfo(); public static final UserProfile USER_PROFILE = getUserProfile(); - public static final Comment COMMENT = getComment(); - public static final Series SERIES = getSeries(); - // Post & Comment Data + public static final String USER_ROLE = "ROLE_USER"; + + // Post + public static final Long POST_ID = 1L; + public static final Post POST = getPost(); public static final String TITLE = "제목을 입력해주세요"; public static final String CONTENT = "내용을 입력해주세요"; - public static final String COMMENT_CONTENT = "댓글 내용"; public static final String POST_TITLE = "게시글 제목"; public static final String POST_CONTENT = "게시글 내용"; + public static final String UPDATE_TITLE = "수정할 제목을 입력해주세요"; + public static final String UPDATE_CONTENT = "수정할 내용을 입력해주세요"; + + // Comment + public static final Comment COMMENT = getComment(); + public static final String COMMENT_CONTENT = "댓글 내용"; + + // Series + public static final Series SERIES = getSeries(); public static final String SERIES_TITLE = "시리즈 제목"; - public static final String USER_ROLE = "ROLE_USER"; + + // Like + public static final Long LIKE_ID = 1L; + public static final Like LIKE = getLike(); + // RootTag & PostTag Data public static final String ROOT_TAG_NAME = "머쓱 태그"; public static final Integer POST_TAG_COUNT = 0; public static final RootTag ROOT_TAG = getRootTag(); public static final PostTag POST_TAG = getPostTag(); + // Over Size String Dummy public static final String OVER_SIZE_50 = "0" + "1234567890".repeat(5); public static final String OVER_SIZE_100 = "0" + "1234567890".repeat(10); public static final String OVER_SIZE_255 = "012345" + "1234567890".repeat(25); public static final String OVER_SIZE_65535 = "012345" + "1234567890".repeat(6553); + // Authentication public static final String BEARER_TYPE = "Bearer "; - private TestUtils() { /* no-op */ } @@ -124,4 +139,10 @@ public static Series getSeries() { .build(); } + public static Like getLike() { + return Like.builder() + .user(USER) + .post(POST) + .build(); + } } diff --git a/src/test/resources/schema.sql b/src/test/resources/schema.sql index 62f6f7e..05c7014 100644 --- a/src/test/resources/schema.sql +++ b/src/test/resources/schema.sql @@ -1,29 +1,31 @@ --- V1__init.sql +-- init.sql # create database if not exists prolog; # use prolog; +DROP TABLE IF EXISTS likes; +DROP TABLE IF EXISTS user_tag; +DROP TABLE IF EXISTS post_tag; +DROP TABLE IF EXISTS root_tag; DROP TABLE IF EXISTS social_account; DROP TABLE IF EXISTS comment; DROP TABLE IF EXISTS post; DROP TABLE IF EXISTS series; DROP TABLE IF EXISTS users; -DROP TABLE IF EXISTS user_tag; -DROP TABLE IF EXISTS post_tag; -DROP TABLE IF EXISTS root_tag; + CREATE TABLE users ( - id bigint NOT NULL PRIMARY KEY AUTO_INCREMENT, - email varchar(100) NOT NULL UNIQUE, + id bigint NOT NULL PRIMARY KEY AUTO_INCREMENT, + email varchar(100) NOT NULL UNIQUE, profile_img_url varchar(255) NULL, - nick_name varchar(100) NULL UNIQUE, - introduce varchar(100) NULL, - prolog_name varchar(100) NOT NULL UNIQUE, - provider varchar(100) NOT NULL, - oauth_id varchar(100) NOT NULL, - created_by varchar(100) NULL, - created_at datetime NOT NULL DEFAULT now(), - updated_at datetime NOT NULL DEFAULT now(), - deleted_at datetime + nick_name varchar(100) NULL UNIQUE, + introduce varchar(100) NULL, + prolog_name varchar(100) NOT NULL UNIQUE, + provider varchar(100) NOT NULL, + oauth_id varchar(100) NOT NULL, + created_by varchar(100) NULL, + created_at datetime NOT NULL DEFAULT now(), + updated_at datetime NOT NULL DEFAULT now(), + deleted_at datetime ); CREATE TABLE series @@ -106,4 +108,16 @@ CREATE TABLE user_tag root_tag_id bigint NOT NULL, FOREIGN KEY fk_user_tag_user_id (user_id) REFERENCES users (id), FOREIGN KEY fk_user_tag_root_tag_id (root_tag_id) REFERENCES root_tag (id) -) +); + +CREATE TABLE likes +( + id bigint NOT NULL PRIMARY KEY AUTO_INCREMENT, + user_id bigint NOT NULL, + post_id bigint NOT NULL, + FOREIGN KEY fk_likes_user_id (user_id) REFERENCES users (id), + FOREIGN KEY fk_likes_post_id (post_id) REFERENCES post (id) +); + +ALTER TABLE post + ADD like_count INT DEFAULT 0; \ No newline at end of file