diff --git a/src/main/java/com/example/solidconnection/common/exception/ErrorCode.java b/src/main/java/com/example/solidconnection/common/exception/ErrorCode.java index 4d135416e..fbd110f9d 100644 --- a/src/main/java/com/example/solidconnection/common/exception/ErrorCode.java +++ b/src/main/java/com/example/solidconnection/common/exception/ErrorCode.java @@ -47,6 +47,7 @@ public enum ErrorCode { REPORT_TARGET_NOT_FOUND(HttpStatus.NOT_FOUND.value(), "존재하지 않는 신고 대상입니다."), CHAT_PARTNER_NOT_FOUND(HttpStatus.BAD_REQUEST.value(), "채팅 상대를 찾을 수 없습니다."), CHAT_PARTICIPANT_NOT_FOUND(HttpStatus.NOT_FOUND.value(), "채팅 참여자를 찾을 수 없습니다."), + BLOCK_USER_NOT_FOUND(HttpStatus.NOT_FOUND.value(), "차단 대상 사용자를 찾을 수 없습니다."), // auth USER_ALREADY_SIGN_OUT(HttpStatus.UNAUTHORIZED.value(), "로그아웃 되었습니다."), @@ -126,6 +127,10 @@ public enum ErrorCode { // report ALREADY_REPORTED_BY_CURRENT_USER(HttpStatus.BAD_REQUEST.value(), "이미 신고한 상태입니다."), + // block + ALREADY_BLOCKED_BY_CURRENT_USER(HttpStatus.BAD_REQUEST.value(), "이미 차단한 상태입니다."), + CANNOT_BLOCK_YOURSELF(HttpStatus.BAD_REQUEST.value(), "자기 자신을 차단할 수 없습니다."), + // chat INVALID_CHAT_ROOM_STATE(HttpStatus.BAD_REQUEST.value(), "잘못된 채팅방 상태입니다."), diff --git a/src/main/java/com/example/solidconnection/community/board/controller/BoardController.java b/src/main/java/com/example/solidconnection/community/board/controller/BoardController.java index 196a48239..89970d334 100644 --- a/src/main/java/com/example/solidconnection/community/board/controller/BoardController.java +++ b/src/main/java/com/example/solidconnection/community/board/controller/BoardController.java @@ -33,11 +33,11 @@ public ResponseEntity findAccessibleCodes() { @GetMapping("/{code}") public ResponseEntity findPostsByCodeAndCategory( - @AuthorizedUser long siteUserId, // todo: '사용하지 않는 인자'로 인증된 유저만 접근하게 하기보다는, 다른 방식으로 접근하는것이 좋을 것 같다 + @AuthorizedUser(required = false) Long siteUserId, @PathVariable(value = "code") String code, @RequestParam(value = "category", defaultValue = "전체") String category) { List postsByCodeAndPostCategory = postQueryService - .findPostsByCodeAndPostCategory(code, category); + .findPostsByCodeAndPostCategory(code, category, siteUserId); return ResponseEntity.ok().body(postsByCodeAndPostCategory); } } diff --git a/src/main/java/com/example/solidconnection/community/comment/repository/CommentRepository.java b/src/main/java/com/example/solidconnection/community/comment/repository/CommentRepository.java index c05cf9bd6..39b42f49a 100644 --- a/src/main/java/com/example/solidconnection/community/comment/repository/CommentRepository.java +++ b/src/main/java/com/example/solidconnection/community/comment/repository/CommentRepository.java @@ -12,25 +12,31 @@ public interface CommentRepository extends JpaRepository { @Query(value = """ - WITH RECURSIVE CommentTree AS ( - SELECT - id, parent_id, post_id, site_user_id, content, - created_at, updated_at, is_deleted, - 0 AS level, CAST(id AS CHAR(255)) AS path - FROM comment - WHERE post_id = :postId AND parent_id IS NULL - UNION ALL - SELECT - c.id, c.parent_id, c.post_id, c.site_user_id, c.content, - c.created_at, c.updated_at, c.is_deleted, - ct.level + 1, CONCAT(ct.path, '->', c.id) - FROM comment c - INNER JOIN CommentTree ct ON c.parent_id = ct.id - ) - SELECT * FROM CommentTree - ORDER BY path - """, nativeQuery = true) - List findCommentTreeByPostId(@Param("postId") Long postId); + WITH RECURSIVE CommentTree AS ( + SELECT + id, parent_id, post_id, site_user_id, content, + created_at, updated_at, is_deleted, + 0 AS level, CAST(id AS CHAR(255)) AS path + FROM comment + WHERE post_id = :postId AND parent_id IS NULL + AND site_user_id NOT IN ( + SELECT blocked_id FROM user_block WHERE blocker_id = :siteUserId + ) + UNION ALL + SELECT + c.id, c.parent_id, c.post_id, c.site_user_id, c.content, + c.created_at, c.updated_at, c.is_deleted, + ct.level + 1, CONCAT(ct.path, '->', c.id) + FROM comment c + INNER JOIN CommentTree ct ON c.parent_id = ct.id + WHERE c.site_user_id NOT IN ( + SELECT blocked_id FROM user_block WHERE blocker_id = :siteUserId + ) + ) + SELECT * FROM CommentTree + ORDER BY path + """, nativeQuery = true) + List findCommentTreeByPostIdExcludingBlockedUsers(@Param("postId") Long postId, @Param("siteUserId") Long siteUserId); default Comment getById(Long id) { return findById(id) diff --git a/src/main/java/com/example/solidconnection/community/comment/service/CommentService.java b/src/main/java/com/example/solidconnection/community/comment/service/CommentService.java index 81b6bb49b..a10be68a9 100644 --- a/src/main/java/com/example/solidconnection/community/comment/service/CommentService.java +++ b/src/main/java/com/example/solidconnection/community/comment/service/CommentService.java @@ -40,7 +40,7 @@ public class CommentService { public List findCommentsByPostId(long siteUserId, Long postId) { SiteUser siteUser = siteUserRepository.findById(siteUserId) .orElseThrow(() -> new CustomException(USER_NOT_FOUND)); - List allComments = commentRepository.findCommentTreeByPostId(postId); + List allComments = commentRepository.findCommentTreeByPostIdExcludingBlockedUsers(postId, siteUserId); List filteredComments = filterCommentsByDeletionRules(allComments); Set userIds = filteredComments.stream() diff --git a/src/main/java/com/example/solidconnection/community/post/repository/PostRepository.java b/src/main/java/com/example/solidconnection/community/post/repository/PostRepository.java index de16d8ab1..aad12fab1 100644 --- a/src/main/java/com/example/solidconnection/community/post/repository/PostRepository.java +++ b/src/main/java/com/example/solidconnection/community/post/repository/PostRepository.java @@ -16,6 +16,15 @@ public interface PostRepository extends JpaRepository { List findByBoardCode(String boardCode); + @Query(""" + SELECT p FROM Post p + WHERE p.boardCode = :boardCode + AND p.siteUserId NOT IN ( + SELECT ub.blockedId FROM UserBlock ub WHERE ub.blockerId = :siteUserId + ) + """) + List findByBoardCodeExcludingBlockedUsers(@Param("boardCode") String boardCode, @Param("siteUserId") Long siteUserId); + @EntityGraph(attributePaths = {"postImageList"}) Optional findPostById(Long id); diff --git a/src/main/java/com/example/solidconnection/community/post/service/PostQueryService.java b/src/main/java/com/example/solidconnection/community/post/service/PostQueryService.java index 85657a8a7..9602cd454 100644 --- a/src/main/java/com/example/solidconnection/community/post/service/PostQueryService.java +++ b/src/main/java/com/example/solidconnection/community/post/service/PostQueryService.java @@ -1,5 +1,6 @@ package com.example.solidconnection.community.post.service; +import static com.example.solidconnection.common.exception.ErrorCode.ACCESS_DENIED; import static com.example.solidconnection.common.exception.ErrorCode.INVALID_BOARD_CODE; import static com.example.solidconnection.common.exception.ErrorCode.INVALID_POST_CATEGORY; import static com.example.solidconnection.common.exception.ErrorCode.USER_NOT_FOUND; @@ -21,6 +22,7 @@ import com.example.solidconnection.siteuser.domain.SiteUser; import com.example.solidconnection.siteuser.dto.PostFindSiteUserResponse; import com.example.solidconnection.siteuser.repository.SiteUserRepository; +import com.example.solidconnection.siteuser.repository.UserBlockRepository; import com.example.solidconnection.util.RedisUtils; import java.util.List; import java.util.Objects; @@ -38,18 +40,24 @@ public class PostQueryService { private final PostRepository postRepository; private final PostLikeRepository postLikeRepository; private final SiteUserRepository siteUserRepository; + private final UserBlockRepository userBlockRepository; private final CommentService commentService; private final RedisService redisService; private final RedisUtils redisUtils; @Transactional(readOnly = true) - public List findPostsByCodeAndPostCategory(String code, String category) { + public List findPostsByCodeAndPostCategory(String code, String category, Long siteUserId) { String boardCode = validateCode(code); PostCategory postCategory = validatePostCategory(category); boardRepository.getByCode(boardCode); - List postList = postRepository.findByBoardCode(boardCode); + List postList; // todo : 추후 개선 필요(현재 최신순으로 응답나가지 않고 있음) + if (siteUserId != null) { + postList = postRepository.findByBoardCodeExcludingBlockedUsers(boardCode, siteUserId); + } else { + postList = postRepository.findByBoardCode(boardCode); + } return PostListResponse.from(getPostListByPostCategory(postList, postCategory)); } @@ -58,6 +66,9 @@ public PostFindResponse findPostById(long siteUserId, Long postId) { SiteUser siteUser = siteUserRepository.findById(siteUserId) .orElseThrow(() -> new CustomException(USER_NOT_FOUND)); Post post = postRepository.getByIdUsingEntityGraph(postId); + + validatedIsBlockedByMe(post, siteUser); + Boolean isOwner = getIsOwner(post, siteUser); Boolean isLiked = getIsLiked(post, siteUser); @@ -111,4 +122,10 @@ private List getPostListByPostCategory(List postList, PostCategory p .filter(post -> post.getCategory().equals(postCategory)) .collect(Collectors.toList()); } + + private void validatedIsBlockedByMe(Post post, SiteUser siteUser) { + if (userBlockRepository.existsByBlockerIdAndBlockedId(siteUser.getId(), post.getSiteUserId())) { + throw new CustomException(ACCESS_DENIED); + } + } } diff --git a/src/main/java/com/example/solidconnection/siteuser/controller/SiteUserController.java b/src/main/java/com/example/solidconnection/siteuser/controller/SiteUserController.java index 64d926eca..77dd01152 100644 --- a/src/main/java/com/example/solidconnection/siteuser/controller/SiteUserController.java +++ b/src/main/java/com/example/solidconnection/siteuser/controller/SiteUserController.java @@ -1,10 +1,19 @@ package com.example.solidconnection.siteuser.controller; +import com.example.solidconnection.common.dto.SliceResponse; +import com.example.solidconnection.common.resolver.AuthorizedUser; import com.example.solidconnection.siteuser.dto.NicknameExistsResponse; +import com.example.solidconnection.siteuser.dto.UserBlockResponse; import com.example.solidconnection.siteuser.service.SiteUserService; import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.data.web.PageableDefault; import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; @@ -23,4 +32,31 @@ public ResponseEntity checkNicknameExists( NicknameExistsResponse nicknameExistsResponse = siteUserService.checkNicknameExists(nickname); return ResponseEntity.ok(nicknameExistsResponse); } + + @GetMapping("/blocks") + public ResponseEntity> getBlockedUsers( + @AuthorizedUser long siteUserId, + @PageableDefault(size = 20, sort = "createdAt", direction = Sort.Direction.DESC) Pageable pageable + ) { + SliceResponse response = siteUserService.getBlockedUsers(siteUserId, pageable); + return ResponseEntity.ok(response); + } + + @PostMapping("/block/{blocked-id}") + public ResponseEntity blockUser( + @AuthorizedUser long siteUserId, + @PathVariable("blocked-id") Long blockedId + ) { + siteUserService.blockUser(siteUserId, blockedId); + return ResponseEntity.ok().build(); + } + + @DeleteMapping("/block/{blocked-id}") + public ResponseEntity cancelUserBlock( + @AuthorizedUser long siteUserId, + @PathVariable("blocked-id") Long blockedId + ) { + siteUserService.cancelUserBlock(siteUserId, blockedId); + return ResponseEntity.ok().build(); + } } diff --git a/src/main/java/com/example/solidconnection/siteuser/domain/UserBlock.java b/src/main/java/com/example/solidconnection/siteuser/domain/UserBlock.java index 2e88ff7cf..9cd78795b 100644 --- a/src/main/java/com/example/solidconnection/siteuser/domain/UserBlock.java +++ b/src/main/java/com/example/solidconnection/siteuser/domain/UserBlock.java @@ -34,4 +34,9 @@ public class UserBlock extends BaseEntity { @Column(name = "blocked_id", nullable = false) private long blockedId; + + public UserBlock(long blockerId, long blockedId) { + this.blockerId = blockerId; + this.blockedId = blockedId; + } } diff --git a/src/main/java/com/example/solidconnection/siteuser/dto/UserBlockResponse.java b/src/main/java/com/example/solidconnection/siteuser/dto/UserBlockResponse.java new file mode 100644 index 000000000..2f307d984 --- /dev/null +++ b/src/main/java/com/example/solidconnection/siteuser/dto/UserBlockResponse.java @@ -0,0 +1,12 @@ +package com.example.solidconnection.siteuser.dto; + +import java.time.ZonedDateTime; + +public record UserBlockResponse( + long id, + long blockedId, + String nickname, + ZonedDateTime createdAt +) { + +} diff --git a/src/main/java/com/example/solidconnection/siteuser/repository/UserBlockRepository.java b/src/main/java/com/example/solidconnection/siteuser/repository/UserBlockRepository.java new file mode 100644 index 000000000..28a88874a --- /dev/null +++ b/src/main/java/com/example/solidconnection/siteuser/repository/UserBlockRepository.java @@ -0,0 +1,27 @@ +package com.example.solidconnection.siteuser.repository; + +import com.example.solidconnection.siteuser.domain.UserBlock; +import com.example.solidconnection.siteuser.dto.UserBlockResponse; +import java.util.Optional; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Slice; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +public interface UserBlockRepository extends JpaRepository { + + boolean existsByBlockerIdAndBlockedId(long blockerId, long blockedId); + + Optional findByBlockerIdAndBlockedId(long blockerId, long blockedId); + + @Query(""" + SELECT new com.example.solidconnection.siteuser.dto.UserBlockResponse( + ub.id, ub.blockedId, su.nickname, ub.createdAt + ) + FROM UserBlock ub + JOIN SiteUser su ON ub.blockedId = su.id + WHERE ub.blockerId = :blockerId + """) + Slice findBlockedUsersWithNickname(@Param("blockerId") long blockerId, Pageable pageable); +} diff --git a/src/main/java/com/example/solidconnection/siteuser/service/SiteUserService.java b/src/main/java/com/example/solidconnection/siteuser/service/SiteUserService.java index e67d71ab8..7052a735a 100644 --- a/src/main/java/com/example/solidconnection/siteuser/service/SiteUserService.java +++ b/src/main/java/com/example/solidconnection/siteuser/service/SiteUserService.java @@ -1,18 +1,71 @@ package com.example.solidconnection.siteuser.service; +import static com.example.solidconnection.common.exception.ErrorCode.ALREADY_BLOCKED_BY_CURRENT_USER; +import static com.example.solidconnection.common.exception.ErrorCode.BLOCK_USER_NOT_FOUND; +import static com.example.solidconnection.common.exception.ErrorCode.CANNOT_BLOCK_YOURSELF; +import static com.example.solidconnection.common.exception.ErrorCode.USER_NOT_FOUND; + +import com.example.solidconnection.common.dto.SliceResponse; +import com.example.solidconnection.common.exception.CustomException; +import com.example.solidconnection.siteuser.domain.UserBlock; import com.example.solidconnection.siteuser.dto.NicknameExistsResponse; +import com.example.solidconnection.siteuser.dto.UserBlockResponse; import com.example.solidconnection.siteuser.repository.SiteUserRepository; +import com.example.solidconnection.siteuser.repository.UserBlockRepository; +import java.util.List; +import java.util.Objects; import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Slice; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; @RequiredArgsConstructor @Service public class SiteUserService { private final SiteUserRepository siteUserRepository; + private final UserBlockRepository userBlockRepository; public NicknameExistsResponse checkNicknameExists(String nickname) { boolean exists = siteUserRepository.existsByNickname(nickname); return NicknameExistsResponse.from(exists); } + + @Transactional(readOnly = true) + public SliceResponse getBlockedUsers(long siteUserId, Pageable pageable) { + Slice slice = userBlockRepository.findBlockedUsersWithNickname(siteUserId, pageable); + + List content = slice.getContent(); + return SliceResponse.of(content, slice); + } + + @Transactional + public void blockUser(long blockerId, long blockedId) { + validateBlockUser(blockerId, blockedId); + UserBlock userBlock = new UserBlock(blockerId, blockedId); + userBlockRepository.save(userBlock); + } + + private void validateBlockUser(long blockerId, long blockedId) { + if (Objects.equals(blockerId, blockedId)) { + throw new CustomException(CANNOT_BLOCK_YOURSELF); + } + if (!siteUserRepository.existsById(blockedId)) { + throw new CustomException(USER_NOT_FOUND); + } + if (userBlockRepository.existsByBlockerIdAndBlockedId(blockerId, blockedId)) { + throw new CustomException(ALREADY_BLOCKED_BY_CURRENT_USER); + } + } + + @Transactional + public void cancelUserBlock(long blockerId, long blockedId) { + if (!siteUserRepository.existsById(blockedId)) { + throw new CustomException(USER_NOT_FOUND); + } + UserBlock userBlock = userBlockRepository.findByBlockerIdAndBlockedId(blockerId, blockedId) + .orElseThrow(() -> new CustomException(BLOCK_USER_NOT_FOUND)); + userBlockRepository.delete(userBlock); + } } diff --git a/src/test/java/com/example/solidconnection/community/comment/service/CommentServiceTest.java b/src/test/java/com/example/solidconnection/community/comment/service/CommentServiceTest.java index c5219b036..63a824e53 100644 --- a/src/test/java/com/example/solidconnection/community/comment/service/CommentServiceTest.java +++ b/src/test/java/com/example/solidconnection/community/comment/service/CommentServiceTest.java @@ -25,6 +25,7 @@ import com.example.solidconnection.siteuser.domain.SiteUser; import com.example.solidconnection.siteuser.dto.PostFindSiteUserResponse; import com.example.solidconnection.siteuser.fixture.SiteUserFixture; +import com.example.solidconnection.siteuser.fixture.UserBlockFixture; import com.example.solidconnection.support.TestContainerSpringBootTest; import jakarta.transaction.Transactional; import java.util.List; @@ -56,6 +57,9 @@ class CommentServiceTest { @Autowired private CommentFixture commentFixture; + @Autowired + private UserBlockFixture userBlockFixture; + private SiteUser user1; private SiteUser user2; private Post post; @@ -187,6 +191,33 @@ class 댓글_조회_테스트 { .containsExactlyInAnyOrder(user2.getId(), user2.getId()) ); } + + @Test + void 차단한_사용자의_댓글은_제외된다() { + // given + userBlockFixture.유저_차단(user1.getId(), user2.getId()); + Comment parentComment1 = commentFixture.부모_댓글("부모 댓글1", post, user1); + Comment childComment1 = commentFixture.자식_댓글("자식 댓글1", post, user1, parentComment1); + Comment childComment2 = commentFixture.자식_댓글("자식 댓글2", post, user2, parentComment1); + Comment parentCommen2 = commentFixture.부모_댓글("부모 댓글2", post, user2); + Comment childComment3 = commentFixture.자식_댓글("자식 댓글1", post, user1, parentCommen2); + Comment childComment4 = commentFixture.자식_댓글("자식 댓글1", post, user1, parentCommen2); + + + // when + List responses = commentService.findCommentsByPostId(user1.getId(), post.getId()); + + // then + assertAll( + () -> assertThat(responses).hasSize(2), + () -> assertThat(responses) + .extracting(PostFindCommentResponse::id) + .containsExactly(parentComment1.getId(), childComment1.getId()), + () -> assertThat(responses) + .extracting(PostFindCommentResponse::id) + .doesNotContain(childComment2.getId(), parentCommen2.getId(), childComment3.getId(), childComment4.getId()) + ); + } } @Nested diff --git a/src/test/java/com/example/solidconnection/community/post/service/PostQueryServiceTest.java b/src/test/java/com/example/solidconnection/community/post/service/PostQueryServiceTest.java index f5e1bb45b..beeb3fc88 100644 --- a/src/test/java/com/example/solidconnection/community/post/service/PostQueryServiceTest.java +++ b/src/test/java/com/example/solidconnection/community/post/service/PostQueryServiceTest.java @@ -1,8 +1,12 @@ package com.example.solidconnection.community.post.service; +import static com.example.solidconnection.common.exception.ErrorCode.ACCESS_DENIED; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.AssertionsForClassTypes.assertThatCode; import static org.junit.jupiter.api.Assertions.assertAll; +import com.example.solidconnection.common.exception.CustomException; +import com.example.solidconnection.community.board.domain.Board; import com.example.solidconnection.community.board.domain.BoardCode; import com.example.solidconnection.community.board.fixture.BoardFixture; import com.example.solidconnection.community.comment.domain.Comment; @@ -15,6 +19,7 @@ import com.example.solidconnection.community.post.fixture.PostImageFixture; import com.example.solidconnection.siteuser.domain.SiteUser; import com.example.solidconnection.siteuser.fixture.SiteUserFixture; +import com.example.solidconnection.siteuser.fixture.UserBlockFixture; import com.example.solidconnection.support.TestContainerSpringBootTest; import com.example.solidconnection.util.RedisUtils; import java.time.ZonedDateTime; @@ -52,6 +57,9 @@ class PostQueryServiceTest { @Autowired private CommentFixture commentFixture; + @Autowired + private UserBlockFixture userBlockFixture; + private SiteUser user; private Post post1; private Post post2; @@ -99,7 +107,8 @@ void setUp() { // when List actualResponses = postQueryService.findPostsByCodeAndPostCategory( BoardCode.FREE.name(), - PostCategory.자유.name() + PostCategory.자유.name(), + null ); // then @@ -121,7 +130,8 @@ void setUp() { // when List actualResponses = postQueryService.findPostsByCodeAndPostCategory( BoardCode.FREE.name(), - PostCategory.전체.name() + PostCategory.전체.name(), + null ); // then @@ -167,6 +177,20 @@ void setUp() { ); } + @Test + void 차단한_사용자의_게시글을_조회하면_예외가_발생한다() { + // given + SiteUser blockedUser = siteUserFixture.사용자(1, "blockedUser"); + userBlockFixture.유저_차단(user.getId(), blockedUser.getId()); + Board board = boardFixture.자유게시판(); + Post post = postFixture.게시글(board, blockedUser); + + // when & then + assertThatCode(() -> postQueryService.findPostById(user.getId(), post.getId())) + .isInstanceOf(CustomException.class) + .hasMessage(ACCESS_DENIED.getMessage()); + } + @Test void 게시글_목록_조회시_첫번째_이미지를_썸네일로_반환한다() { // given @@ -178,7 +202,8 @@ void setUp() { // when List actualResponses = postQueryService.findPostsByCodeAndPostCategory( BoardCode.FREE.name(), - PostCategory.전체.name() + PostCategory.전체.name(), + null ); // then @@ -195,7 +220,8 @@ void setUp() { // when List actualResponses = postQueryService.findPostsByCodeAndPostCategory( BoardCode.FREE.name(), - PostCategory.전체.name() + PostCategory.전체.name(), + null ); // then @@ -206,4 +232,28 @@ void setUp() { assertThat(postResponse.postThumbnailUrl()).isNull(); } + + @Test + void 차단한_사용자의_게시글은_제외된다() { + // given + SiteUser blockedUser = siteUserFixture.사용자(1, "blockedUser"); + SiteUser notBlockedUser = siteUserFixture.사용자(2, "notBlockedUser"); + userBlockFixture.유저_차단(user.getId(), blockedUser.getId()); + Board board = boardFixture.자유게시판(); + Post blockedPost = postFixture.게시글(board, blockedUser); + Post notBlockedPost = postFixture.게시글(board, notBlockedUser); + + // when + List response = postQueryService.findPostsByCodeAndPostCategory( + BoardCode.FREE.name(), + PostCategory.전체.name(), + user.getId() + ); + + // then + assertAll( + () -> assertThat(response).extracting(PostListResponse::id).contains(notBlockedPost.getId()), + () -> assertThat(response).extracting(PostListResponse::id).doesNotContain(blockedPost.getId()) + ); + } } diff --git a/src/test/java/com/example/solidconnection/siteuser/fixture/UserBlockFixture.java b/src/test/java/com/example/solidconnection/siteuser/fixture/UserBlockFixture.java new file mode 100644 index 000000000..cfb11b6e9 --- /dev/null +++ b/src/test/java/com/example/solidconnection/siteuser/fixture/UserBlockFixture.java @@ -0,0 +1,19 @@ +package com.example.solidconnection.siteuser.fixture; + +import com.example.solidconnection.siteuser.domain.UserBlock; +import lombok.RequiredArgsConstructor; +import org.springframework.boot.test.context.TestComponent; + +@TestComponent +@RequiredArgsConstructor +public class UserBlockFixture { + + private final UserBlockFixtureBuilder userBlockFixtureBuilder; + + public UserBlock 유저_차단(long blockerId, long blockedId) { + return userBlockFixtureBuilder.userBlock() + .blockerId(blockerId) + .blockedId(blockedId) + .create(); + } +} diff --git a/src/test/java/com/example/solidconnection/siteuser/fixture/UserBlockFixtureBuilder.java b/src/test/java/com/example/solidconnection/siteuser/fixture/UserBlockFixtureBuilder.java new file mode 100644 index 000000000..deb4e0642 --- /dev/null +++ b/src/test/java/com/example/solidconnection/siteuser/fixture/UserBlockFixtureBuilder.java @@ -0,0 +1,35 @@ +package com.example.solidconnection.siteuser.fixture; + +import com.example.solidconnection.siteuser.domain.UserBlock; +import com.example.solidconnection.siteuser.repository.UserBlockRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.boot.test.context.TestComponent; + +@TestComponent +@RequiredArgsConstructor +public class UserBlockFixtureBuilder { + + private final UserBlockRepository userBlockRepository; + + private long blockerId; + private long blockedId; + + public UserBlockFixtureBuilder userBlock() { + return new UserBlockFixtureBuilder(userBlockRepository); + } + + public UserBlockFixtureBuilder blockerId(long blockerId) { + this.blockerId = blockerId; + return this; + } + + public UserBlockFixtureBuilder blockedId(long blockedId) { + this.blockedId = blockedId; + return this; + } + + public UserBlock create() { + UserBlock userBlock = new UserBlock(blockerId, blockedId); + return userBlockRepository.save(userBlock); + } +} diff --git a/src/test/java/com/example/solidconnection/siteuser/service/SiteUserServiceTest.java b/src/test/java/com/example/solidconnection/siteuser/service/SiteUserServiceTest.java index 3a81d40e2..6dd35d282 100644 --- a/src/test/java/com/example/solidconnection/siteuser/service/SiteUserServiceTest.java +++ b/src/test/java/com/example/solidconnection/siteuser/service/SiteUserServiceTest.java @@ -1,16 +1,30 @@ package com.example.solidconnection.siteuser.service; +import static com.example.solidconnection.common.exception.ErrorCode.ALREADY_BLOCKED_BY_CURRENT_USER; +import static com.example.solidconnection.common.exception.ErrorCode.BLOCK_USER_NOT_FOUND; +import static com.example.solidconnection.common.exception.ErrorCode.CANNOT_BLOCK_YOURSELF; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.AssertionsForClassTypes.assertThatCode; +import static org.junit.jupiter.api.Assertions.assertAll; +import com.example.solidconnection.common.dto.SliceResponse; +import com.example.solidconnection.common.exception.CustomException; import com.example.solidconnection.siteuser.domain.SiteUser; +import com.example.solidconnection.siteuser.domain.UserBlock; import com.example.solidconnection.siteuser.dto.NicknameExistsResponse; +import com.example.solidconnection.siteuser.dto.UserBlockResponse; import com.example.solidconnection.siteuser.fixture.SiteUserFixture; +import com.example.solidconnection.siteuser.fixture.UserBlockFixture; +import com.example.solidconnection.siteuser.repository.UserBlockRepository; import com.example.solidconnection.support.TestContainerSpringBootTest; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; @TestContainerSpringBootTest @DisplayName("유저 서비스 테스트") @@ -19,9 +33,15 @@ class SiteUserServiceTest { @Autowired private SiteUserService siteUserService; + @Autowired + private UserBlockRepository userBlockRepository; + @Autowired private SiteUserFixture siteUserFixture; + @Autowired + private UserBlockFixture userBlockFixture; + private SiteUser user; @BeforeEach @@ -50,4 +70,111 @@ class 닉네임_중복_검사 { assertThat(response.exists()).isFalse(); } } + + @Nested + class 유저_차단_조회 { + + private static final int NO_NEXT_PAGE_NUMBER = -1; + + private SiteUser blockedUser1; + private SiteUser blockedUser2; + private Pageable pageable; + + @BeforeEach + void setUp() { + blockedUser1 = siteUserFixture.사용자(1, "blockedUser1"); + blockedUser2 = siteUserFixture.사용자(2, "blockedUser2"); + pageable = PageRequest.of(0, 20, Sort.by(Sort.Direction.DESC, "createdAt")); + } + + @Test + void 최신순으로_차단한_사용자를_정상적으로_조회한다() { + // given + UserBlock userBlock1 = userBlockFixture.유저_차단(user.getId(), blockedUser1.getId()); + UserBlock userBlock2 = userBlockFixture.유저_차단(user.getId(), blockedUser2.getId()); + + // when + SliceResponse response = siteUserService.getBlockedUsers(user.getId(), pageable); + + // then + assertAll( + () -> assertThat(response.content()).hasSize(2), + () -> assertThat(response.content().get(0).id()).isEqualTo(userBlock2.getId()), + () -> assertThat(response.content().get(0).blockedId()).isEqualTo(blockedUser2.getId()), + () -> assertThat(response.content().get(1).id()).isEqualTo(userBlock1.getId()), + () -> assertThat(response.content().get(1).blockedId()).isEqualTo(blockedUser1.getId()) + ); + } + + @Test + void 차단한_사용자가_없으면_빈_목록을_반환한다() { + // when + SliceResponse response = siteUserService.getBlockedUsers(user.getId(), pageable); + + // then + assertAll( + () -> assertThat(response.content()).isEmpty(), + () -> assertThat(response.nextPageNumber()).isEqualTo(NO_NEXT_PAGE_NUMBER) + ); + } + } + + @Nested + class 유저_차단 { + + private SiteUser blockedUser; + + @BeforeEach + void setUp() { + blockedUser = siteUserFixture.사용자(1, "blockedUser"); + } + + @Test + void 성공적으로_유저를_차단한다() { + // when + siteUserService.blockUser(user.getId(), blockedUser.getId()); + + // then + assertThat(userBlockRepository.existsByBlockerIdAndBlockedId(user.getId(), blockedUser.getId())).isTrue(); + } + + @Test + void 자기_자신을_차단하면_예외가_발생한다() { + // when & then + assertThatCode(() -> siteUserService.blockUser(user.getId(), user.getId())) + .isInstanceOf(CustomException.class) + .hasMessage(CANNOT_BLOCK_YOURSELF.getMessage()); + } + + @Test + void 이미_차단했으면_예외가_발생한다() { + // given + siteUserService.blockUser(user.getId(), blockedUser.getId()); + + // when & then + assertThatCode(() -> siteUserService.blockUser(user.getId(), blockedUser.getId())) + .isInstanceOf(CustomException.class) + .hasMessage(ALREADY_BLOCKED_BY_CURRENT_USER.getMessage()); + } + + @Test + void 성공적으로_유저_차단을_취소한다() { + // given + userBlockFixture.유저_차단(user.getId(), blockedUser.getId()); + + // when + siteUserService.cancelUserBlock(user.getId(), blockedUser.getId()); + + // then + assertThat(userBlockRepository.existsByBlockerIdAndBlockedId(user.getId(), blockedUser.getId())).isFalse(); + } + + @Test + void 차단하지_않았으면_예외가_발생한다() { + // when & then + assertThatCode(() -> siteUserService.cancelUserBlock(user.getId(), blockedUser.getId())) + .isInstanceOf(CustomException.class) + .hasMessage(BLOCK_USER_NOT_FOUND.getMessage()); + } + } }