diff --git a/src/docs/asciidoc/comments.adoc b/src/docs/asciidoc/comments.adoc index 2321a088..1491db53 100644 --- a/src/docs/asciidoc/comments.adoc +++ b/src/docs/asciidoc/comments.adoc @@ -2,12 +2,12 @@ == 댓글 API [[댓글-생성]] -=== 댓글 생성 +=== `POST` 댓글 생성 operation::comment-controller-test/create-comment[snippets='http-request,curl-request,path-parameters,request-headers,request-fields,http-response'] [[댓글-조회]] -=== 댓글 조회 +=== `GET` 댓글 조회 operation::comment-controller-test/find-comments[snippets='http-request,curl-request,path-parameters,http-response,response-fields'] diff --git a/src/docs/asciidoc/posts.adoc b/src/docs/asciidoc/posts.adoc index 6206b90b..fc4dea9f 100644 --- a/src/docs/asciidoc/posts.adoc +++ b/src/docs/asciidoc/posts.adoc @@ -27,7 +27,14 @@ operation::post-controller-test/find-my-post[snippets='http-request,curl-request operation::post-controller-test/find-voted-post[snippets='http-request,curl-request,query-parameters,request-headers,http-response,response-fields'] +[[게시글-투표-마감]] +=== `POST` 게시글 투표 마감 + +operation::post-controller-test/close-post[snippets='http-request,curl-request,path-parameters,request-headers,http-response'] + +[[게시글-수정]] + [[게시글-삭제]] -=== 게시글 삭제 (미구현) +=== `DELETE` 게시글 삭제 operation::post-controller-test/delete-post[snippets='http-request,curl-request,path-parameters,request-headers,http-response'] diff --git a/src/docs/asciidoc/users.adoc b/src/docs/asciidoc/users.adoc index a342582e..9faf6982 100644 --- a/src/docs/asciidoc/users.adoc +++ b/src/docs/asciidoc/users.adoc @@ -2,6 +2,6 @@ == 유저 API [[유저-정보-조회]] -=== 유저 정보 조회 (미구현) +=== `GET` 유저 정보 조회 operation::user-controller-test/find-user-info[snippets='http-request,curl-request,path-parameters,http-response,response-fields'] \ No newline at end of file diff --git a/src/docs/asciidoc/votes.adoc b/src/docs/asciidoc/votes.adoc index e89d4323..98b1cdc6 100644 --- a/src/docs/asciidoc/votes.adoc +++ b/src/docs/asciidoc/votes.adoc @@ -7,7 +7,7 @@ operation::vote-controller-test/vote[snippets='http-request,curl-request,request-headers,request-fields,http-response'] [[게스트-투표]] -=== `POST` 게스트 투표 (미구현) +=== `POST` 게스트 투표 operation::vote-controller-test/guest-vote[snippets='http-request,curl-request,request-headers,request-fields,http-response'] diff --git a/src/main/java/com/swyp8team2/comment/presentation/dto/CommentResponse.java b/src/main/java/com/swyp8team2/comment/presentation/dto/CommentResponse.java index 702158f2..30af16ab 100644 --- a/src/main/java/com/swyp8team2/comment/presentation/dto/CommentResponse.java +++ b/src/main/java/com/swyp8team2/comment/presentation/dto/CommentResponse.java @@ -11,7 +11,7 @@ public record CommentResponse( Long commentId, String content, AuthorDto author, - Long voteId, + Long imageId, LocalDateTime createdAt ) implements CursorDto { diff --git a/src/main/java/com/swyp8team2/common/exception/ApplicationControllerAdvice.java b/src/main/java/com/swyp8team2/common/exception/ApplicationControllerAdvice.java index 9abbe93f..2796149d 100644 --- a/src/main/java/com/swyp8team2/common/exception/ApplicationControllerAdvice.java +++ b/src/main/java/com/swyp8team2/common/exception/ApplicationControllerAdvice.java @@ -5,9 +5,11 @@ import org.springframework.http.ResponseEntity; import org.springframework.web.HttpRequestMethodNotSupportedException; import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.MissingRequestHeaderException; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; import org.springframework.web.method.annotation.HandlerMethodValidationException; +import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException; import org.springframework.web.servlet.resource.NoResourceFoundException; import javax.naming.AuthenticationException; @@ -68,6 +70,20 @@ public ResponseEntity handle(AccessDeniedException e) { return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(new ErrorResponse(ErrorCode.INVALID_TOKEN)); } + @ExceptionHandler(MissingRequestHeaderException.class) + public ResponseEntity handle(MissingRequestHeaderException e) { + log.debug("MissingRequestHeaderException {}", e.getMessage()); + return ResponseEntity.badRequest() + .body(new ErrorResponse(ErrorCode.INVALID_ARGUMENT)); + } + + @ExceptionHandler(MethodArgumentTypeMismatchException.class) + public ResponseEntity handle(MethodArgumentTypeMismatchException e) { + log.debug("MethodArgumentTypeMismatchException {}", e.getMessage()); + return ResponseEntity.badRequest() + .body(new ErrorResponse(ErrorCode.INVALID_ARGUMENT)); + } + @ExceptionHandler(Exception.class) public ResponseEntity handle(Exception e) { log.error("Exception", e); diff --git a/src/main/java/com/swyp8team2/common/exception/ErrorCode.java b/src/main/java/com/swyp8team2/common/exception/ErrorCode.java index 0cbbdf1a..3a1f52d5 100644 --- a/src/main/java/com/swyp8team2/common/exception/ErrorCode.java +++ b/src/main/java/com/swyp8team2/common/exception/ErrorCode.java @@ -18,6 +18,8 @@ public enum ErrorCode { POST_NOT_FOUND("존재하지 않는 게시글"), DESCRIPTION_LENGTH_EXCEEDED("게시글 설명 길이 초과"), INVALID_POST_IMAGE_COUNT("게시글 이미지 개수 오류"), + NOT_POST_AUTHOR("게시글 작성자가 아님"), + POST_ALREADY_CLOSED("이미 마감된 게시글"), //401 EXPIRED_TOKEN("토큰 만료"), diff --git a/src/main/java/com/swyp8team2/post/application/PostService.java b/src/main/java/com/swyp8team2/post/application/PostService.java index ae02e2e9..7eea9767 100644 --- a/src/main/java/com/swyp8team2/post/application/PostService.java +++ b/src/main/java/com/swyp8team2/post/application/PostService.java @@ -119,7 +119,7 @@ public List findPostStatus(Long postId) { int totalVoteCount = getTotalVoteCount(post.getImages()); return post.getImages().stream() .map(image -> { - String ratio = ratioCalculator.calculate(image.getVoteCount(), totalVoteCount); + String ratio = ratioCalculator.calculate(totalVoteCount, image.getVoteCount()); return new PostImageVoteStatusResponse(image.getId(), image.getName(), image.getVoteCount(), ratio); }).toList(); } @@ -131,4 +131,19 @@ private int getTotalVoteCount(List images) { } return totalVoteCount; } + + @Transactional + public void delete(Long userId, Long postId) { + Post post = postRepository.findById(postId) + .orElseThrow(() -> new BadRequestException(ErrorCode.POST_NOT_FOUND)); + post.validateOwner(userId); + postRepository.delete(post); + } + + @Transactional + public void close(Long userId, Long postId) { + Post post = postRepository.findById(postId) + .orElseThrow(() -> new BadRequestException(ErrorCode.POST_NOT_FOUND)); + post.close(userId); + } } diff --git a/src/main/java/com/swyp8team2/post/domain/Post.java b/src/main/java/com/swyp8team2/post/domain/Post.java index b036b0e8..96ad72dd 100644 --- a/src/main/java/com/swyp8team2/post/domain/Post.java +++ b/src/main/java/com/swyp8team2/post/domain/Post.java @@ -94,4 +94,24 @@ public void cancelVote(Long imageId) { .orElseThrow(() -> new InternalServerException(ErrorCode.POST_IMAGE_NOT_FOUND)); image.decreaseVoteCount(); } + + public void close(Long userId) { + validateOwner(userId); + if (state == State.CLOSED) { + throw new BadRequestException(ErrorCode.POST_ALREADY_CLOSED); + } + this.state = State.CLOSED; + } + + public void validateOwner(Long userId) { + if (!this.userId.equals(userId)) { + throw new BadRequestException(ErrorCode.NOT_POST_AUTHOR); + } + } + + public void validateProgress() { + if (!this.state.equals(State.PROGRESS)) { + throw new BadRequestException(ErrorCode.POST_ALREADY_CLOSED); + } + } } diff --git a/src/main/java/com/swyp8team2/post/presentation/PostController.java b/src/main/java/com/swyp8team2/post/presentation/PostController.java index 1d67011e..52ff749d 100644 --- a/src/main/java/com/swyp8team2/post/presentation/PostController.java +++ b/src/main/java/com/swyp8team2/post/presentation/PostController.java @@ -16,6 +16,7 @@ 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.PatchMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; @@ -80,11 +81,21 @@ public ResponseEntity findPost(@PathVariable("shareUrl") String sh )); } - @DeleteMapping("/{shareUrl}") + @PostMapping("/{postId}/close") + public ResponseEntity closePost( + @PathVariable("postId") Long postId, + @AuthenticationPrincipal UserInfo userInfo + ) { + postService.close(userInfo.userId(), postId); + return ResponseEntity.ok().build(); + } + + @DeleteMapping("/{postId}") public ResponseEntity deletePost( - @PathVariable("shareUrl") String shareUrl, + @PathVariable("postId") Long postId, @AuthenticationPrincipal UserInfo userInfo ) { + postService.delete(userInfo.userId(), postId); return ResponseEntity.ok().build(); } diff --git a/src/main/java/com/swyp8team2/user/application/UserService.java b/src/main/java/com/swyp8team2/user/application/UserService.java index 1d09f4bc..71e1c954 100644 --- a/src/main/java/com/swyp8team2/user/application/UserService.java +++ b/src/main/java/com/swyp8team2/user/application/UserService.java @@ -1,7 +1,10 @@ package com.swyp8team2.user.application; +import com.swyp8team2.common.exception.BadRequestException; +import com.swyp8team2.common.exception.ErrorCode; import com.swyp8team2.user.domain.User; import com.swyp8team2.user.domain.UserRepository; +import com.swyp8team2.user.presentation.dto.UserInfoResponse; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -9,6 +12,7 @@ import java.util.Optional; @Service +@Transactional(readOnly = true) @RequiredArgsConstructor public class UserService { @@ -29,4 +33,10 @@ private String getNickname(String email) { return Optional.ofNullable(email) .orElseGet(() -> "user_" + System.currentTimeMillis()); } + + public UserInfoResponse findById(Long userId) { + User user = userRepository.findById(userId) + .orElseThrow(() -> new BadRequestException(ErrorCode.USER_NOT_FOUND)); + return UserInfoResponse.of(user); + } } diff --git a/src/main/java/com/swyp8team2/user/presentation/UserController.java b/src/main/java/com/swyp8team2/user/presentation/UserController.java index d232886b..dc5ac165 100644 --- a/src/main/java/com/swyp8team2/user/presentation/UserController.java +++ b/src/main/java/com/swyp8team2/user/presentation/UserController.java @@ -1,5 +1,6 @@ package com.swyp8team2.user.presentation; +import com.swyp8team2.user.application.UserService; import com.swyp8team2.user.presentation.dto.UserInfoResponse; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; @@ -13,8 +14,10 @@ @RequestMapping("/users") public class UserController { + private final UserService userService; + @GetMapping("/{userId}") public ResponseEntity findUserInfo(@PathVariable("userId") Long userId) { - return ResponseEntity.ok(new UserInfoResponse(1L, "nickname", "https://image.com/profile-image")); + return ResponseEntity.ok(userService.findById(userId)); } } diff --git a/src/main/java/com/swyp8team2/user/presentation/dto/UserInfoResponse.java b/src/main/java/com/swyp8team2/user/presentation/dto/UserInfoResponse.java index 19c204fb..13bf6e26 100644 --- a/src/main/java/com/swyp8team2/user/presentation/dto/UserInfoResponse.java +++ b/src/main/java/com/swyp8team2/user/presentation/dto/UserInfoResponse.java @@ -1,8 +1,13 @@ package com.swyp8team2.user.presentation.dto; +import com.swyp8team2.user.domain.User; + public record UserInfoResponse( Long id, String nickname, String profileUrl ) { + public static UserInfoResponse of(User user) { + return new UserInfoResponse(user.getId(), user.getNickname(), user.getProfileUrl()); + } } diff --git a/src/main/java/com/swyp8team2/vote/application/VoteService.java b/src/main/java/com/swyp8team2/vote/application/VoteService.java index 1c7b2bf0..9860e8a7 100644 --- a/src/main/java/com/swyp8team2/vote/application/VoteService.java +++ b/src/main/java/com/swyp8team2/vote/application/VoteService.java @@ -2,11 +2,13 @@ import com.swyp8team2.common.exception.BadRequestException; import com.swyp8team2.common.exception.ErrorCode; +import com.swyp8team2.post.domain.Post; import com.swyp8team2.post.domain.PostRepository; import com.swyp8team2.user.domain.User; import com.swyp8team2.user.domain.UserRepository; import com.swyp8team2.vote.domain.Vote; import com.swyp8team2.vote.domain.VoteRepository; +import jakarta.validation.constraints.NotNull; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -21,27 +23,36 @@ public class VoteService { private final PostRepository postRepository; @Transactional - public Long vote(Long userId, Long postId, Long imageId) { - User user = userRepository.findById(userId) + public Long vote(Long voterId, Long postId, Long imageId) { + User voter = userRepository.findById(voterId) .orElseThrow(() -> new BadRequestException(ErrorCode.USER_NOT_FOUND)); - voteRepository.findByUserSeqAndPostId(user.getSeq(), postId) - .ifPresent(vote -> deleteExistingVote(postId, vote)); - Vote vote = createVote(postId, imageId, user); + deleteVoteIfExisting(postId, voter.getSeq()); + Vote vote = createVote(postId, imageId, voter.getSeq()); return vote.getId(); } - private Vote createVote(Long postId, Long imageId, User user) { - Vote vote = voteRepository.save(Vote.of(postId, imageId, user.getSeq())); - postRepository.findById(postId) - .orElseThrow(() -> new BadRequestException(ErrorCode.POST_NOT_FOUND)) - .vote(imageId); + private void deleteVoteIfExisting(Long postId, String userSeq) { + voteRepository.findByUserSeqAndPostId(userSeq, postId) + .ifPresent(vote -> { + voteRepository.delete(vote); + postRepository.findById(postId) + .orElseThrow(() -> new BadRequestException(ErrorCode.POST_NOT_FOUND)) + .cancelVote(vote.getPostImageId()); + }); + } + + private Vote createVote(Long postId, Long imageId, String userSeq) { + Post post = postRepository.findById(postId) + .orElseThrow(() -> new BadRequestException(ErrorCode.POST_NOT_FOUND)); + post.validateProgress(); + Vote vote = voteRepository.save(Vote.of(post.getId(), imageId, userSeq)); + post.vote(imageId); return vote; } - private void deleteExistingVote(Long postId, Vote vote) { - voteRepository.delete(vote); - postRepository.findById(postId) - .orElseThrow(() -> new BadRequestException(ErrorCode.POST_NOT_FOUND)) - .cancelVote(vote.getPostImageId()); + public Long guestVote(String guestId, Long postId, Long imageId) { + deleteVoteIfExisting(postId, guestId); + Vote vote = createVote(postId, imageId, guestId); + return vote.getId(); } } diff --git a/src/main/java/com/swyp8team2/vote/presentation/VoteController.java b/src/main/java/com/swyp8team2/vote/presentation/VoteController.java index df3e6711..25025afd 100644 --- a/src/main/java/com/swyp8team2/vote/presentation/VoteController.java +++ b/src/main/java/com/swyp8team2/vote/presentation/VoteController.java @@ -4,14 +4,11 @@ import com.swyp8team2.common.presentation.CustomHeader; import com.swyp8team2.vote.application.VoteService; import com.swyp8team2.vote.presentation.dto.ChangeVoteRequest; -import com.swyp8team2.vote.presentation.dto.GuestVoteRequest; import com.swyp8team2.vote.presentation.dto.VoteRequest; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; -import org.springframework.security.core.Authentication; import org.springframework.security.core.annotation.AuthenticationPrincipal; -import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.PatchMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; @@ -33,7 +30,7 @@ public ResponseEntity vote( @Valid @RequestBody VoteRequest request, @AuthenticationPrincipal UserInfo userInfo ) { - voteService.vote(userInfo.userId(), postId, request.voteId()); + voteService.vote(userInfo.userId(), postId, request.imageId()); return ResponseEntity.ok().build(); } @@ -43,6 +40,7 @@ public ResponseEntity guestVote( @RequestHeader(CustomHeader.GUEST_ID) String guestId, @Valid @RequestBody VoteRequest request ) { + voteService.guestVote(guestId, postId, request.imageId()); return ResponseEntity.ok().build(); } diff --git a/src/main/java/com/swyp8team2/vote/presentation/dto/ChangeVoteRequest.java b/src/main/java/com/swyp8team2/vote/presentation/dto/ChangeVoteRequest.java index 816e9a7d..f9c102f8 100644 --- a/src/main/java/com/swyp8team2/vote/presentation/dto/ChangeVoteRequest.java +++ b/src/main/java/com/swyp8team2/vote/presentation/dto/ChangeVoteRequest.java @@ -4,6 +4,6 @@ public record ChangeVoteRequest( @NotNull - Long voteId + Long imageId ) { } diff --git a/src/main/java/com/swyp8team2/vote/presentation/dto/VoteRequest.java b/src/main/java/com/swyp8team2/vote/presentation/dto/VoteRequest.java index 202c8157..9c7b2adb 100644 --- a/src/main/java/com/swyp8team2/vote/presentation/dto/VoteRequest.java +++ b/src/main/java/com/swyp8team2/vote/presentation/dto/VoteRequest.java @@ -4,6 +4,6 @@ public record VoteRequest( @NotNull - Long voteId + Long imageId ) { } diff --git a/src/test/java/com/swyp8team2/comment/presentation/CommentControllerTest.java b/src/test/java/com/swyp8team2/comment/presentation/CommentControllerTest.java index 3c55561e..46dfbf8b 100644 --- a/src/test/java/com/swyp8team2/comment/presentation/CommentControllerTest.java +++ b/src/test/java/com/swyp8team2/comment/presentation/CommentControllerTest.java @@ -119,10 +119,10 @@ void findComments() throws Exception { fieldWithPath("data[].author.profileUrl") .type(JsonFieldType.STRING) .description("작성자 프로필 이미지 url"), - fieldWithPath("data[].voteId") + fieldWithPath("data[].imageId") .type(JsonFieldType.NUMBER) .optional() - .description("작성자 투표 Id (투표 없을 시 null)"), + .description("작성자가 투표한 이미지 Id (투표 없을 시 null)"), fieldWithPath("data[].createdAt") .type(JsonFieldType.STRING) .description("댓글 작성일") diff --git a/src/test/java/com/swyp8team2/post/application/PostServiceTest.java b/src/test/java/com/swyp8team2/post/application/PostServiceTest.java index b86dd717..53b927be 100644 --- a/src/test/java/com/swyp8team2/post/application/PostServiceTest.java +++ b/src/test/java/com/swyp8team2/post/application/PostServiceTest.java @@ -7,6 +7,7 @@ import com.swyp8team2.post.domain.Post; import com.swyp8team2.post.domain.PostImage; import com.swyp8team2.post.domain.PostRepository; +import com.swyp8team2.post.domain.State; import com.swyp8team2.post.presentation.dto.CreatePostRequest; import com.swyp8team2.post.presentation.dto.PostResponse; import com.swyp8team2.post.presentation.dto.PostImageRequestDto; @@ -237,4 +238,79 @@ void findPostStatus() throws Exception { () -> assertThat(response.get(1).voteRatio()).isEqualTo("0.0") ); } + + @Test + @DisplayName("투표 마감") + void close() throws Exception { + //given + User user = userRepository.save(createUser(1)); + ImageFile imageFile1 = imageFileRepository.save(createImageFile(1)); + ImageFile imageFile2 = imageFileRepository.save(createImageFile(2)); + Post post = postRepository.save(createPost(user.getId(), imageFile1, imageFile2, 1)); + + //when + post.close(user.getId()); + + //then + postRepository.findById(post.getId()).get(); + assertThat(post.getState()).isEqualTo(State.CLOSED); + } + + @Test + @DisplayName("투표 마감 - 게시글 작성자가 아닐 경우") + void close_notPostAuthor() throws Exception { + //given + User user = userRepository.save(createUser(1)); + ImageFile imageFile1 = imageFileRepository.save(createImageFile(1)); + ImageFile imageFile2 = imageFileRepository.save(createImageFile(2)); + Post post = postRepository.save(createPost(user.getId(), imageFile1, imageFile2, 1)); + + //when then + assertThatThrownBy(() -> post.close(2L)) + .isInstanceOf(BadRequestException.class) + .hasMessage(ErrorCode.NOT_POST_AUTHOR.getMessage()); + } + + @Test + @DisplayName("투표 마감 - 이미 마감된 게시글인 경우") + void close_alreadyClosed() throws Exception { + //given + User user = userRepository.save(createUser(1)); + ImageFile imageFile1 = imageFileRepository.save(createImageFile(1)); + ImageFile imageFile2 = imageFileRepository.save(createImageFile(2)); + Post post = postRepository.save(createPost(user.getId(), imageFile1, imageFile2, 1)); + post.close(user.getId()); + + //when then + assertThatThrownBy(() -> post.close(user.getId())) + .isInstanceOf(BadRequestException.class) + .hasMessage(ErrorCode.POST_ALREADY_CLOSED.getMessage()); + } + + @Test + @DisplayName("투표 마감 - 존재하지 않는 게시글일 경우") + void close_notFoundPost() throws Exception { + //given + + //when then + assertThatThrownBy(() -> postService.close(1L, 1L)) + .isInstanceOf(BadRequestException.class) + .hasMessage(ErrorCode.POST_NOT_FOUND.getMessage()); + } + + @Test + @DisplayName("게시글 삭제") + void delete() throws Exception { + //given + User user = userRepository.save(createUser(1)); + ImageFile imageFile1 = imageFileRepository.save(createImageFile(1)); + ImageFile imageFile2 = imageFileRepository.save(createImageFile(2)); + Post post = postRepository.save(createPost(user.getId(), imageFile1, imageFile2, 1)); + + //when + postService.delete(user.getId(), post.getId()); + + //then + assertThat(postRepository.findById(post.getId())).isEmpty(); + } } diff --git a/src/test/java/com/swyp8team2/post/application/RatioCalculatorTest.java b/src/test/java/com/swyp8team2/post/application/RatioCalculatorTest.java index c9262704..27e95e3d 100644 --- a/src/test/java/com/swyp8team2/post/application/RatioCalculatorTest.java +++ b/src/test/java/com/swyp8team2/post/application/RatioCalculatorTest.java @@ -17,7 +17,7 @@ void setUp() { } @ParameterizedTest(name = "{index}: totalVoteCount={0}, voteCount={1} => result={2}") - @CsvSource({"3, 2, 66.7", "3, 1, 33.3", "4, 2, 50.0", "4, 3, 75.0", "0, 0, 0.0", "1, 0, 0.0", "1, 1, 100.0"}) + @CsvSource({"3, 2, 66.7", "3, 1, 33.3", "4, 2, 50.0", "4, 3, 75.0", "0, 0, 0.0", "1, 0, 0.0", "1, 1, 100.0", "10, 7, 70.0", "10, 3, 30.0"}) @DisplayName("비율 계산") void calculate(int totalVoteCount, int voteCount, String result) throws Exception { //given diff --git a/src/test/java/com/swyp8team2/post/domain/PostTest.java b/src/test/java/com/swyp8team2/post/domain/PostTest.java index def54c57..ab59071e 100644 --- a/src/test/java/com/swyp8team2/post/domain/PostTest.java +++ b/src/test/java/com/swyp8team2/post/domain/PostTest.java @@ -3,6 +3,7 @@ import com.swyp8team2.common.exception.BadRequestException; import com.swyp8team2.common.exception.ErrorCode; import com.swyp8team2.common.exception.InternalServerException; +import org.assertj.core.api.Assertions; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -75,4 +76,56 @@ void create_descriptionCountExceeded() throws Exception { .isInstanceOf(BadRequestException.class) .hasMessage(ErrorCode.DESCRIPTION_LENGTH_EXCEEDED.getMessage()); } + + @Test + @DisplayName("투표 마감") + void close() throws Exception { + //given + long userId = 1L; + List postImages = List.of( + PostImage.create("뽀또A", 1L), + PostImage.create("뽀또B", 2L) + ); + Post post = new Post(null, userId, "description", State.PROGRESS, postImages, "shareUrl"); + + //when + post.close(userId); + + //then + assertThat(post.getState()).isEqualTo(State.CLOSED); + } + + @Test + @DisplayName("투표 마감 - 이미 마감된 게시글인 경우") + void close_alreadyClosed() throws Exception { + //given + long userId = 1L; + List postImages = List.of( + PostImage.create("뽀또A", 1L), + PostImage.create("뽀또B", 2L) + ); + Post post = new Post(null, userId, "description", State.CLOSED, postImages, "shareUrl"); + + //when then + assertThatThrownBy(() -> post.close(userId)) + .isInstanceOf(BadRequestException.class) + .hasMessage(ErrorCode.POST_ALREADY_CLOSED.getMessage()); + } + + @Test + @DisplayName("투표 마감 - 게시글 작성자가 아닌 경우") + void close_notPostAuthor() throws Exception { + //given + long userId = 1L; + List postImages = List.of( + PostImage.create("뽀또A", 1L), + PostImage.create("뽀또B", 2L) + ); + Post post = new Post(null, userId, "description", State.PROGRESS, postImages, "shareUrl"); + + //when then + assertThatThrownBy(() -> post.close(2L)) + .isInstanceOf(BadRequestException.class) + .hasMessage(ErrorCode.NOT_POST_AUTHOR.getMessage()); + } } diff --git a/src/test/java/com/swyp8team2/post/presentation/PostControllerTest.java b/src/test/java/com/swyp8team2/post/presentation/PostControllerTest.java index 11c0659c..2615f5d9 100644 --- a/src/test/java/com/swyp8team2/post/presentation/PostControllerTest.java +++ b/src/test/java/com/swyp8team2/post/presentation/PostControllerTest.java @@ -23,6 +23,8 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; import static org.springframework.restdocs.headers.HeaderDocumentation.requestHeaders; import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.get; import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; @@ -158,15 +160,16 @@ void deletePost() throws Exception { //given //when then - mockMvc.perform(RestDocumentationRequestBuilders.delete("/posts/{shareUrl}", "shareUrl") + mockMvc.perform(RestDocumentationRequestBuilders.delete("/posts/{postId}", 1) .header(HttpHeaders.AUTHORIZATION, "Bearer token")) .andExpect(status().isOk()) .andDo(restDocs.document( requestHeaders(authorizationHeader()), pathParameters( - parameterWithName("shareUrl").description("게시글 공유 URL") + parameterWithName("postId").description("게시글 Id") ) )); + verify(postService, times(1)).delete(any(), any()); } @Test @@ -278,4 +281,23 @@ void findVotedPost() throws Exception { ) )); } + + @Test + @WithMockUserInfo + @DisplayName("게시글 마감") + void closePost() throws Exception { + //given + + //when then + mockMvc.perform(RestDocumentationRequestBuilders.post("/posts/{postId}/close", 1) + .header(HttpHeaders.AUTHORIZATION, "Bearer token")) + .andExpect(status().isOk()) + .andDo(restDocs.document( + requestHeaders(authorizationHeader()), + pathParameters( + parameterWithName("postId").description("게시글 Id") + ) + )); + verify(postService, times(1)).close(any(), any()); + } } diff --git a/src/test/java/com/swyp8team2/support/WebUnitTest.java b/src/test/java/com/swyp8team2/support/WebUnitTest.java index fbb726db..ebd1b17c 100644 --- a/src/test/java/com/swyp8team2/support/WebUnitTest.java +++ b/src/test/java/com/swyp8team2/support/WebUnitTest.java @@ -6,6 +6,7 @@ import com.swyp8team2.comment.application.CommentService; import com.swyp8team2.image.application.ImageService; import com.swyp8team2.post.application.PostService; +import com.swyp8team2.user.application.UserService; import com.swyp8team2.vote.application.VoteService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; @@ -40,4 +41,7 @@ public abstract class WebUnitTest { @MockitoBean protected CommentService commentService; + + @MockitoBean + protected UserService userService; } diff --git a/src/test/java/com/swyp8team2/user/presentation/UserControllerTest.java b/src/test/java/com/swyp8team2/user/presentation/UserControllerTest.java index 63a47bd9..f11a7990 100644 --- a/src/test/java/com/swyp8team2/user/presentation/UserControllerTest.java +++ b/src/test/java/com/swyp8team2/user/presentation/UserControllerTest.java @@ -7,6 +7,7 @@ import org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders; import org.springframework.security.test.context.support.WithMockUser; +import static org.mockito.BDDMockito.given; import static org.springframework.restdocs.payload.JsonFieldType.NUMBER; import static org.springframework.restdocs.payload.JsonFieldType.STRING; import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; @@ -24,6 +25,8 @@ class UserControllerTest extends RestDocsTest { void findUserInfo() throws Exception { //given UserInfoResponse response = new UserInfoResponse(1L, "nickname", "https://image.com/profile-image"); + given(userService.findById(1L)) + .willReturn(response); //when then mockMvc.perform(RestDocumentationRequestBuilders.get("/users/{userId}", "1")) diff --git a/src/test/java/com/swyp8team2/vote/application/VoteServiceTest.java b/src/test/java/com/swyp8team2/vote/application/VoteServiceTest.java index e17a96db..04c545cd 100644 --- a/src/test/java/com/swyp8team2/vote/application/VoteServiceTest.java +++ b/src/test/java/com/swyp8team2/vote/application/VoteServiceTest.java @@ -1,9 +1,13 @@ package com.swyp8team2.vote.application; +import com.swyp8team2.common.exception.BadRequestException; +import com.swyp8team2.common.exception.ErrorCode; import com.swyp8team2.image.domain.ImageFile; import com.swyp8team2.image.domain.ImageFileRepository; import com.swyp8team2.post.domain.Post; +import com.swyp8team2.post.domain.PostImage; import com.swyp8team2.post.domain.PostRepository; +import com.swyp8team2.post.domain.State; import com.swyp8team2.support.IntegrationTest; import com.swyp8team2.user.domain.User; import com.swyp8team2.user.domain.UserRepository; @@ -13,10 +17,13 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; +import java.util.List; + import static com.swyp8team2.support.fixture.FixtureGenerator.createImageFile; import static com.swyp8team2.support.fixture.FixtureGenerator.createPost; import static com.swyp8team2.support.fixture.FixtureGenerator.createUser; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.junit.jupiter.api.Assertions.*; class VoteServiceTest extends IntegrationTest { @@ -83,4 +90,104 @@ void vote_change() { () -> assertThat(findPost.getImages().get(1).getVoteCount()).isEqualTo(1) ); } + + @Test + @DisplayName("투표하기 - 투표 마감된 경우") + void vote_alreadyClosed() { + // given + User user = userRepository.save(createUser(1)); + ImageFile imageFile1 = imageFileRepository.save(createImageFile(1)); + ImageFile imageFile2 = imageFileRepository.save(createImageFile(2)); + Post post = postRepository.save(new Post( + null, + user.getId(), + "description", + State.CLOSED, + List.of( + PostImage.create("뽀또A", imageFile1.getId()), + PostImage.create("뽀또B", imageFile2.getId()) + ), + "shareUrl" + )); + + // when + assertThatThrownBy(() -> voteService.vote(user.getId(), post.getId(), post.getImages().get(0).getId())) + .isInstanceOf(BadRequestException.class) + .hasMessage(ErrorCode.POST_ALREADY_CLOSED.getMessage()); + } + + @Test + @DisplayName("게스트 투표하기") + void guestVote() { + // given + String guestId = "guestId"; + User user = userRepository.save(createUser(1)); + ImageFile imageFile1 = imageFileRepository.save(createImageFile(1)); + ImageFile imageFile2 = imageFileRepository.save(createImageFile(2)); + Post post = postRepository.save(createPost(user.getId(), imageFile1, imageFile2, 1)); + + // when + Long voteId = voteService.guestVote(guestId, post.getId(), post.getImages().get(0).getId()); + + // then + Vote vote = voteRepository.findById(voteId).get(); + Post findPost = postRepository.findById(post.getId()).get(); + assertAll( + () -> assertThat(vote.getUserSeq()).isEqualTo(guestId), + () -> assertThat(vote.getPostId()).isEqualTo(post.getId()), + () -> assertThat(vote.getPostImageId()).isEqualTo(post.getImages().get(0).getId()), + () -> assertThat(findPost.getImages().get(0).getVoteCount()).isEqualTo(1) + ); + } + + @Test + @DisplayName("게스트 투표하기 - 다른 이미지로 투표 변경한 경우") + void guestVote_change() { + // given + String guestId = "guestId"; + User user = userRepository.save(createUser(1)); + ImageFile imageFile1 = imageFileRepository.save(createImageFile(1)); + ImageFile imageFile2 = imageFileRepository.save(createImageFile(2)); + Post post = postRepository.save(createPost(user.getId(), imageFile1, imageFile2, 1)); + voteService.guestVote(guestId, post.getId(), post.getImages().get(0).getId()); + + // when + Long voteId = voteService.guestVote(guestId, post.getId(), post.getImages().get(1).getId()); + + // then + Vote vote = voteRepository.findById(voteId).get(); + Post findPost = postRepository.findById(post.getId()).get(); + assertAll( + () -> assertThat(vote.getUserSeq()).isEqualTo(guestId), + () -> assertThat(vote.getPostId()).isEqualTo(post.getId()), + () -> assertThat(vote.getPostImageId()).isEqualTo(post.getImages().get(1).getId()), + () -> assertThat(findPost.getImages().get(0).getVoteCount()).isEqualTo(0), + () -> assertThat(findPost.getImages().get(1).getVoteCount()).isEqualTo(1) + ); + } + + @Test + @DisplayName("게스트 투표하기 - 투표 마감된 경우") + void guestVote_alreadyClosed() { + // given + User user = userRepository.save(createUser(1)); + ImageFile imageFile1 = imageFileRepository.save(createImageFile(1)); + ImageFile imageFile2 = imageFileRepository.save(createImageFile(2)); + Post post = postRepository.save(new Post( + null, + user.getId(), + "description", + State.CLOSED, + List.of( + PostImage.create("뽀또A", imageFile1.getId()), + PostImage.create("뽀또B", imageFile2.getId()) + ), + "shareUrl" + )); + + // when + assertThatThrownBy(() -> voteService.guestVote("guestId", post.getId(), post.getImages().get(0).getId())) + .isInstanceOf(BadRequestException.class) + .hasMessage(ErrorCode.POST_ALREADY_CLOSED.getMessage()); + } } diff --git a/src/test/java/com/swyp8team2/vote/presentation/VoteControllerTest.java b/src/test/java/com/swyp8team2/vote/presentation/VoteControllerTest.java index e0e8263d..c28de442 100644 --- a/src/test/java/com/swyp8team2/vote/presentation/VoteControllerTest.java +++ b/src/test/java/com/swyp8team2/vote/presentation/VoteControllerTest.java @@ -14,6 +14,11 @@ import java.util.UUID; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; import static org.springframework.restdocs.headers.HeaderDocumentation.requestHeaders; import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.patch; import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.post; @@ -44,11 +49,12 @@ void vote() throws Exception { parameterWithName("postId").description("게시글 Id") ), requestFields( - fieldWithPath("voteId") + fieldWithPath("imageId") .type(JsonFieldType.NUMBER) .description("투표 후보 Id") ) )); + verify(voteService, times(1)).vote(any(), any(), any()); } @Test @@ -70,11 +76,12 @@ void guestVote() throws Exception { parameterWithName("postId").description("게시글 Id") ), requestFields( - fieldWithPath("voteId") + fieldWithPath("imageId") .type(JsonFieldType.NUMBER) .description("투표 후보 Id") ) )); + verify(voteService, times(1)).guestVote(any(), any(), any()); } @Test @@ -96,9 +103,9 @@ void changeVote() throws Exception { parameterWithName("postId").description("변경할 게시글 Id") ), requestFields( - fieldWithPath("voteId") + fieldWithPath("imageId") .type(JsonFieldType.NUMBER) - .description("변경할 투표 후보 Id") + .description("변경할 투표 이미지 Id") ) )); } @@ -122,9 +129,9 @@ void guestChangeVote() throws Exception { parameterWithName("postId").description("변경활 게시글 Id") ), requestFields( - fieldWithPath("voteId") + fieldWithPath("imageId") .type(JsonFieldType.NUMBER) - .description("변경할 투표 후보 Id") + .description("변경할 투표 이미지 Id") ) )); }