diff --git a/.github/workflows/be-merge-dev.yml b/.github/workflows/be-merge-dev.yml index e19fbb58..03c2969a 100644 --- a/.github/workflows/be-merge-dev.yml +++ b/.github/workflows/be-merge-dev.yml @@ -4,7 +4,7 @@ on: workflow_dispatch: pull_request: - branches: [ develop-BE-2 ] + branches: [ develop-BE ] types: [ closed ] paths: backend/** @@ -40,12 +40,22 @@ jobs: - name: Gradle build 시작 run: ./gradlew clean build working-directory: backend - - - name: jar 파일 artifact에 업로드 - uses: actions/upload-artifact@v3 + + - name: Docker buildx 설치 + uses: docker/setup-buildx-action@v2.9.1 + + - name: Docker Hub 로그인 + uses: docker/login-action@v2.2.0 with: - name: BackendApplication - path: backend/build/libs/mapbefine.jar + username: ${{ secrets.DOCKERHUB_DEV_USERNAME }} + password: ${{ secrets.DOCKERHUB_DEV_TOKEN }} + + - name: Docker Image Build + working-directory: backend + run: docker build --platform linux/arm64/v8 -t mapbefine/mapbefine -f Dockerfile-dev . + + - name: Docker Hub Push + run: docker push mapbefine/mapbefine deploy: if: github.event.pull_request.merged @@ -54,17 +64,16 @@ jobs: needs: build-and-upload steps: - - name: 구버전 jar 파일 삭제 - run: rm -rf /home/ubuntu/backend/build/*.jar - - - name: jar파일 artifact에서 다운로드 - uses: actions/download-artifact@v3 - with: - name: BackendApplication - path: /home/ubuntu/backend/build/ + - name: 최신 Docker Image 가져오기 + run: | + sudo docker login --username ${{ secrets.DOCKERHUB_DEV_USERNAME }} --password ${{ secrets.DOCKERHUB_DEV_TOKEN }} + sudo docker pull mapbefine/mapbefine:latest - - name: 배포하기 - run: /home/ubuntu/backend/deploy.sh + - name: Docker Compose + run: | + cd /home/ubuntu/backend + sudo sh zero-downtime-deploy.sh + sudo docker image prune -af - name: 슬랙 메시지 보내기 diff --git a/.github/workflows/be-merge-prod.yml b/.github/workflows/be-merge-prod.yml index 3ba5433e..445318cf 100644 --- a/.github/workflows/be-merge-prod.yml +++ b/.github/workflows/be-merge-prod.yml @@ -2,7 +2,7 @@ name: Backend production CI/CD on: workflow_dispatch: - + pull_request: branches: [ main ] types: [ closed ] @@ -10,11 +10,11 @@ on: permissions: contents: read - + jobs: - build-and-upload: + build-and-upload: if: github.event.pull_request.merged - + runs-on: ubuntu-22.04 steps: @@ -41,30 +41,39 @@ jobs: run: ./gradlew clean build working-directory: backend - - name: jar 파일 artifact에 업로드 - uses: actions/upload-artifact@v3 + - name: Docker buildx 설치 + uses: docker/setup-buildx-action@v2.9.1 + + - name: Docker Hub 로그인 + uses: docker/login-action@v2.2.0 with: - name: BackendApplication - path: backend/build/libs/mapbefine.jar + username: ${{ secrets.DOCKERHUB_DEV_USERNAME }} + password: ${{ secrets.DOCKERHUB_DEV_TOKEN }} + + - name: Docker Image Build + working-directory: backend + run: docker build --platform linux/arm64/v8 -t mapbefine/mapbefine -f Dockerfile-prod . + + - name: Docker Hub Push + run: docker push mapbefine/mapbefine deploy: + if: github.event.pull_request.merged + runs-on: [ self-hosted, prod ] needs: build-and-upload - - if: github.event.pull_request.merged - - steps: - - name: 구버전 jar 파일 삭제 - run: rm -rf /home/ubuntu/backend/build/*.jar - - name: jar파일 artifact에서 다운로드 - uses: actions/download-artifact@v3 - with: - name: BackendApplication - path: /home/ubuntu/backend/build/ + steps: + - name: 최신 Docker Image 가져오기 + run: | + sudo docker login --username ${{ secrets.DOCKERHUB_DEV_USERNAME }} --password ${{ secrets.DOCKERHUB_DEV_TOKEN }} + sudo docker pull mapbefine/mapbefine:latest - - name: 배포하기 - run: /home/ubuntu/backend/deploy.sh + - name: Docker Compose + run: | + cd /home/ubuntu/backend + sudo sh zero-downtime-deploy.sh + sudo docker image prune -af - name: 슬랙 메시지 보내기 diff --git a/.github/workflows/be-pull-request.yml b/.github/workflows/be-pull-request.yml index 65236d82..cea976b9 100644 --- a/.github/workflows/be-pull-request.yml +++ b/.github/workflows/be-pull-request.yml @@ -4,7 +4,7 @@ on: workflow_dispatch: pull_request: - branches: [ main, develop-BE-2 ] + branches: [ main, develop-BE ] paths: backend/** permissions: diff --git a/backend/Dockerfile-dev b/backend/Dockerfile-dev new file mode 100644 index 00000000..1e09cbf4 --- /dev/null +++ b/backend/Dockerfile-dev @@ -0,0 +1,3 @@ +FROM openjdk:17 +COPY build/libs/mapbefine.jar mapbefine.jar +ENTRYPOINT ["java", "-jar","-Dspring.profiles.active=dev", "mapbefine.jar"] diff --git a/backend/Dockerfile-prod b/backend/Dockerfile-prod new file mode 100644 index 00000000..1c0570e2 --- /dev/null +++ b/backend/Dockerfile-prod @@ -0,0 +1,3 @@ +FROM openjdk:17 +COPY build/libs/mapbefine.jar mapbefine.jar +ENTRYPOINT ["java", "-jar","-Dspring.profiles.active=prod", "mapbefine.jar"] diff --git a/backend/build.gradle b/backend/build.gradle index c4192d94..8f8b620d 100644 --- a/backend/build.gradle +++ b/backend/build.gradle @@ -28,8 +28,8 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'org.springframework.boot:spring-boot-starter-validation' implementation 'org.springframework.boot:spring-boot-starter-webflux' - implementation group: 'com.github.maricn', name: 'logback-slack-appender', version: '1.6.1' - + implementation 'com.github.maricn:logback-slack-appender:1.6.1' + implementation 'org.hibernate:hibernate-spatial:6.2.5.Final' implementation 'mysql:mysql-connector-java:8.0.32' implementation 'io.jsonwebtoken:jjwt:0.9.1' @@ -44,6 +44,8 @@ dependencies { testImplementation 'io.rest-assured:rest-assured' testImplementation 'io.rest-assured:spring-mock-mvc' testImplementation 'org.assertj:assertj-core:3.19.0' + testImplementation 'org.testcontainers:mysql:1.17.2' + testImplementation 'org.testcontainers:junit-jupiter:1.17.2' // S3 implementation platform('com.amazonaws:aws-java-sdk-bom:1.11.1000') diff --git a/backend/src/docs/asciidoc/admin.adoc b/backend/src/docs/asciidoc/admin.adoc index 64b33844..d84acb3e 100644 --- a/backend/src/docs/asciidoc/admin.adoc +++ b/backend/src/docs/asciidoc/admin.adoc @@ -1,29 +1,29 @@ -// == 관리자 기능 -// -// === 전체 회원 조회 -// -// operation::admin-controller-test/find-all-member-details[snippets='http-request,http-response'] -// -// === 회원 상세 조회 -// -// operation::admin-controller-test/find-member[snippets='http-request,http-response'] -// -// === 회원 차단(삭제) -// -// operation::admin-controller-test/delete-member[snippets='http-request,http-response'] -// -// === 토픽 삭제 -// -// operation::admin-controller-test/delete-topic[snippets='http-request,http-response'] -// -// === 토픽 이미지 삭제 -// -// operation::admin-controller-test/delete-topic-image[snippets='http-request,http-response'] -// -// === 핀 삭제 -// -// operation::admin-controller-test/delete-pin[snippets='http-request,http-response'] -// -// === 핀 이미지 삭제 -// -// operation::admin-controller-test/delete-pin-image[snippets='http-request,http-response'] +== 관리자 기능 + +=== 전체 회원 조회 + +operation::admin-controller-test/find-all-member-details[snippets='http-request,http-response'] + +=== 회원 상세 조회 + +operation::admin-controller-test/find-member[snippets='http-request,http-response'] + +=== 회원 차단(삭제) + +operation::admin-controller-test/delete-member[snippets='http-request,http-response'] + +=== 토픽 삭제 + +operation::admin-controller-test/delete-topic[snippets='http-request,http-response'] + +=== 토픽 이미지 삭제 + +operation::admin-controller-test/delete-topic-image[snippets='http-request,http-response'] + +=== 핀 삭제 + +operation::admin-controller-test/delete-pin[snippets='http-request,http-response'] + +=== 핀 이미지 삭제 + +operation::admin-controller-test/delete-pin-image[snippets='http-request,http-response'] diff --git a/backend/src/docs/asciidoc/member.adoc b/backend/src/docs/asciidoc/member.adoc index 520b3943..77b59c4a 100644 --- a/backend/src/docs/asciidoc/member.adoc +++ b/backend/src/docs/asciidoc/member.adoc @@ -4,9 +4,9 @@ operation::member-controller-test/find-all-member[snippets='http-request,http-response'] -=== 회원 단일 조회 +=== 회원의 나의 프로필 조회 -operation::member-controller-test/find-member-by-id[snippets='http-request,http-response'] +operation::member-controller-test/find-my-profile[snippets='http-request,http-response'] === 회원의 나의 지도 목록 조회 diff --git a/backend/src/docs/asciidoc/pin.adoc b/backend/src/docs/asciidoc/pin.adoc index 33b15e36..fab90bba 100644 --- a/backend/src/docs/asciidoc/pin.adoc +++ b/backend/src/docs/asciidoc/pin.adoc @@ -20,10 +20,6 @@ operation::pin-controller-test/add[snippets='http-request,http-response'] operation::pin-controller-test/update[snippets='http-request,http-response'] -=== 핀 삭제 - -operation::pin-controller-test/delete[snippets='http-request,http-response'] - === 핀 이미지 추가 operation::pin-controller-test/add-image[snippets='http-request,http-response'] @@ -31,3 +27,23 @@ operation::pin-controller-test/add-image[snippets='http-request,http-response'] === 핀 이미지 삭제 operation::pin-controller-test/remove-image[snippets='http-request,http-response'] + +=== 핀 댓글 생성 + +operation::pin-controller-test/create-parent-pin-comment[snippets='http-request,http-response'] + +=== 핀 대댓글 생성 + +operation::pin-controller-test/create-child-pin-comment[snippets='http-request,http-response'] + +=== 핀 댓글 조회 + +operation::pin-controller-test/find-all-pin-comment-by-pin-id[snippets='http-request,http-response'] + +=== 핀 댓글 수정 + +operation::pin-controller-test/update-pin-comment[snippets='http-request,http-response'] + +=== 핀 댓글 삭제 + +operation::pin-controller-test/remove-pin-comment[snippets='http-request,http-response'] diff --git a/backend/src/docs/asciidoc/topic.adoc b/backend/src/docs/asciidoc/topic.adoc index 42356837..509e4716 100644 --- a/backend/src/docs/asciidoc/topic.adoc +++ b/backend/src/docs/asciidoc/topic.adoc @@ -40,7 +40,6 @@ operation::topic-controller-test/copy-pin[snippets='http-request,http-response'] operation::topic-controller-test/update[snippets='http-request,http-response'] -=== 토픽 삭제 - -operation::topic-controller-test/delete[snippets='http-request,http-response'] +=== 토픽 이미지 수정 +operation::topic-controller-test/update-image[snippets='http-request,http-response'] diff --git a/backend/src/main/java/com/mapbefine/mapbefine/admin/application/AdminCommandService.java b/backend/src/main/java/com/mapbefine/mapbefine/admin/application/AdminCommandService.java index d19f772c..989a79f0 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/admin/application/AdminCommandService.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/admin/application/AdminCommandService.java @@ -1,5 +1,6 @@ package com.mapbefine.mapbefine.admin.application; +import static com.mapbefine.mapbefine.pin.exception.PinErrorCode.PIN_NOT_FOUND; import static com.mapbefine.mapbefine.topic.exception.TopicErrorCode.TOPIC_NOT_FOUND; import com.mapbefine.mapbefine.atlas.domain.AtlasRepository; @@ -11,14 +12,14 @@ import com.mapbefine.mapbefine.pin.domain.Pin; import com.mapbefine.mapbefine.pin.domain.PinImageRepository; import com.mapbefine.mapbefine.pin.domain.PinRepository; +import com.mapbefine.mapbefine.pin.exception.PinException.PinNotFoundException; import com.mapbefine.mapbefine.topic.domain.Topic; import com.mapbefine.mapbefine.topic.domain.TopicRepository; import com.mapbefine.mapbefine.topic.exception.TopicException; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - import java.util.List; import java.util.NoSuchElementException; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; @Service @Transactional @@ -53,6 +54,7 @@ public AdminCommandService( public void blockMember(Long memberId) { Member member = findMemberById(memberId); member.updateStatus(Status.BLOCKED); + memberRepository.flush(); deleteAllRelatedMember(member); } @@ -61,12 +63,12 @@ private void deleteAllRelatedMember(Member member) { List pinIds = extractPinIdsByMember(member); Long memberId = member.getId(); - pinImageRepository.deleteAllByPinIds(pinIds); - topicRepository.deleteAllByMemberId(memberId); - pinRepository.deleteAllByMemberId(memberId); permissionRepository.deleteAllByMemberId(memberId); atlasRepository.deleteAllByMemberId(memberId); bookmarkRepository.deleteAllByMemberId(memberId); + pinImageRepository.deleteAllByPinIds(pinIds); + pinRepository.deleteAllByMemberId(memberId); + topicRepository.deleteAllByMemberId(memberId); } private Member findMemberById(Long id) { @@ -82,21 +84,41 @@ private List extractPinIdsByMember(Member member) { } public void deleteTopic(Long topicId) { + Topic topic = findTopicById(topicId); + List pinIds = extractPinIdsByTopic(topic); + + permissionRepository.deleteAllByTopicId(topicId); + atlasRepository.deleteAllByTopicId(topicId); + bookmarkRepository.deleteAllByTopicId(topicId); + pinImageRepository.deleteAllByPinIds(pinIds); + pinRepository.deleteAllByTopicId(topicId); topicRepository.deleteById(topicId); } + private List extractPinIdsByTopic(Topic topic) { + return topic.getPins() + .stream() + .map(Pin::getId) + .toList(); + } + public void deleteTopicImage(Long topicId) { Topic topic = findTopicById(topicId); topic.removeImage(); } private Topic findTopicById(Long topicId) { - return topicRepository.findByIdAndIsDeletedFalse(topicId) + return topicRepository.findById(topicId) .orElseThrow(() -> new TopicException.TopicNotFoundException(TOPIC_NOT_FOUND, List.of(topicId))); } public void deletePin(Long pinId) { - pinRepository.deleteById(pinId); + Pin pin = pinRepository.findById(pinId) + .orElseThrow(() -> new PinNotFoundException(PIN_NOT_FOUND, pinId)); + + pin.decreaseTopicPinCount(); + pinImageRepository.deleteAllByPinId(pinId); + pinRepository.deleteById(pin.getId()); } public void deletePinImage(Long pinImageId) { diff --git a/backend/src/main/java/com/mapbefine/mapbefine/admin/presentation/AdminController.java b/backend/src/main/java/com/mapbefine/mapbefine/admin/presentation/AdminController.java index fb7533ce..4e290ae1 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/admin/presentation/AdminController.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/admin/presentation/AdminController.java @@ -20,7 +20,6 @@ public class AdminController { private final AdminQueryService adminQueryService; private final AdminCommandService adminCommandService; - public AdminController(AdminQueryService adminQueryService, AdminCommandService adminCommandService) { this.adminQueryService = adminQueryService; this.adminCommandService = adminCommandService; diff --git a/backend/src/main/java/com/mapbefine/mapbefine/atlas/application/AtlasCommandService.java b/backend/src/main/java/com/mapbefine/mapbefine/atlas/application/AtlasCommandService.java index b1db0033..95c9588b 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/atlas/application/AtlasCommandService.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/atlas/application/AtlasCommandService.java @@ -13,11 +13,10 @@ import com.mapbefine.mapbefine.member.domain.MemberRepository; import com.mapbefine.mapbefine.topic.domain.Topic; import com.mapbefine.mapbefine.topic.domain.TopicRepository; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - import java.util.NoSuchElementException; import java.util.Objects; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; @Service @Transactional @@ -73,7 +72,7 @@ private boolean isTopicAlreadyAdded(Long topicId, Long memberId) { } private Topic findTopicById(Long topicId) { - return topicRepository.findByIdAndIsDeletedFalse(topicId) + return topicRepository.findById(topicId) .orElseThrow(() -> new AtlasBadRequestException(ILLEGAL_TOPIC_ID)); } diff --git a/backend/src/main/java/com/mapbefine/mapbefine/atlas/domain/AtlasRepository.java b/backend/src/main/java/com/mapbefine/mapbefine/atlas/domain/AtlasRepository.java index 664cdc94..1cd455ed 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/atlas/domain/AtlasRepository.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/atlas/domain/AtlasRepository.java @@ -1,13 +1,23 @@ package com.mapbefine.mapbefine.atlas.domain; 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; @Repository public interface AtlasRepository extends JpaRepository { boolean existsByMemberIdAndTopicId(Long memberId, Long topicId); + void deleteByMemberIdAndTopicId(Long memberId, Long topicId); - void deleteAllByMemberId(Long memberId); + @Modifying(clearAutomatically = true) + @Query("delete from Atlas a where a.member.id = :memberId") + void deleteAllByMemberId(@Param("memberId") Long memberId); + + @Modifying(clearAutomatically = true) + @Query("delete from Atlas a where a.topic.id = :topicId") + void deleteAllByTopicId(@Param("topicId") Long topicId); } diff --git a/backend/src/main/java/com/mapbefine/mapbefine/auth/application/AuthService.java b/backend/src/main/java/com/mapbefine/mapbefine/auth/application/AuthService.java index d35346de..e4419cf6 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/auth/application/AuthService.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/auth/application/AuthService.java @@ -11,7 +11,6 @@ import com.mapbefine.mapbefine.topic.domain.Topic; import java.util.List; import java.util.Objects; -import java.util.Optional; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -32,9 +31,12 @@ public void validateMember(Long memberId) { } public AuthMember findAuthMemberByMemberId(Long memberId) { - return memberRepository.findById(memberId) - .map(this::convertToAuthMember) + Member member = memberRepository.findById(memberId) .orElseThrow(() -> new IllegalArgumentException("findAuthMemberByMemberId; memberId= " + memberId)); + if (member.isNormalStatus()) { + return convertToAuthMember(member); + } + throw new AuthUnauthorizedException(AuthErrorCode.ILLEGAL_MEMBER_ID); } private AuthMember convertToAuthMember(Member member) { @@ -63,15 +65,4 @@ private List getCreatedTopics(Member member) { .toList(); } - public boolean isAdmin(Long memberId) { - if (Objects.isNull(memberId)) { - return false; - } - - Optional member = memberRepository.findById(memberId); - - return member.map(Member::isAdmin) - .orElse(false); - } - } diff --git a/backend/src/main/java/com/mapbefine/mapbefine/auth/domain/AuthMember.java b/backend/src/main/java/com/mapbefine/mapbefine/auth/domain/AuthMember.java index d27f7cc2..3c65bd27 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/auth/domain/AuthMember.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/auth/domain/AuthMember.java @@ -1,8 +1,8 @@ package com.mapbefine.mapbefine.auth.domain; -import com.mapbefine.mapbefine.member.domain.Role; import com.mapbefine.mapbefine.topic.domain.Topic; import java.util.List; +import java.util.Objects; public abstract class AuthMember { @@ -28,10 +28,20 @@ protected AuthMember( public abstract boolean canPinCreateOrUpdate(Topic topic); - public abstract boolean isRole(Role role); - + public abstract boolean canPinCommentCreate(Topic topic); + public Long getMemberId() { return memberId; } + public boolean isSameMember(Long memberId) { + return Objects.equals(memberId, this.memberId); + } + + public abstract boolean isAdmin(); + + public abstract boolean isUser(); + + public abstract boolean isGuest(); + } diff --git a/backend/src/main/java/com/mapbefine/mapbefine/auth/domain/member/Admin.java b/backend/src/main/java/com/mapbefine/mapbefine/auth/domain/member/Admin.java index f6a54648..73311c0a 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/auth/domain/member/Admin.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/auth/domain/member/Admin.java @@ -1,7 +1,6 @@ package com.mapbefine.mapbefine.auth.domain.member; import com.mapbefine.mapbefine.auth.domain.AuthMember; -import com.mapbefine.mapbefine.member.domain.Role; import com.mapbefine.mapbefine.topic.domain.Topic; import java.util.Collections; @@ -36,8 +35,23 @@ public boolean canPinCreateOrUpdate(Topic topic) { } @Override - public boolean isRole(Role role) { - return Role.ADMIN == role; + public boolean canPinCommentCreate(Topic topic) { + return true; + } + + @Override + public boolean isAdmin() { + return true; + } + + @Override + public boolean isUser() { + return false; + } + + @Override + public boolean isGuest() { + return false; } } diff --git a/backend/src/main/java/com/mapbefine/mapbefine/auth/domain/member/Guest.java b/backend/src/main/java/com/mapbefine/mapbefine/auth/domain/member/Guest.java index dcd8dbeb..fb064a2f 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/auth/domain/member/Guest.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/auth/domain/member/Guest.java @@ -1,7 +1,6 @@ package com.mapbefine.mapbefine.auth.domain.member; import com.mapbefine.mapbefine.auth.domain.AuthMember; -import com.mapbefine.mapbefine.member.domain.Role; import com.mapbefine.mapbefine.topic.domain.Topic; import com.mapbefine.mapbefine.topic.domain.TopicStatus; import java.util.Collections; @@ -38,7 +37,23 @@ public boolean canPinCreateOrUpdate(Topic topic) { } @Override - public boolean isRole(Role role) { - return Role.GUEST == role; + public boolean canPinCommentCreate(Topic topic) { + return false; } + + @Override + public boolean isAdmin() { + return false; + } + + @Override + public boolean isUser() { + return false; + } + + @Override + public boolean isGuest() { + return true; + } + } diff --git a/backend/src/main/java/com/mapbefine/mapbefine/auth/domain/member/User.java b/backend/src/main/java/com/mapbefine/mapbefine/auth/domain/member/User.java index ed2ea7b8..e7ebd671 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/auth/domain/member/User.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/auth/domain/member/User.java @@ -1,7 +1,6 @@ package com.mapbefine.mapbefine.auth.domain.member; import com.mapbefine.mapbefine.auth.domain.AuthMember; -import com.mapbefine.mapbefine.member.domain.Role; import com.mapbefine.mapbefine.topic.domain.Topic; import com.mapbefine.mapbefine.topic.domain.TopicStatus; import java.util.List; @@ -23,12 +22,14 @@ public User( @Override public boolean canRead(Topic topic) { TopicStatus topicStatus = topic.getTopicStatus(); - return topicStatus.isPublic() || isGroup(topic.getId()); + + return topicStatus.isPublic() || hasPermission(topic.getId()); } @Override public boolean canDelete(Topic topic) { TopicStatus topicStatus = topic.getTopicStatus(); + return topicStatus.isPrivate() && isCreator(topic.getId()); } @@ -40,24 +41,36 @@ public boolean canTopicUpdate(Topic topic) { @Override public boolean canPinCreateOrUpdate(Topic topic) { TopicStatus topicStatus = topic.getTopicStatus(); + return topicStatus.isAllMembers() || hasPermission(topic.getId()); } - private boolean isCreator(Long topicId) { - return createdTopic.contains(topicId); + @Override + public boolean canPinCommentCreate(Topic topic) { + return canRead(topic); } - private boolean isGroup(Long topicId) { - return isCreator(topicId) || hasPermission(topicId); + @Override + public boolean isAdmin() { + return false; } - private boolean hasPermission(Long topicId) { - return createdTopic.contains(topicId) || topicsWithPermission.contains(topicId); + @Override + public boolean isUser() { + return true; } @Override - public boolean isRole(Role role) { - return Role.USER == role; + public boolean isGuest() { + return false; + } + + private boolean isCreator(Long topicId) { + return createdTopic.contains(topicId); + } + + private boolean hasPermission(Long topicId) { + return isCreator(topicId) || topicsWithPermission.contains(topicId); } } diff --git a/backend/src/main/java/com/mapbefine/mapbefine/bookmark/application/BookmarkCommandService.java b/backend/src/main/java/com/mapbefine/mapbefine/bookmark/application/BookmarkCommandService.java index 7c9828dc..b0dea0ea 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/bookmark/application/BookmarkCommandService.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/bookmark/application/BookmarkCommandService.java @@ -15,11 +15,10 @@ import com.mapbefine.mapbefine.member.domain.MemberRepository; import com.mapbefine.mapbefine.topic.domain.Topic; import com.mapbefine.mapbefine.topic.domain.TopicRepository; +import java.util.NoSuchElementException; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import java.util.NoSuchElementException; - @Service @Transactional public class BookmarkCommandService { @@ -53,7 +52,7 @@ public Long addTopicInBookmark(AuthMember authMember, Long topicId) { } private Topic getTopicById(Long topicId) { - return topicRepository.findByIdAndIsDeletedFalse(topicId) + return topicRepository.findById(topicId) .orElseThrow(() -> new BookmarkBadRequestException(ILLEGAL_TOPIC_ID)); } diff --git a/backend/src/main/java/com/mapbefine/mapbefine/bookmark/domain/BookmarkRepository.java b/backend/src/main/java/com/mapbefine/mapbefine/bookmark/domain/BookmarkRepository.java index 34c58d3c..f16d4079 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/bookmark/domain/BookmarkRepository.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/bookmark/domain/BookmarkRepository.java @@ -1,17 +1,23 @@ package com.mapbefine.mapbefine.bookmark.domain; +import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Modifying; - -import java.util.Optional; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; public interface BookmarkRepository extends JpaRepository { + Optional findByMemberIdAndTopicId(Long memberId, Long topicId); + boolean existsByMemberIdAndTopicId(Long memberId, Long topicId); @Modifying(clearAutomatically = true) - void deleteAllByMemberId(Long memberId); + @Query("delete from Bookmark b where b.member.id = :memberId") + void deleteAllByMemberId(@Param("memberId") Long memberId); - Optional findByMemberIdAndTopicId(Long memberId, Long topicId); + @Modifying(clearAutomatically = true) + @Query("delete from Bookmark b where b.topic.id = :topicId") + void deleteAllByTopicId(@Param("topicId") Long topicId); } diff --git a/backend/src/main/java/com/mapbefine/mapbefine/common/interceptor/AuthInterceptor.java b/backend/src/main/java/com/mapbefine/mapbefine/common/interceptor/AuthInterceptor.java index 7ee91245..46e01ba2 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/common/interceptor/AuthInterceptor.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/common/interceptor/AuthInterceptor.java @@ -40,7 +40,7 @@ public boolean preHandle( if (!(handler instanceof HandlerMethod handlerMethod)) { return true; } - if (isAuthMemberNotRequired(handlerMethod)) { + if (isAuthMemberNotRequired(handlerMethod) && isLoginNotRequired(handlerMethod)) { return true; } @@ -60,6 +60,10 @@ private boolean isAuthMemberNotRequired(HandlerMethod handlerMethod) { .noneMatch(parameter -> parameter.getParameterType().equals(AuthMember.class)); } + private boolean isLoginNotRequired(HandlerMethod handlerMethod) { + return !isLoginRequired(handlerMethod); + } + private boolean isLoginRequired(HandlerMethod handlerMethod) { LoginRequired loginRequired = handlerMethod.getMethodAnnotation(LoginRequired.class); diff --git a/backend/src/main/java/com/mapbefine/mapbefine/history/application/PinHistoryCommandService.java b/backend/src/main/java/com/mapbefine/mapbefine/history/application/PinHistoryCommandService.java new file mode 100644 index 00000000..fc593dd6 --- /dev/null +++ b/backend/src/main/java/com/mapbefine/mapbefine/history/application/PinHistoryCommandService.java @@ -0,0 +1,31 @@ +package com.mapbefine.mapbefine.history.application; + +import com.mapbefine.mapbefine.history.domain.PinHistory; +import com.mapbefine.mapbefine.history.domain.PinHistoryRepository; +import com.mapbefine.mapbefine.pin.domain.Pin; +import com.mapbefine.mapbefine.pin.event.PinUpdateEvent; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.event.EventListener; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Slf4j +@Transactional +@Service +public class PinHistoryCommandService { + + private final PinHistoryRepository pinHistoryRepository; + + public PinHistoryCommandService(PinHistoryRepository pinHistoryRepository) { + this.pinHistoryRepository = pinHistoryRepository; + } + + @EventListener + public void saveHistory(PinUpdateEvent event) { + Pin pin = event.pin(); + pinHistoryRepository.save(new PinHistory(pin, event.member())); + + log.debug("pin history saved for update pin id =: {}", pin.getId()); + } + +} diff --git a/backend/src/main/java/com/mapbefine/mapbefine/history/domain/PinHistory.java b/backend/src/main/java/com/mapbefine/mapbefine/history/domain/PinHistory.java new file mode 100644 index 00000000..46025976 --- /dev/null +++ b/backend/src/main/java/com/mapbefine/mapbefine/history/domain/PinHistory.java @@ -0,0 +1,55 @@ +package com.mapbefine.mapbefine.history.domain; + +import static lombok.AccessLevel.PROTECTED; + +import com.mapbefine.mapbefine.common.entity.BaseTimeEntity; +import com.mapbefine.mapbefine.member.domain.Member; +import com.mapbefine.mapbefine.pin.domain.Pin; +import com.mapbefine.mapbefine.pin.domain.PinInfo; +import jakarta.persistence.Column; +import jakarta.persistence.Embedded; +import jakarta.persistence.Entity; +import jakarta.persistence.EntityListeners; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import java.time.LocalDateTime; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; + +@EntityListeners(AuditingEntityListener.class) +@Entity +@NoArgsConstructor(access = PROTECTED) +@Getter +public class PinHistory extends BaseTimeEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne + @JoinColumn(name = "pin_id", nullable = false) + private Pin pin; + + @ManyToOne + @JoinColumn(name = "member_id", nullable = false) + private Member member; + + @Embedded + private PinInfo pinInfo; + + @Column(name = "pin_updated_at", nullable = false) + private LocalDateTime pinUpdatedAt; + + public PinHistory(Pin pin, Member member) { + this.pin = pin; + PinInfo history = pin.getPinInfo(); + this.pinInfo = PinInfo.of(history.getName(), history.getDescription()); + this.pinUpdatedAt = pin.getUpdatedAt(); + this.member = member; + } + +} diff --git a/backend/src/main/java/com/mapbefine/mapbefine/history/domain/PinHistoryRepository.java b/backend/src/main/java/com/mapbefine/mapbefine/history/domain/PinHistoryRepository.java new file mode 100644 index 00000000..d210f541 --- /dev/null +++ b/backend/src/main/java/com/mapbefine/mapbefine/history/domain/PinHistoryRepository.java @@ -0,0 +1,10 @@ +package com.mapbefine.mapbefine.history.domain; + +import java.util.List; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface PinHistoryRepository extends JpaRepository { + List findAllByPinId(Long pinId); +} diff --git a/backend/src/main/java/com/mapbefine/mapbefine/location/application/LocationQueryService.java b/backend/src/main/java/com/mapbefine/mapbefine/location/application/LocationQueryService.java index 7dd0235c..5c67c85e 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/location/application/LocationQueryService.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/location/application/LocationQueryService.java @@ -44,7 +44,7 @@ public List findNearbyTopicsSortedByPinCount( ) { Coordinate coordinate = Coordinate.of(latitude, longitude); List nearLocation = locationRepository.findAllByCoordinateAndDistanceInMeters( - coordinate, + coordinate.getCoordinate(), NEAR_DISTANCE_METERS ); diff --git a/backend/src/main/java/com/mapbefine/mapbefine/location/domain/Coordinate.java b/backend/src/main/java/com/mapbefine/mapbefine/location/domain/Coordinate.java index cb3f7a58..1ec913d6 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/location/domain/Coordinate.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/location/domain/Coordinate.java @@ -1,10 +1,6 @@ package com.mapbefine.mapbefine.location.domain; import static com.mapbefine.mapbefine.location.exception.LocationErrorCode.ILLEGAL_COORDINATE_RANGE; -import static java.lang.Math.acos; -import static java.lang.Math.cos; -import static java.lang.Math.sin; -import static java.lang.Math.toRadians; import static lombok.AccessLevel.PROTECTED; import com.mapbefine.mapbefine.location.exception.LocationException.LocationBadRequestException; @@ -12,6 +8,9 @@ import jakarta.persistence.Embeddable; import lombok.Getter; import lombok.NoArgsConstructor; +import org.locationtech.jts.geom.GeometryFactory; +import org.locationtech.jts.geom.Point; +import org.locationtech.jts.geom.PrecisionModel; @Embeddable @NoArgsConstructor(access = PROTECTED) @@ -23,21 +22,25 @@ public class Coordinate { private static final double LONGITUDE_LOWER_BOUND = 124; private static final double LONGITUDE_UPPER_BOUND = 132; - @Column(columnDefinition = "Decimal(18,15)") - private double latitude; + /* + * 4326은 데이터베이스에서 사용하는 여러 SRID 값 중, 일반적인 GPS기반의 위/경도 좌표를 저장할 때 쓰이는 값입니다. + * */ + private static final GeometryFactory geometryFactory = new GeometryFactory(new PrecisionModel(), 4326); - @Column(columnDefinition = "Decimal(18,15)") - private double longitude; + @Column(columnDefinition = "geometry SRID 4326", nullable = false) + private Point coordinate; - private Coordinate(double latitude, double longitude) { - this.latitude = latitude; - this.longitude = longitude; + private Coordinate(Point point) { + this.coordinate = point; } + public static Coordinate of(double latitude, double longitude) { validateRange(latitude, longitude); - return new Coordinate(latitude, longitude); + Point point = geometryFactory.createPoint(new org.locationtech.jts.geom.Coordinate(longitude, latitude)); + + return new Coordinate(point); } private static void validateRange(double latitude, double longitude) { @@ -51,13 +54,12 @@ private static boolean isNotInRange(double latitude, double longitude) { || (longitude < LONGITUDE_LOWER_BOUND || LONGITUDE_UPPER_BOUND < longitude); } - public double calculateDistanceInMeters(Coordinate otherCoordinate) { - double earthRadius = 6_371_000; + public double getLatitude() { + return coordinate.getY(); + } - return acos(sin(toRadians(otherCoordinate.latitude)) * sin(toRadians(this.latitude)) + ( - cos(toRadians(otherCoordinate.latitude)) * cos(toRadians(this.latitude)) - * cos(toRadians(otherCoordinate.longitude - this.longitude)) - )) * earthRadius; + public double getLongitude() { + return coordinate.getX(); } } diff --git a/backend/src/main/java/com/mapbefine/mapbefine/location/domain/LocationRepository.java b/backend/src/main/java/com/mapbefine/mapbefine/location/domain/LocationRepository.java index 6f6a932b..a483ece6 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/location/domain/LocationRepository.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/location/domain/LocationRepository.java @@ -1,6 +1,7 @@ package com.mapbefine.mapbefine.location.domain; import java.util.List; +import org.locationtech.jts.geom.Point; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; @@ -11,15 +12,12 @@ public interface LocationRepository extends JpaRepository { @Query( "SELECT l FROM Location l " - + "WHERE ( 6371000 * acos( cos( radians(:#{#current_coordinate.latitude}) ) " - + " * cos( radians( l.coordinate.latitude ) ) " - + " * cos( radians( l.coordinate.longitude ) - radians(:#{#current_coordinate.longitude}) ) " - + " + sin( radians(:#{#current_coordinate.latitude}) ) " - + " * sin( radians( l.coordinate.latitude ) ) ) ) <= :distance" + + "WHERE ST_Contains(ST_Buffer(:coordinate, :distance), l.coordinate.coordinate)" ) List findAllByCoordinateAndDistanceInMeters( - @Param("current_coordinate") Coordinate coordinate, + @Param("coordinate") Point coordinate, @Param("distance") double distance ); + } diff --git a/backend/src/main/java/com/mapbefine/mapbefine/member/application/MemberCommandService.java b/backend/src/main/java/com/mapbefine/mapbefine/member/application/MemberCommandService.java index d7c315f4..f3360468 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/member/application/MemberCommandService.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/member/application/MemberCommandService.java @@ -26,7 +26,7 @@ public void updateInfoById(AuthMember authMember, MemberUpdateRequest request) { validateNicknameDuplicated(nickName); - member.update(nickName); + member.updateNickName(nickName); } private Member findMemberById(Long memberId) { diff --git a/backend/src/main/java/com/mapbefine/mapbefine/member/application/MemberQueryService.java b/backend/src/main/java/com/mapbefine/mapbefine/member/application/MemberQueryService.java index 6e0df34f..261ec15f 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/member/application/MemberQueryService.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/member/application/MemberQueryService.java @@ -14,11 +14,10 @@ import com.mapbefine.mapbefine.topic.domain.Topic; import com.mapbefine.mapbefine.topic.domain.TopicRepository; import com.mapbefine.mapbefine.topic.dto.response.TopicResponse; +import java.util.List; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import java.util.List; - @Service @Transactional(readOnly = true) public class MemberQueryService { @@ -41,21 +40,22 @@ public MemberQueryService( this.topicRepository = topicRepository; } - public MemberDetailResponse findById(Long id) { - Member member = findMemberById(id); + public MemberDetailResponse findMemberDetail(AuthMember authMember) { + Member member = findMemberById(authMember.getMemberId()); return MemberDetailResponse.from(member); } private Member findMemberById(Long id) { return memberRepository.findById(id) + .filter(Member::isNormalStatus) .orElseThrow(() -> new MemberNotFoundException(MemberErrorCode.MEMBER_NOT_FOUND, id)); } - // TODO: 2023/09/13 차단된 or 탈퇴한 사용자 필터링 필요 public List findAll() { return memberRepository.findAll() .stream() + .filter(Member::isNormalStatus) .map(MemberResponse::from) .toList(); } @@ -63,7 +63,8 @@ public List findAll() { public List findAllTopicsInBookmark(AuthMember authMember) { Member member = findMemberById(authMember.getMemberId()); - List bookMarkedTopics = topicRepository.findTopicsByBookmarksMemberIdAndIsDeletedFalse(authMember.getMemberId()); + List bookMarkedTopics = topicRepository.findTopicsByBookmarksMemberId( + authMember.getMemberId()); return bookMarkedTopics.stream() .map(topic -> TopicResponse.from( topic, @@ -73,20 +74,12 @@ public List findAllTopicsInBookmark(AuthMember authMember) { .toList(); } - private List findTopicsInAtlas(Member member) { - return member.getAtlantes() - .stream() - .map(Atlas::getTopic) - .toList(); - } - private boolean isInAtlas(Long memberId, Long topicId) { return atlasRepository.existsByMemberIdAndTopicId(memberId, topicId); } public List findAllTopicsInAtlas(AuthMember authMember) { Member member = findMemberById(authMember.getMemberId()); - List topicsInAtlas = findTopicsInAtlas(member); return topicsInAtlas.stream() @@ -98,12 +91,18 @@ public List findAllTopicsInAtlas(AuthMember authMember) { .toList(); } + private List findTopicsInAtlas(Member member) { + return member.getAtlantes() + .stream() + .map(Atlas::getTopic) + .toList(); + } + private boolean isInBookmark(Long memberId, Long topicId) { return bookmarkRepository.existsByMemberIdAndTopicId(memberId, topicId); } public List findMyAllTopics(AuthMember authMember) { - Long memberId = authMember.getMemberId(); Member member = findMemberById(memberId); diff --git a/backend/src/main/java/com/mapbefine/mapbefine/member/domain/Member.java b/backend/src/main/java/com/mapbefine/mapbefine/member/domain/Member.java index 31ebf243..865e3f0b 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/member/domain/Member.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/member/domain/Member.java @@ -104,12 +104,16 @@ private static String createNicknameSuffix() { .substring(0, DEFAULT_NICKNAME_SUFFIX_LENGTH); } - public void update( + public void updateNickName( String nickName ) { memberInfo = memberInfo.createUpdatedMemberInfo(nickName); } + public void updateStatus(Status status) { + memberInfo = memberInfo.createUpdatedMemberInfo(status); + } + public void addTopic(Topic topic) { createdTopics.add(topic); } @@ -147,14 +151,4 @@ public List getTopicsWithPermissions() { public boolean isNormalStatus() { return memberInfo.getStatus() == Status.NORMAL; } - - public void updateStatus(Status status) { - memberInfo = MemberInfo.of( - memberInfo.getNickName(), - memberInfo.getEmail(), - memberInfo.getImageUrl(), - memberInfo.getRole(), - status - ); - } } diff --git a/backend/src/main/java/com/mapbefine/mapbefine/member/domain/MemberInfo.java b/backend/src/main/java/com/mapbefine/mapbefine/member/domain/MemberInfo.java index ee36d80a..5bedae4b 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/member/domain/MemberInfo.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/member/domain/MemberInfo.java @@ -113,6 +113,11 @@ public MemberInfo createUpdatedMemberInfo(String nickName) { return MemberInfo.of(nickName, this.email, this.imageUrl.getImageUrl(), this.role, this.status); } + public MemberInfo createUpdatedMemberInfo(Status status) { + + return MemberInfo.of(this.nickName, this.email, this.imageUrl.getImageUrl(), this.role, status); + } + public String getImageUrl() { return imageUrl.getImageUrl(); } diff --git a/backend/src/main/java/com/mapbefine/mapbefine/member/dto/response/MemberResponse.java b/backend/src/main/java/com/mapbefine/mapbefine/member/dto/response/MemberResponse.java index 394ee43c..7ce35b2a 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/member/dto/response/MemberResponse.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/member/dto/response/MemberResponse.java @@ -3,10 +3,9 @@ import com.mapbefine.mapbefine.member.domain.Member; import com.mapbefine.mapbefine.member.domain.MemberInfo; -public record MemberResponse ( +public record MemberResponse( Long id, - String nickName, - String email + String nickName ) { public static MemberResponse from(Member member) { @@ -14,8 +13,7 @@ public static MemberResponse from(Member member) { return new MemberResponse( member.getId(), - memberInfo.getNickName(), - memberInfo.getEmail() + memberInfo.getNickName() ); } diff --git a/backend/src/main/java/com/mapbefine/mapbefine/member/exception/MemberErrorCode.java b/backend/src/main/java/com/mapbefine/mapbefine/member/exception/MemberErrorCode.java index 98c42af4..1b22edea 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/member/exception/MemberErrorCode.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/member/exception/MemberErrorCode.java @@ -10,6 +10,7 @@ public enum MemberErrorCode { ILLEGAL_EMAIL_NULL("05002", "이메일은 필수로 입력해야합니다."), ILLEGAL_EMAIL_PATTERN("05003", "올바르지 않은 이메일 형식입니다."), FORBIDDEN_MEMBER_STATUS("05100", "탈퇴 혹은 차단된 회원입니다."), + FORBIDDEN_ACCESS("05101", "해당 회원 정보에 대한 접근 권한이 없습니다."), MEMBER_NOT_FOUND("05400", "존재하지 않는 회원입니다."), ILLEGAL_NICKNAME_ALREADY_EXISTS("05900", "이미 존재하는 닉네임입니다."), ; diff --git a/backend/src/main/java/com/mapbefine/mapbefine/member/presentation/MemberController.java b/backend/src/main/java/com/mapbefine/mapbefine/member/presentation/MemberController.java index a9e63c5e..401ef01b 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/member/presentation/MemberController.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/member/presentation/MemberController.java @@ -13,7 +13,6 @@ import org.springframework.http.ResponseEntity; 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.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @@ -39,9 +38,9 @@ public ResponseEntity> findAllMember() { } @LoginRequired - @GetMapping("/{memberId}") - public ResponseEntity findMemberById(@PathVariable Long memberId) { - MemberDetailResponse response = memberQueryService.findById(memberId); + @GetMapping("/my/profiles") + public ResponseEntity findMyProfile(AuthMember authMember) { + MemberDetailResponse response = memberQueryService.findMemberDetail(authMember); return ResponseEntity.ok(response); } diff --git a/backend/src/main/java/com/mapbefine/mapbefine/permission/application/PermissionCommandService.java b/backend/src/main/java/com/mapbefine/mapbefine/permission/application/PermissionCommandService.java index 62865a7a..1e35500b 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/permission/application/PermissionCommandService.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/permission/application/PermissionCommandService.java @@ -15,11 +15,10 @@ import com.mapbefine.mapbefine.permission.exception.PermissionException.PermissionForbiddenException; import com.mapbefine.mapbefine.topic.domain.Topic; import com.mapbefine.mapbefine.topic.domain.TopicRepository; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - import java.util.List; import java.util.Objects; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; @Service @Transactional @@ -78,7 +77,7 @@ private boolean isNotSelfMember(AuthMember authMember, Member member) { private Topic findTopic(PermissionRequest request) { Long topicId = request.topicId(); - return topicRepository.findByIdAndIsDeletedFalse(topicId) + return topicRepository.findById(topicId) .orElseThrow(() -> new PermissionBadRequestException(ILLEGAL_TOPIC_ID, topicId)); } diff --git a/backend/src/main/java/com/mapbefine/mapbefine/permission/application/PermissionQueryService.java b/backend/src/main/java/com/mapbefine/mapbefine/permission/application/PermissionQueryService.java index 5afb2404..e9602c73 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/permission/application/PermissionQueryService.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/permission/application/PermissionQueryService.java @@ -14,11 +14,10 @@ import com.mapbefine.mapbefine.topic.domain.TopicStatus; import com.mapbefine.mapbefine.topic.exception.TopicErrorCode; import com.mapbefine.mapbefine.topic.exception.TopicException.TopicNotFoundException; +import java.util.List; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import java.util.List; - @Service @Transactional(readOnly = true) public class PermissionQueryService { @@ -43,12 +42,13 @@ public TopicAccessDetailResponse findTopicAccessDetailById(Long topicId) { } private Publicity findTopicPublicityById(Long topicId) { - return topicRepository.findByIdAndIsDeletedFalse(topicId) + return topicRepository.findById(topicId) .map(Topic::getTopicStatus) .map(TopicStatus::getPublicity) .orElseThrow(() -> new TopicNotFoundException(TopicErrorCode.TOPIC_NOT_FOUND, topicId)); } + @Deprecated(since = "2023.10.06") public PermissionMemberDetailResponse findPermissionById(Long permissionId) { Permission permission = permissionRepository.findById(permissionId) .orElseThrow(() -> new PermissionNotFoundException(PERMISSION_NOT_FOUND, permissionId)); diff --git a/backend/src/main/java/com/mapbefine/mapbefine/permission/domain/PermissionRepository.java b/backend/src/main/java/com/mapbefine/mapbefine/permission/domain/PermissionRepository.java index 7d14c061..3824d460 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/permission/domain/PermissionRepository.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/permission/domain/PermissionRepository.java @@ -2,6 +2,9 @@ import java.util.List; 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; public interface PermissionRepository extends JpaRepository { @@ -9,5 +12,12 @@ public interface PermissionRepository extends JpaRepository { boolean existsByTopicIdAndMemberId(Long topicId, Long memberId); - void deleteAllByMemberId(Long memberId); + @Modifying(clearAutomatically = true) + @Query("delete from Permission p where p.member.id = :memberId") + void deleteAllByMemberId(@Param("memberId") Long memberId); + + @Modifying(clearAutomatically = true) + @Query("delete from Permission p where p.topic.id = :topicId") + void deleteAllByTopicId(@Param("topicId") Long topicId); + } diff --git a/backend/src/main/java/com/mapbefine/mapbefine/permission/presentation/PermissionController.java b/backend/src/main/java/com/mapbefine/mapbefine/permission/presentation/PermissionController.java index 188348ea..e26be0e7 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/permission/presentation/PermissionController.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/permission/presentation/PermissionController.java @@ -56,8 +56,7 @@ public ResponseEntity findTopicAccessDetailByTopicId( return ResponseEntity.ok(response); } - // TODO 이 API를 쓰는 곳이 있나? + 결국 특정 회원을 조회하는 건데 어떤 API인지 알기 어렵다.. - // 회원 정보 조회는 /members 에서 하는 걸로 충분하지 않나? 재사용성이 떨어진다. 테스트의 DisplayName도 매칭이 안된다. + @Deprecated(since = "2023.10.06") @LoginRequired @GetMapping("/{permissionId}") public ResponseEntity findPermissionById(@PathVariable Long permissionId) { diff --git a/backend/src/main/java/com/mapbefine/mapbefine/pin/application/PinCommandService.java b/backend/src/main/java/com/mapbefine/mapbefine/pin/application/PinCommandService.java index c91628c1..206fece1 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/pin/application/PinCommandService.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/pin/application/PinCommandService.java @@ -1,6 +1,10 @@ package com.mapbefine.mapbefine.pin.application; import static com.mapbefine.mapbefine.image.exception.ImageErrorCode.IMAGE_FILE_IS_NULL; +import static com.mapbefine.mapbefine.pin.exception.PinCommentErrorCode.FORBIDDEN_PIN_COMMENT_CREATE; +import static com.mapbefine.mapbefine.pin.exception.PinCommentErrorCode.FORBIDDEN_PIN_COMMENT_DELETE; +import static com.mapbefine.mapbefine.pin.exception.PinCommentErrorCode.FORBIDDEN_PIN_COMMENT_UPDATE; +import static com.mapbefine.mapbefine.pin.exception.PinCommentErrorCode.PIN_COMMENT_NOT_FOUND; import static com.mapbefine.mapbefine.pin.exception.PinErrorCode.FORBIDDEN_PIN_CREATE_OR_UPDATE; import static com.mapbefine.mapbefine.pin.exception.PinErrorCode.ILLEGAL_PIN_ID; import static com.mapbefine.mapbefine.pin.exception.PinErrorCode.ILLEGAL_PIN_IMAGE_ID; @@ -16,52 +20,67 @@ import com.mapbefine.mapbefine.member.domain.Member; import com.mapbefine.mapbefine.member.domain.MemberRepository; import com.mapbefine.mapbefine.pin.domain.Pin; +import com.mapbefine.mapbefine.pin.domain.PinComment; +import com.mapbefine.mapbefine.pin.domain.PinCommentRepository; import com.mapbefine.mapbefine.pin.domain.PinImage; import com.mapbefine.mapbefine.pin.domain.PinImageRepository; import com.mapbefine.mapbefine.pin.domain.PinRepository; +import com.mapbefine.mapbefine.pin.dto.request.PinCommentCreateRequest; +import com.mapbefine.mapbefine.pin.dto.request.PinCommentUpdateRequest; import com.mapbefine.mapbefine.pin.dto.request.PinCreateRequest; import com.mapbefine.mapbefine.pin.dto.request.PinImageCreateRequest; import com.mapbefine.mapbefine.pin.dto.request.PinUpdateRequest; +import com.mapbefine.mapbefine.pin.event.PinUpdateEvent; +import com.mapbefine.mapbefine.pin.exception.PinCommentException.PinCommentForbiddenException; +import com.mapbefine.mapbefine.pin.exception.PinCommentException.PinCommentNotFoundException; import com.mapbefine.mapbefine.pin.exception.PinException.PinBadRequestException; import com.mapbefine.mapbefine.pin.exception.PinException.PinForbiddenException; import com.mapbefine.mapbefine.topic.domain.Topic; import com.mapbefine.mapbefine.topic.domain.TopicRepository; import com.mapbefine.mapbefine.topic.exception.TopicException.TopicBadRequestException; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; -import org.springframework.web.multipart.MultipartFile; - import java.util.List; import java.util.NoSuchElementException; import java.util.Objects; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.multipart.MultipartFile; +@Slf4j @Transactional @Service public class PinCommandService { private static final double DUPLICATE_LOCATION_DISTANCE_METERS = 10.0; + private final ApplicationEventPublisher eventPublisher; + private final ImageService imageService; private final PinRepository pinRepository; private final LocationRepository locationRepository; private final TopicRepository topicRepository; private final MemberRepository memberRepository; private final PinImageRepository pinImageRepository; - private final ImageService imageService; + private final PinCommentRepository pinCommentRepository; public PinCommandService( + ApplicationEventPublisher eventPublisher, + ImageService imageService, PinRepository pinRepository, LocationRepository locationRepository, TopicRepository topicRepository, MemberRepository memberRepository, PinImageRepository pinImageRepository, - ImageService imageService + PinCommentRepository pinCommentRepository ) { + this.imageService = imageService; + this.eventPublisher = eventPublisher; this.pinRepository = pinRepository; this.locationRepository = locationRepository; this.topicRepository = topicRepository; this.memberRepository = memberRepository; this.pinImageRepository = pinImageRepository; - this.imageService = imageService; + this.pinCommentRepository = pinCommentRepository; } public long save( @@ -82,8 +101,8 @@ public long save( ); addPinImagesToPin(images, pin); - pinRepository.save(pin); + eventPublisher.publishEvent(new PinUpdateEvent(pin, member)); return pin.getId(); } @@ -100,7 +119,7 @@ private Topic findTopic(Long topicId) { if (Objects.isNull(topicId)) { throw new TopicBadRequestException(ILLEGAL_TOPIC_ID); } - return topicRepository.findByIdAndIsDeletedFalse(topicId) + return topicRepository.findById(topicId) .orElseThrow(() -> new TopicBadRequestException(ILLEGAL_TOPIC_ID)); } @@ -115,8 +134,9 @@ private Member findMember(Long memberId) { private Location findDuplicateOrCreatePinLocation(PinCreateRequest request) { Coordinate coordinate = Coordinate.of(request.latitude(), request.longitude()); - return locationRepository.findAllByCoordinateAndDistanceInMeters(coordinate, - DUPLICATE_LOCATION_DISTANCE_METERS) + return locationRepository.findAllByCoordinateAndDistanceInMeters( + coordinate.getCoordinate(), DUPLICATE_LOCATION_DISTANCE_METERS + ) .stream() .filter(location -> location.isSameAddress(request.address())) .findFirst() @@ -139,14 +159,16 @@ public void update( Long pinId, PinUpdateRequest request ) { + Member member = findMember(authMember.getMemberId()); Pin pin = findPin(pinId); validatePinCreateOrUpdate(authMember, pin.getTopic()); pin.updatePinInfo(request.name(), request.description()); + eventPublisher.publishEvent(new PinUpdateEvent(pin, member)); } private Pin findPin(Long pinId) { - return pinRepository.findByIdAndIsDeletedFalse(pinId) + return pinRepository.findById(pinId) .orElseThrow(() -> new PinBadRequestException(ILLEGAL_PIN_ID)); } @@ -182,7 +204,7 @@ public void removeImageById(AuthMember authMember, Long pinImageId) { } private PinImage findPinImage(Long pinImageId) { - return pinImageRepository.findByIdAndIsDeletedFalse(pinImageId) + return pinImageRepository.findById(pinImageId) .orElseThrow(() -> new PinBadRequestException(ILLEGAL_PIN_IMAGE_ID)); } @@ -193,4 +215,90 @@ private void validatePinCreateOrUpdate(AuthMember authMember, Topic topic) { throw new PinForbiddenException(FORBIDDEN_PIN_CREATE_OR_UPDATE); } + + public Long savePinComment(AuthMember authMember, PinCommentCreateRequest request) { + Pin pin = findPin(request.pinId()); + validatePinCommentCreate(authMember, pin); + Member member = findMember(authMember.getMemberId()); + PinComment pinComment = createPinComment( + pin, + member, + request.parentPinCommentId(), + request.content() + ); + pinCommentRepository.save(pinComment); + + return pinComment.getId(); + } + + private void validatePinCommentCreate(AuthMember authMember, Pin pin) { + if (authMember.canPinCommentCreate(pin.getTopic())) { + return; + } + + throw new PinCommentForbiddenException(FORBIDDEN_PIN_COMMENT_CREATE); + } + + private PinComment createPinComment( + Pin pin, + Member member, + Long parentPinCommentId, + String content + ) { + if (Objects.isNull(parentPinCommentId)) { + return PinComment.ofParentPinComment(pin, member, content); + } + + PinComment parentPinComment = findPinComment(parentPinCommentId); + + return PinComment.ofChildPinComment(pin, parentPinComment, member, content); + } + + public void updatePinComment( + AuthMember member, + Long pinCommentId, + PinCommentUpdateRequest request + ) { + PinComment pinComment = findPinComment(pinCommentId); + + validatePinCommentUpdate(member, pinComment); + + pinComment.updateContent(request.content()); + } + + private void validatePinCommentUpdate(AuthMember authMember, PinComment pinComment) { + if (isPinCommentCreatorOrAdmin(authMember, pinComment)) { + return; + } + + throw new PinCommentForbiddenException(FORBIDDEN_PIN_COMMENT_UPDATE); + } + + private boolean isPinCommentCreatorOrAdmin(AuthMember authMember, PinComment pinComment) { + Long creatorId = pinComment.getCreator().getId(); + + return authMember.isSameMember(creatorId) || authMember.isAdmin(); + } + + private PinComment findPinComment(Long pinCommentId) { + return pinCommentRepository.findById(pinCommentId) + .orElseThrow(() -> new PinCommentNotFoundException(PIN_COMMENT_NOT_FOUND, pinCommentId)); + } + + public void deletePinComment(AuthMember member, Long pinCommentId) { + PinComment pinComment = findPinComment(pinCommentId); + + validatePinCommentDelete(member, pinComment); + + pinCommentRepository.delete(pinComment); + } + + private void validatePinCommentDelete(AuthMember authMember, PinComment pinComment) { + if (isPinCommentCreatorOrAdmin(authMember, pinComment)) { + return; + } + + throw new PinCommentForbiddenException(FORBIDDEN_PIN_COMMENT_DELETE); + } + } diff --git a/backend/src/main/java/com/mapbefine/mapbefine/pin/application/PinQueryService.java b/backend/src/main/java/com/mapbefine/mapbefine/pin/application/PinQueryService.java index 441752d7..746b8f06 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/pin/application/PinQueryService.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/pin/application/PinQueryService.java @@ -5,29 +5,34 @@ import com.mapbefine.mapbefine.auth.domain.AuthMember; import com.mapbefine.mapbefine.pin.domain.Pin; +import com.mapbefine.mapbefine.pin.domain.PinComment; +import com.mapbefine.mapbefine.pin.domain.PinCommentRepository; import com.mapbefine.mapbefine.pin.domain.PinRepository; +import com.mapbefine.mapbefine.pin.dto.response.PinCommentResponse; import com.mapbefine.mapbefine.pin.dto.response.PinDetailResponse; import com.mapbefine.mapbefine.pin.dto.response.PinResponse; import com.mapbefine.mapbefine.pin.exception.PinException.PinForbiddenException; import com.mapbefine.mapbefine.pin.exception.PinException.PinNotFoundException; import com.mapbefine.mapbefine.topic.domain.Topic; +import java.util.List; +import java.util.Objects; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import java.util.List; - @Transactional(readOnly = true) @Service public class PinQueryService { private final PinRepository pinRepository; + private final PinCommentRepository pinCommentRepository; - public PinQueryService(PinRepository pinRepository) { + public PinQueryService(PinRepository pinRepository, PinCommentRepository pinCommentRepository) { + this.pinCommentRepository = pinCommentRepository; this.pinRepository = pinRepository; } public List findAllReadable(AuthMember member) { - return pinRepository.findAllByIsDeletedFalse() + return pinRepository.findAll() .stream() .filter(pin -> member.canRead(pin.getTopic())) .map(PinResponse::from) @@ -35,7 +40,7 @@ public List findAllReadable(AuthMember member) { } public PinDetailResponse findDetailById(AuthMember member, Long pinId) { - Pin pin = pinRepository.findByIdAndIsDeletedFalse(pinId) + Pin pin = pinRepository.findById(pinId) .orElseThrow(() -> new PinNotFoundException(PIN_NOT_FOUND, pinId)); validateReadAuth(member, pin.getTopic()); @@ -51,10 +56,35 @@ private void validateReadAuth(AuthMember member, Topic topic) { } public List findAllPinsByMemberId(AuthMember authMember, Long memberId) { - return pinRepository.findAllByCreatorIdAndIsDeletedFalse(memberId) + return pinRepository.findAllByCreatorId(memberId) .stream() .filter(pin -> authMember.canRead(pin.getTopic())) .map(PinResponse::from) .toList(); } + + public List findAllPinCommentsByPinId(AuthMember member, Long pinId) { + Pin pin = findPin(pinId); + validateReadAuth(member, pin.getTopic()); + + List pinComments = pinCommentRepository.findAllByPinId(pinId); + + return pinComments.stream() + .map(pinComment -> PinCommentResponse.of(pinComment, isCanChangePinComment(member, pinComment))) + .toList(); + } + + private Pin findPin(Long pinId) { + return pinRepository.findById(pinId) + .orElseThrow(() -> new PinNotFoundException(PIN_NOT_FOUND, pinId)); + } + + private boolean isCanChangePinComment(AuthMember member, PinComment pinComment) { + Long creatorId = pinComment.getCreator().getId(); + + return (Objects.nonNull(member.getMemberId()) + && member.isSameMember(creatorId)) + || member.isAdmin(); + } + } diff --git a/backend/src/main/java/com/mapbefine/mapbefine/pin/domain/Pin.java b/backend/src/main/java/com/mapbefine/mapbefine/pin/domain/Pin.java index 8081b913..1229f070 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/pin/domain/Pin.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/pin/domain/Pin.java @@ -24,10 +24,12 @@ import lombok.Getter; import lombok.NoArgsConstructor; import org.hibernate.annotations.ColumnDefault; +import org.hibernate.annotations.Where; @Entity @NoArgsConstructor(access = PROTECTED) @Getter +@Where(clause = "is_deleted = false") public class Pin extends BaseTimeEntity { @Id @@ -46,7 +48,7 @@ public class Pin extends BaseTimeEntity { private Topic topic; @ManyToOne - @JoinColumn(name = "member_id") + @JoinColumn(name = "member_id", nullable = false) private Member creator; @Column(nullable = false) @@ -102,6 +104,10 @@ public void updatePinInfo(String name, String description) { pinInfo = PinInfo.of(name, description); } + public void decreaseTopicPinCount() { + topic.decreasePinCount(); + } + public void copyToTopic(Member creator, Topic topic) { Pin copiedPin = Pin.createPinAssociatedWithLocationAndTopicAndMember( pinInfo.getName(), @@ -132,6 +138,10 @@ public double getLongitude() { return location.getLongitude(); } + public String getDescription() { + return pinInfo.getDescription(); + } + public String getRoadBaseAddress() { Address address = location.getAddress(); return address.getRoadBaseAddress(); diff --git a/backend/src/main/java/com/mapbefine/mapbefine/pin/domain/PinComment.java b/backend/src/main/java/com/mapbefine/mapbefine/pin/domain/PinComment.java new file mode 100644 index 00000000..6981570d --- /dev/null +++ b/backend/src/main/java/com/mapbefine/mapbefine/pin/domain/PinComment.java @@ -0,0 +1,120 @@ +package com.mapbefine.mapbefine.pin.domain; + +import static com.mapbefine.mapbefine.pin.exception.PinCommentErrorCode.ILLEGAL_CONTENT_LENGTH; +import static com.mapbefine.mapbefine.pin.exception.PinCommentErrorCode.ILLEGAL_CONTENT_NULL; +import static com.mapbefine.mapbefine.pin.exception.PinCommentErrorCode.ILLEGAL_PIN_COMMENT_DEPTH; +import static lombok.AccessLevel.PROTECTED; + +import com.mapbefine.mapbefine.common.entity.BaseTimeEntity; +import com.mapbefine.mapbefine.member.domain.Member; +import com.mapbefine.mapbefine.pin.exception.PinCommentException.PinCommentBadRequestException; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.Lob; +import jakarta.persistence.ManyToOne; +import java.util.Objects; +import java.util.Optional; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.hibernate.annotations.OnDelete; +import org.hibernate.annotations.OnDeleteAction; + +@Entity +@NoArgsConstructor(access = PROTECTED) +@Getter +public class PinComment extends BaseTimeEntity { + + private static final int MAX_CONTENT_LENGTH = 1000; + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne + @JoinColumn(name = "pin_id") + private Pin pin; + + @ManyToOne + @JoinColumn(name = "parent_pin_comment_id") + @OnDelete(action = OnDeleteAction.CASCADE) + private PinComment parentPinComment; + + @ManyToOne + @JoinColumn(name = "member_id", nullable = false) + private Member creator; + + @Lob + @Column(nullable = false, length = 1000) + private String content; + + private PinComment( + Pin pin, + PinComment parentPinComment, + Member creator, + String content + ) { + this.pin = pin; + this.parentPinComment = parentPinComment; + this.creator = creator; + this.content = content; + } + + public static PinComment ofParentPinComment( + Pin pin, + Member creator, + String content + ) { + validateContent(content); + + return new PinComment(pin, null, creator, content); + } + + public static PinComment ofChildPinComment( + Pin pin, + PinComment parentPinComment, + Member creator, + String content + ) { + validatePinCommentDepth(parentPinComment); + validateContent(content); + + + return new PinComment(pin, parentPinComment, creator, content); + } + + private static void validatePinCommentDepth(PinComment parentPinComment) { + if (parentPinComment.isParentComment()) { + return; + } + + throw new PinCommentBadRequestException(ILLEGAL_PIN_COMMENT_DEPTH); + } + + private static void validateContent(String content) { + if (Objects.isNull(content)) { + throw new PinCommentBadRequestException(ILLEGAL_CONTENT_NULL); + } + if (content.isBlank() || content.length() > MAX_CONTENT_LENGTH) { + throw new PinCommentBadRequestException(ILLEGAL_CONTENT_LENGTH); + } + } + + public void updateContent(String content) { + validateContent(content); + this.content = content; + } + + public boolean isParentComment() { + return Objects.isNull(parentPinComment); + } + + public Optional getParentPinCommentId() { + return Optional.ofNullable(parentPinComment) + .map(PinComment::getId); + } + +} diff --git a/backend/src/main/java/com/mapbefine/mapbefine/pin/domain/PinCommentRepository.java b/backend/src/main/java/com/mapbefine/mapbefine/pin/domain/PinCommentRepository.java new file mode 100644 index 00000000..5f2256eb --- /dev/null +++ b/backend/src/main/java/com/mapbefine/mapbefine/pin/domain/PinCommentRepository.java @@ -0,0 +1,12 @@ +package com.mapbefine.mapbefine.pin.domain; + +import java.util.List; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface PinCommentRepository extends JpaRepository { + + List findAllByPinId(Long pinId); + +} diff --git a/backend/src/main/java/com/mapbefine/mapbefine/pin/domain/PinImage.java b/backend/src/main/java/com/mapbefine/mapbefine/pin/domain/PinImage.java index 4da68c7d..66a3136e 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/pin/domain/PinImage.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/pin/domain/PinImage.java @@ -14,10 +14,12 @@ import lombok.Getter; import lombok.NoArgsConstructor; import org.hibernate.annotations.ColumnDefault; +import org.hibernate.annotations.Where; @Entity @NoArgsConstructor(access = AccessLevel.PROTECTED) @Getter +@Where(clause = "is_deleted = false") public class PinImage extends BaseTimeEntity { @Id diff --git a/backend/src/main/java/com/mapbefine/mapbefine/pin/domain/PinImageRepository.java b/backend/src/main/java/com/mapbefine/mapbefine/pin/domain/PinImageRepository.java index 818b0d5a..fb10e25d 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/pin/domain/PinImageRepository.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/pin/domain/PinImageRepository.java @@ -1,20 +1,19 @@ package com.mapbefine.mapbefine.pin.domain; +import java.util.List; +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; -import java.util.List; -import java.util.Optional; - @Repository public interface PinImageRepository extends JpaRepository { - Optional findByIdAndIsDeletedFalse(Long pinId); + Optional findById(Long pinId); - List findAllByPinIdAndIsDeletedFalse(Long pinId); + List findAllByPinId(Long pinId); @Modifying(clearAutomatically = true) @Query("update PinImage p set p.isDeleted = true where p.pin.id = :pinId") diff --git a/backend/src/main/java/com/mapbefine/mapbefine/pin/domain/PinRepository.java b/backend/src/main/java/com/mapbefine/mapbefine/pin/domain/PinRepository.java index 3b221b9d..00827a6a 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/pin/domain/PinRepository.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/pin/domain/PinRepository.java @@ -1,5 +1,6 @@ package com.mapbefine.mapbefine.pin.domain; +import java.util.List; import org.springframework.data.jpa.repository.EntityGraph; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Modifying; @@ -7,23 +8,15 @@ import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; -import java.util.List; -import java.util.Optional; - @Repository public interface PinRepository extends JpaRepository { @EntityGraph(attributePaths = {"location", "topic", "creator", "pinImages"}) - List findAllByIsDeletedFalse(); - - Optional findByIdAndIsDeletedFalse(Long pinId); + List findAll(); @EntityGraph(attributePaths = {"location", "topic", "creator", "pinImages"}) List findAllByTopicId(Long topicId); - @EntityGraph(attributePaths = {"location", "topic", "creator", "pinImages"}) - List findAllByCreatorIdAndIsDeletedFalse(Long creatorId); - @EntityGraph(attributePaths = {"location", "topic", "creator", "pinImages"}) List findAllByCreatorId(Long creatorId); diff --git a/backend/src/main/java/com/mapbefine/mapbefine/pin/dto/request/PinCommentCreateRequest.java b/backend/src/main/java/com/mapbefine/mapbefine/pin/dto/request/PinCommentCreateRequest.java new file mode 100644 index 00000000..ceb256b0 --- /dev/null +++ b/backend/src/main/java/com/mapbefine/mapbefine/pin/dto/request/PinCommentCreateRequest.java @@ -0,0 +1,8 @@ +package com.mapbefine.mapbefine.pin.dto.request; + +public record PinCommentCreateRequest( + Long pinId, + Long parentPinCommentId, + String content +) { +} diff --git a/backend/src/main/java/com/mapbefine/mapbefine/pin/dto/request/PinCommentUpdateRequest.java b/backend/src/main/java/com/mapbefine/mapbefine/pin/dto/request/PinCommentUpdateRequest.java new file mode 100644 index 00000000..03e63609 --- /dev/null +++ b/backend/src/main/java/com/mapbefine/mapbefine/pin/dto/request/PinCommentUpdateRequest.java @@ -0,0 +1,6 @@ +package com.mapbefine.mapbefine.pin.dto.request; + +public record PinCommentUpdateRequest( + String content +) { +} diff --git a/backend/src/main/java/com/mapbefine/mapbefine/pin/dto/response/PinCommentResponse.java b/backend/src/main/java/com/mapbefine/mapbefine/pin/dto/response/PinCommentResponse.java new file mode 100644 index 00000000..8de49140 --- /dev/null +++ b/backend/src/main/java/com/mapbefine/mapbefine/pin/dto/response/PinCommentResponse.java @@ -0,0 +1,33 @@ +package com.mapbefine.mapbefine.pin.dto.response; + +import com.mapbefine.mapbefine.member.domain.MemberInfo; +import com.mapbefine.mapbefine.pin.domain.PinComment; +import java.time.LocalDateTime; +import java.util.Optional; + +public record PinCommentResponse( + Long id, + String content, + String creator, + String creatorImageUrl, + Long parentPinCommentId, + boolean canChange, + LocalDateTime updatedAt +) { + + public static PinCommentResponse of(PinComment pinComment, boolean canChange) { + MemberInfo memberInfo = pinComment.getCreator().getMemberInfo(); + Optional parentPinCommentId = pinComment.getParentPinCommentId(); + + return new PinCommentResponse( + pinComment.getId(), + pinComment.getContent(), + memberInfo.getNickName(), + memberInfo.getImageUrl(), + parentPinCommentId.orElse(null), + canChange, + pinComment.getUpdatedAt() + ); + } + +} diff --git a/backend/src/main/java/com/mapbefine/mapbefine/pin/event/PinUpdateEvent.java b/backend/src/main/java/com/mapbefine/mapbefine/pin/event/PinUpdateEvent.java new file mode 100644 index 00000000..b8123309 --- /dev/null +++ b/backend/src/main/java/com/mapbefine/mapbefine/pin/event/PinUpdateEvent.java @@ -0,0 +1,7 @@ +package com.mapbefine.mapbefine.pin.event; + +import com.mapbefine.mapbefine.member.domain.Member; +import com.mapbefine.mapbefine.pin.domain.Pin; + +public record PinUpdateEvent(Pin pin, Member member) { +} diff --git a/backend/src/main/java/com/mapbefine/mapbefine/pin/exception/PinCommentErrorCode.java b/backend/src/main/java/com/mapbefine/mapbefine/pin/exception/PinCommentErrorCode.java new file mode 100644 index 00000000..1cf0cb83 --- /dev/null +++ b/backend/src/main/java/com/mapbefine/mapbefine/pin/exception/PinCommentErrorCode.java @@ -0,0 +1,25 @@ +package com.mapbefine.mapbefine.pin.exception; + +import lombok.Getter; + +@Getter +public enum PinCommentErrorCode { + + ILLEGAL_CONTENT_NULL("11000", "핀 댓글의 내용은 필수로 입력해야합니다."), + ILLEGAL_CONTENT_LENGTH("11001", "핀 댓글의 내용이 최소 1 자에서 최대 1000 자여야 합니다."), + ILLEGAL_PIN_COMMENT_DEPTH("11002", "핀 대댓글에는 대댓글을 달 수 없습니다."), + FORBIDDEN_PIN_COMMENT_CREATE("11003", "핀 댓글을 추가할 권한이 없습니다."), + FORBIDDEN_PIN_COMMENT_UPDATE("11004", "핀 댓글을 수정할 권한이 없습니다."), + FORBIDDEN_PIN_COMMENT_DELETE("11005", "핀 댓글을 삭제할 권한이 없습니다."), + PIN_COMMENT_NOT_FOUND("11006", "존재하지 않는 핀 댓글입니다."), + ; + + private final String code; + private final String message; + + PinCommentErrorCode(String code, String message) { + this.code = code; + this.message = message; + } + +} diff --git a/backend/src/main/java/com/mapbefine/mapbefine/pin/exception/PinCommentException.java b/backend/src/main/java/com/mapbefine/mapbefine/pin/exception/PinCommentException.java new file mode 100644 index 00000000..c4e72923 --- /dev/null +++ b/backend/src/main/java/com/mapbefine/mapbefine/pin/exception/PinCommentException.java @@ -0,0 +1,27 @@ +package com.mapbefine.mapbefine.pin.exception; + +import com.mapbefine.mapbefine.common.exception.BadRequestException; +import com.mapbefine.mapbefine.common.exception.ErrorCode; +import com.mapbefine.mapbefine.common.exception.ForbiddenException; + +public class PinCommentException { + + public static class PinCommentBadRequestException extends BadRequestException { + public PinCommentBadRequestException(PinCommentErrorCode errorCode) { + super(new ErrorCode<>(errorCode.getCode(), errorCode.getMessage())); + } + } + + public static class PinCommentForbiddenException extends ForbiddenException { + public PinCommentForbiddenException(PinCommentErrorCode errorCode) { + super(new ErrorCode<>(errorCode.getCode(), errorCode.getMessage())); + } + } + + public static class PinCommentNotFoundException extends ForbiddenException { + public PinCommentNotFoundException(PinCommentErrorCode errorCode, Long id) { + super(new ErrorCode<>(errorCode.getCode(), errorCode.getMessage(), id)); + } + } + +} diff --git a/backend/src/main/java/com/mapbefine/mapbefine/pin/exception/PinException.java b/backend/src/main/java/com/mapbefine/mapbefine/pin/exception/PinException.java index fa98ffbc..a8afe8bc 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/pin/exception/PinException.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/pin/exception/PinException.java @@ -18,10 +18,10 @@ public PinForbiddenException(PinErrorCode errorCode) { } } - public static class PinNotFoundException extends ForbiddenException { public PinNotFoundException(PinErrorCode errorCode, Long id) { super(new ErrorCode<>(errorCode.getCode(), errorCode.getMessage(), id)); } } + } diff --git a/backend/src/main/java/com/mapbefine/mapbefine/pin/presentation/PinController.java b/backend/src/main/java/com/mapbefine/mapbefine/pin/presentation/PinController.java index d144d889..7effbe81 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/pin/presentation/PinController.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/pin/presentation/PinController.java @@ -4,9 +4,12 @@ import com.mapbefine.mapbefine.common.interceptor.LoginRequired; import com.mapbefine.mapbefine.pin.application.PinCommandService; import com.mapbefine.mapbefine.pin.application.PinQueryService; +import com.mapbefine.mapbefine.pin.dto.request.PinCommentCreateRequest; +import com.mapbefine.mapbefine.pin.dto.request.PinCommentUpdateRequest; import com.mapbefine.mapbefine.pin.dto.request.PinCreateRequest; import com.mapbefine.mapbefine.pin.dto.request.PinImageCreateRequest; import com.mapbefine.mapbefine.pin.dto.request.PinUpdateRequest; +import com.mapbefine.mapbefine.pin.dto.response.PinCommentResponse; import com.mapbefine.mapbefine.pin.dto.response.PinDetailResponse; import com.mapbefine.mapbefine.pin.dto.response.PinResponse; import java.net.URI; @@ -65,6 +68,7 @@ public ResponseEntity update( .build(); } + @Deprecated(since = "2023.10.10 (이미지 삭제 로직 불완전, 사용되지 않는 API)") @LoginRequired @DeleteMapping("/{pinId}") public ResponseEntity delete(AuthMember member, @PathVariable Long pinId) { @@ -121,6 +125,44 @@ public ResponseEntity removeImage(AuthMember member, @PathVariable Long pi return ResponseEntity.noContent().build(); } + @LoginRequired + @PostMapping("/comments") + public ResponseEntity addPinComment(AuthMember member, @RequestBody PinCommentCreateRequest request) { + Long commentId = pinCommandService.savePinComment(member, request); + + return ResponseEntity.created(URI.create("/pins/comments/" + commentId)) + .build(); + } + + @GetMapping("/{pinId}/comments") + public ResponseEntity> findPinCommentByPinId(AuthMember member, @PathVariable Long pinId) { + List allResponse = pinQueryService.findAllPinCommentsByPinId(member, pinId); + + return ResponseEntity.ok(allResponse); + } + + @PutMapping("/comments/{pinCommentId}") + public ResponseEntity updatePinComment( + AuthMember member, + @PathVariable Long pinCommentId, + @RequestBody PinCommentUpdateRequest request + ) { + pinCommandService.updatePinComment(member, pinCommentId, request); + + return ResponseEntity.created(URI.create("/pins/comments/" + pinCommentId)) + .build(); + } + + @DeleteMapping("/comments/{pinCommentId}") + public ResponseEntity removePinComment( + AuthMember member, + @PathVariable Long pinCommentId + ) { + pinCommandService.deletePinComment(member, pinCommentId); + + return ResponseEntity.noContent().build(); + } + } diff --git a/backend/src/main/java/com/mapbefine/mapbefine/topic/application/TopicCommandService.java b/backend/src/main/java/com/mapbefine/mapbefine/topic/application/TopicCommandService.java index f6030259..a40117e9 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/topic/application/TopicCommandService.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/topic/application/TopicCommandService.java @@ -22,14 +22,13 @@ import com.mapbefine.mapbefine.topic.dto.request.TopicUpdateRequest; import com.mapbefine.mapbefine.topic.exception.TopicException.TopicBadRequestException; import com.mapbefine.mapbefine.topic.exception.TopicException.TopicForbiddenException; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; -import org.springframework.web.multipart.MultipartFile; - import java.util.Collection; import java.util.List; import java.util.NoSuchElementException; import java.util.Objects; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.multipart.MultipartFile; @Transactional @Service @@ -94,7 +93,9 @@ private Member findCreatorByAuthMember(AuthMember member) { } return memberRepository.findById(memberId) - .orElseThrow(() -> new NoSuchElementException("findCreatorByAuthMember; member not found; id=" + memberId)); + .orElseThrow( + () -> new NoSuchElementException("findCreatorByAuthMember; member not found; id=" + memberId) + ); } private void copyPinsToTopic( @@ -160,7 +161,7 @@ private Topic convertToTopic(AuthMember member, TopicMergeRequest request) { } private List findAllTopics(List topicIds) { - List findTopics = topicRepository.findByIdInAndIsDeletedFalse(topicIds); + List findTopics = topicRepository.findByIdIn(topicIds); if (topicIds.size() != findTopics.size()) { throw new TopicBadRequestException(ILLEGAL_TOPIC_ID); @@ -196,7 +197,7 @@ public void copyPin(AuthMember member, Long topicId, List pinIds) { } private Topic findTopic(Long topicId) { - return topicRepository.findByIdAndIsDeletedFalse(topicId) + return topicRepository.findById(topicId) .orElseThrow(() -> new TopicBadRequestException(ILLEGAL_TOPIC_ID)); } @@ -222,7 +223,7 @@ public void updateTopicInfo( validateUpdateAuth(member, topic); - topic.updateTopicInfo(request.name(), request.description(), request.image()); + topic.updateTopicInfo(request.name(), request.description()); topic.updateTopicStatus(request.publicity(), request.permissionType()); } @@ -233,15 +234,17 @@ private void validateUpdateAuth(AuthMember member, Topic topic) { throw new TopicForbiddenException(FORBIDDEN_TOPIC_UPDATE); } + @Deprecated(since = "2023.10.06") public void delete(AuthMember member, Long topicId) { Topic topic = findTopic(topicId); - validateDeleteAuth(member, topic); + /// TODO: 2023/10/06 연관관계 다 삭제해야 하는데, 관리자 API와 중복 로직이며 관리자 API에서만 사용됨 pinRepository.deleteAllByTopicId(topicId); topicRepository.deleteById(topicId); } + @Deprecated(since = "2023.10.06") private void validateDeleteAuth(AuthMember member, Topic topic) { if (member.canDelete(topic)) { return; @@ -249,4 +252,13 @@ private void validateDeleteAuth(AuthMember member, Topic topic) { throw new TopicForbiddenException(FORBIDDEN_TOPIC_DELETE); } + public void updateTopicImage(AuthMember member, Long topicId, MultipartFile image) { + Topic topic = findTopic(topicId); + + validateUpdateAuth(member, topic); + + String imageUrl = imageService.upload(image); + topic.updateTopicImageUrl(imageUrl); + } + } diff --git a/backend/src/main/java/com/mapbefine/mapbefine/topic/application/TopicQueryService.java b/backend/src/main/java/com/mapbefine/mapbefine/topic/application/TopicQueryService.java index 5404bfef..053d9012 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/topic/application/TopicQueryService.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/topic/application/TopicQueryService.java @@ -14,13 +14,12 @@ import com.mapbefine.mapbefine.topic.dto.response.TopicResponse; import com.mapbefine.mapbefine.topic.exception.TopicException.TopicForbiddenException; import com.mapbefine.mapbefine.topic.exception.TopicException.TopicNotFoundException; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - import java.util.Comparator; import java.util.List; import java.util.NoSuchElementException; import java.util.Objects; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; @Service @Transactional(readOnly = true) @@ -51,7 +50,7 @@ public List findAllReadable(AuthMember authMember) { } private List getGuestTopicResponses(AuthMember authMember) { - return topicRepository.findAllByIsDeletedFalse() + return topicRepository.findAll() .stream() .filter(authMember::canRead) .map(TopicResponse::fromGuestQuery) @@ -61,7 +60,7 @@ private List getGuestTopicResponses(AuthMember authMember) { private List getUserTopicResponses(AuthMember authMember) { Member member = findMemberById(authMember.getMemberId()); - return topicRepository.findAllByIsDeletedFalse().stream() + return topicRepository.findAll().stream() .filter(authMember::canRead) .map(topic -> TopicResponse.from( topic, @@ -103,7 +102,7 @@ public TopicDetailResponse findDetailById(AuthMember authMember, Long topicId) { } private Topic findTopic(Long id) { - return topicRepository.findByIdAndIsDeletedFalse(id) + return topicRepository.findById(id) .orElseThrow(() -> new TopicNotFoundException(TOPIC_NOT_FOUND, List.of(id))); } @@ -116,7 +115,7 @@ private void validateReadableTopic(AuthMember member, Topic topic) { } public List findDetailsByIds(AuthMember authMember, List ids) { - List topics = topicRepository.findByIdInAndIsDeletedFalse(ids); + List topics = topicRepository.findByIdIn(ids); validateTopicsCount(ids, topics); validateReadableTopics(authMember, topics); @@ -163,7 +162,7 @@ private void validateReadableTopics(AuthMember member, List topics) { public List findAllTopicsByMemberId(AuthMember authMember, Long memberId) { if (Objects.isNull(authMember.getMemberId())) { - return topicRepository.findAllByCreatorIdAndIsDeletedFalse(memberId) + return topicRepository.findAllByCreatorId(memberId) .stream() .filter(authMember::canRead) .map(TopicResponse::fromGuestQuery) @@ -172,7 +171,7 @@ public List findAllTopicsByMemberId(AuthMember authMember, Long m Member member = findMemberById(authMember.getMemberId()); - return topicRepository.findAllByCreatorIdAndIsDeletedFalse(memberId) + return topicRepository.findAllByCreatorId(memberId) .stream() .filter(authMember::canRead) .map(topic -> TopicResponse.from( @@ -193,7 +192,7 @@ public List findAllByOrderByUpdatedAtDesc(AuthMember authMember) private List getUserNewestTopicResponse(AuthMember authMember) { Member member = findMemberById(authMember.getMemberId()); - return topicRepository.findAllByIsDeletedFalseOrderByLastPinUpdatedAtDesc() + return topicRepository.findAllByOrderByLastPinUpdatedAtDesc() .stream() .filter(authMember::canRead) .map(topic -> TopicResponse.from( @@ -205,7 +204,7 @@ private List getUserNewestTopicResponse(AuthMember authMember) { } private List getGuestNewestTopicResponse(AuthMember authMember) { - return topicRepository.findAllByIsDeletedFalseOrderByLastPinUpdatedAtDesc() + return topicRepository.findAllByOrderByLastPinUpdatedAtDesc() .stream() .filter(authMember::canRead) .map(TopicResponse::fromGuestQuery) @@ -220,7 +219,7 @@ public List findAllBestTopics(AuthMember authMember) { } private List getGuestBestTopicResponse(AuthMember authMember) { - return topicRepository.findAllByIsDeletedFalse() + return topicRepository.findAll() .stream() .filter(authMember::canRead) .sorted(Comparator.comparing(Topic::countBookmarks).reversed()) @@ -231,7 +230,7 @@ private List getGuestBestTopicResponse(AuthMember authMember) { private List getUserBestTopicResponse(AuthMember authMember) { Member member = findMemberById(authMember.getMemberId()); - return topicRepository.findAllByIsDeletedFalse() + return topicRepository.findAll() .stream() .filter(authMember::canRead) .sorted(Comparator.comparing(Topic::countBookmarks).reversed()) diff --git a/backend/src/main/java/com/mapbefine/mapbefine/topic/domain/Topic.java b/backend/src/main/java/com/mapbefine/mapbefine/topic/domain/Topic.java index 598e098b..896c7064 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/topic/domain/Topic.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/topic/domain/Topic.java @@ -25,10 +25,12 @@ import lombok.Getter; import lombok.NoArgsConstructor; import org.hibernate.annotations.ColumnDefault; +import org.hibernate.annotations.Where; @Entity @NoArgsConstructor(access = PROTECTED) @Getter +@Where(clause = "is_deleted = false") public class Topic extends BaseTimeEntity { @Id @@ -103,10 +105,9 @@ public static Topic createTopicAssociatedWithCreator( public void updateTopicInfo( String name, - String description, - String imageUrl + String description ) { - this.topicInfo = TopicInfo.of(name, description, imageUrl); + this.topicInfo = TopicInfo.of(name, description, topicInfo.getImageUrl()); } public void updateLastPinUpdatedAt(LocalDateTime lastPinUpdatedAt) { @@ -117,6 +118,10 @@ public void updateTopicStatus(Publicity publicity, PermissionType permissionType topicStatus.update(publicity, permissionType); } + public void updateTopicImageUrl(String imageUrl) { + this.topicInfo = TopicInfo.of(topicInfo.getName(), topicInfo.getDescription(), imageUrl); + } + public int countPins() { return pinCount; } @@ -147,6 +152,10 @@ public void removeImage() { this.topicInfo = topicInfo.removeImage(); } + public void decreasePinCount() { + pinCount--; + } + public void removeBookmark(Bookmark bookmark) { bookmarks.remove(bookmark); bookmarkCount--; diff --git a/backend/src/main/java/com/mapbefine/mapbefine/topic/domain/TopicRepository.java b/backend/src/main/java/com/mapbefine/mapbefine/topic/domain/TopicRepository.java index cbaba31d..480d5967 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/topic/domain/TopicRepository.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/topic/domain/TopicRepository.java @@ -1,5 +1,7 @@ package com.mapbefine.mapbefine.topic.domain; +import java.util.List; +import java.util.Optional; import org.springframework.data.jpa.repository.EntityGraph; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Modifying; @@ -7,32 +9,26 @@ import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; -import java.util.List; -import java.util.Optional; - @Repository public interface TopicRepository extends JpaRepository { @EntityGraph(attributePaths = {"creator", "permissions"}) - Optional findByIdAndIsDeletedFalse(Long id); - - @EntityGraph(attributePaths = {"creator", "permissions"}) - List findAllByIsDeletedFalse(); + Optional findById(Long id); @EntityGraph(attributePaths = {"creator", "permissions"}) - List findByIdInAndIsDeletedFalse(List ids); + List findAll(); @EntityGraph(attributePaths = {"creator", "permissions"}) - List findAllByIsDeletedFalseOrderByLastPinUpdatedAtDesc(); + List findByIdIn(List ids); @EntityGraph(attributePaths = {"creator", "permissions"}) - List findAllByCreatorIdAndIsDeletedFalse(Long creatorId); + List findAllByOrderByLastPinUpdatedAtDesc(); @EntityGraph(attributePaths = {"creator", "permissions"}) List findAllByCreatorId(Long creatorId); @EntityGraph(attributePaths = {"creator", "permissions"}) - List findTopicsByBookmarksMemberIdAndIsDeletedFalse(Long memberId); + List findTopicsByBookmarksMemberId(Long memberId); @Modifying(clearAutomatically = true) @Query("update Topic t set t.isDeleted = true where t.id = :topicId") diff --git a/backend/src/main/java/com/mapbefine/mapbefine/topic/dto/request/TopicUpdateRequest.java b/backend/src/main/java/com/mapbefine/mapbefine/topic/dto/request/TopicUpdateRequest.java index 76bd803a..fabb540f 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/topic/dto/request/TopicUpdateRequest.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/topic/dto/request/TopicUpdateRequest.java @@ -5,7 +5,6 @@ public record TopicUpdateRequest( String name, - String image, String description, Publicity publicity, PermissionType permissionType diff --git a/backend/src/main/java/com/mapbefine/mapbefine/topic/presentation/TopicController.java b/backend/src/main/java/com/mapbefine/mapbefine/topic/presentation/TopicController.java index 7bb8d169..1f833155 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/topic/presentation/TopicController.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/topic/presentation/TopicController.java @@ -78,7 +78,9 @@ public ResponseEntity mergeAndCreate( @LoginRequired @PostMapping("/{topicId}/copy") - public ResponseEntity copyPin(AuthMember member, @PathVariable Long topicId, @RequestParam List pinIds) { + public ResponseEntity copyPin( + AuthMember member, @PathVariable Long topicId, @RequestParam List pinIds + ) { topicCommandService.copyPin(member, topicId, pinIds); return ResponseEntity.ok().build(); @@ -137,6 +139,29 @@ public ResponseEntity update( return ResponseEntity.ok().build(); } + @GetMapping("/bests") + public ResponseEntity> findAllBestTopics(AuthMember authMember) { + List responses = topicQueryService.findAllBestTopics(authMember); + + return ResponseEntity.ok(responses); + } + + @LoginRequired + @PutMapping( + value = "/images/{topicId}", + consumes = MediaType.MULTIPART_FORM_DATA_VALUE + ) + public ResponseEntity deleteImage( + AuthMember authMember, + @PathVariable Long topicId, + @RequestPart MultipartFile image + ) { + topicCommandService.updateTopicImage(authMember, topicId, image); + + return ResponseEntity.ok().build(); + } + + @Deprecated(since = "2023.10.06") @LoginRequired @DeleteMapping("/{topicId}") public ResponseEntity delete(AuthMember member, @PathVariable Long topicId) { @@ -145,11 +170,4 @@ public ResponseEntity delete(AuthMember member, @PathVariable Long topicId return ResponseEntity.noContent().build(); } - @GetMapping("/bests") - public ResponseEntity> findAllBestTopics(AuthMember authMember) { - List responses = topicQueryService.findAllBestTopics(authMember); - - return ResponseEntity.ok(responses); - } - } diff --git a/backend/src/main/resources/config b/backend/src/main/resources/config index 9479615c..d7b073fa 160000 --- a/backend/src/main/resources/config +++ b/backend/src/main/resources/config @@ -1 +1 @@ -Subproject commit 9479615c3c4ce12643814ae88fc264895cbe58da +Subproject commit d7b073fabc2dcf0a9bdb40424917a734cc225985 diff --git a/backend/src/main/resources/data-default.sql b/backend/src/main/resources/data-default.sql index 8d6887eb..4fc4dcc1 100644 --- a/backend/src/main/resources/data-default.sql +++ b/backend/src/main/resources/data-default.sql @@ -5,7 +5,7 @@ VALUES ('dummyMember', 'dummy@gmail.com', 'https://map-befine-official.github.io 1L, 'KAKAO', now(), now()); -INSERT INTO topic (name, image_url, description,가 +INSERT INTO topic (name, image_url, description, permission_type, publicity, member_id, created_at, updated_at, last_pin_updated_at) @@ -14,3 +14,6 @@ VALUES ('dummyTopic', 'https://map-befine-official.github.io/favicon.png', 'desc 1L, now(), now(), now()) ; + +INSERT INTO bookmark (member_id, topic_id) +VALUES (1L, 1L); diff --git a/backend/src/main/resources/logback-spring.xml b/backend/src/main/resources/logback-spring.xml index 944c99dd..d720ab2f 100644 --- a/backend/src/main/resources/logback-spring.xml +++ b/backend/src/main/resources/logback-spring.xml @@ -8,12 +8,16 @@ - - + - + + + + + + - + diff --git a/backend/src/test/java/com/mapbefine/mapbefine/DatabaseCleanup.java b/backend/src/test/java/com/mapbefine/mapbefine/DatabaseCleanup.java index d5883ea4..1e211ee5 100644 --- a/backend/src/test/java/com/mapbefine/mapbefine/DatabaseCleanup.java +++ b/backend/src/test/java/com/mapbefine/mapbefine/DatabaseCleanup.java @@ -14,7 +14,7 @@ public class DatabaseCleanup implements InitializingBean { private static final String TRUNCATE_SQL_MESSAGE = "TRUNCATE TABLE %s"; - private static final String SET_REFERENTIAL_INTEGRITY_SQL_MESSAGE = "SET REFERENTIAL_INTEGRITY %s"; + private static final String SET_REFERENTIAL_INTEGRITY_SQL_MESSAGE = "SET FOREIGN_KEY_CHECKS = %s"; private static final String DISABLE_REFERENTIAL_QUERY = String.format(SET_REFERENTIAL_INTEGRITY_SQL_MESSAGE, false); private static final String ENABLE_REFERENTIAL_QUERY = String.format(SET_REFERENTIAL_INTEGRITY_SQL_MESSAGE, true); diff --git a/backend/src/test/java/com/mapbefine/mapbefine/MapbefineApplicationTests.java b/backend/src/test/java/com/mapbefine/mapbefine/MapbefineApplicationTests.java deleted file mode 100644 index 79c553f4..00000000 --- a/backend/src/test/java/com/mapbefine/mapbefine/MapbefineApplicationTests.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.mapbefine.mapbefine; - -import org.junit.jupiter.api.Test; -import org.springframework.boot.test.context.SpringBootTest; - -@SpringBootTest -class MapbefineApplicationTests { - - @Test - void contextLoads() { - } - -} diff --git a/backend/src/test/java/com/mapbefine/mapbefine/TestDatabaseContainer.java b/backend/src/test/java/com/mapbefine/mapbefine/TestDatabaseContainer.java new file mode 100644 index 00000000..271cdc7f --- /dev/null +++ b/backend/src/test/java/com/mapbefine/mapbefine/TestDatabaseContainer.java @@ -0,0 +1,25 @@ +package com.mapbefine.mapbefine; + +import org.springframework.test.context.DynamicPropertyRegistry; +import org.springframework.test.context.DynamicPropertySource; +import org.testcontainers.containers.MySQLContainer; + + +public abstract class TestDatabaseContainer { + + private static final MySQLContainer mySQLContainer = new MySQLContainer("mysql:8.0.32"); + + static { + mySQLContainer.start(); + } + + @DynamicPropertySource + public static void overrideProps(DynamicPropertyRegistry registry) { + registry.add("spring.datasource.url", mySQLContainer::getJdbcUrl); + registry.add("spring.datasource.username", mySQLContainer::getUsername); + registry.add("spring.datasource.password", mySQLContainer::getPassword); + registry.add("spring.datasource.driver-class-name", mySQLContainer::getDriverClassName); + } + +} + diff --git a/backend/src/test/java/com/mapbefine/mapbefine/admin/application/AdminCommandServiceTest.java b/backend/src/test/java/com/mapbefine/mapbefine/admin/application/AdminCommandServiceTest.java index 0289b727..288eefda 100644 --- a/backend/src/test/java/com/mapbefine/mapbefine/admin/application/AdminCommandServiceTest.java +++ b/backend/src/test/java/com/mapbefine/mapbefine/admin/application/AdminCommandServiceTest.java @@ -1,8 +1,9 @@ package com.mapbefine.mapbefine.admin.application; import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertAll; +import static org.assertj.core.api.SoftAssertions.assertSoftly; +import com.mapbefine.mapbefine.TestDatabaseContainer; import com.mapbefine.mapbefine.atlas.domain.Atlas; import com.mapbefine.mapbefine.atlas.domain.AtlasRepository; import com.mapbefine.mapbefine.bookmark.domain.Bookmark; @@ -26,45 +27,39 @@ import com.mapbefine.mapbefine.pin.domain.PinRepository; import com.mapbefine.mapbefine.topic.TopicFixture; import com.mapbefine.mapbefine.topic.domain.Topic; -import com.mapbefine.mapbefine.topic.domain.TopicInfo; import com.mapbefine.mapbefine.topic.domain.TopicRepository; import org.junit.jupiter.api.BeforeEach; 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.orm.jpa.TestEntityManager; @ServiceTest -class AdminCommandServiceTest { +class AdminCommandServiceTest extends TestDatabaseContainer { @Autowired private AdminCommandService adminCommandService; @Autowired private MemberRepository memberRepository; - @Autowired private TopicRepository topicRepository; - @Autowired private PinRepository pinRepository; - @Autowired private LocationRepository locationRepository; - @Autowired private PinImageRepository pinImageRepository; - @Autowired private AtlasRepository atlasRepository; - @Autowired private PermissionRepository permissionRepository; - @Autowired private BookmarkRepository bookmarkRepository; - private Location location; - private Member admin; + @Autowired + TestEntityManager testEntityManager; + private Member member; private Topic topic; private Pin pin; @@ -72,15 +67,14 @@ class AdminCommandServiceTest { @BeforeEach void setup() { - admin = memberRepository.save(MemberFixture.create("Admin", "admin@naver.com", Role.ADMIN)); member = memberRepository.save(MemberFixture.create("member", "member@gmail.com", Role.USER)); topic = topicRepository.save(TopicFixture.createByName("topic", member)); - location = locationRepository.save(LocationFixture.create()); + Location location = locationRepository.save(LocationFixture.create()); pin = pinRepository.save(PinFixture.create(location, topic, member)); pinImage = pinImageRepository.save(PinImageFixture.create(pin)); } - @DisplayName("Member를 차단(탈퇴시킬)할 경우, Member가 생성한 토픽, 핀, 핀 이미지가 soft-deleting 된다.") + @DisplayName("Member를 차단(탈퇴시킬)할 경우, Member가 생성한 토픽, 핀, 핀 이미지(soft delete)와 연관된 엔티티들을 삭제한다.") @Test void blockMember_Success() { //given @@ -92,7 +86,7 @@ void blockMember_Success() { atlasRepository.save(atlas); permissionRepository.save(permission); - assertAll(() -> { + assertSoftly(softly -> { assertThat(member.getMemberInfo().getStatus()).isEqualTo(Status.NORMAL); assertThat(topic.isDeleted()).isFalse(); assertThat(pin.isDeleted()).isFalse(); @@ -103,18 +97,17 @@ void blockMember_Success() { }); //when + testEntityManager.clear(); adminCommandService.blockMember(member.getId()); //then - Topic deletedTopic = topicRepository.findById(topic.getId()).get(); - Pin deletedPin = pinRepository.findById(pin.getId()).get(); - PinImage deletedPinImage = pinImageRepository.findById(pinImage.getId()).get(); - - assertAll(() -> { - assertThat(member.getMemberInfo().getStatus()).isEqualTo(Status.BLOCKED); - assertThat(deletedTopic.isDeleted()).isTrue(); - assertThat(deletedPin.isDeleted()).isTrue(); - assertThat(deletedPinImage.isDeleted()).isTrue(); + Member blockedMember = memberRepository.findById(member.getId()).get(); + + assertSoftly(softly -> { + assertThat(blockedMember.getMemberInfo().getStatus()).isEqualTo(Status.BLOCKED); + assertThat(topicRepository.existsById(topic.getId())).isFalse(); + assertThat(pinRepository.existsById(pin.getId())).isFalse(); + assertThat(pinImageRepository.existsById(pinImage.getId())).isFalse(); assertThat(bookmarkRepository.existsByMemberIdAndTopicId(member.getId(), topic.getId())).isFalse(); assertThat(atlasRepository.existsByMemberIdAndTopicId(member.getId(), topic.getId())).isFalse(); assertThat(permissionRepository.existsByTopicIdAndMemberId(topic.getId(), member.getId())).isFalse(); @@ -128,21 +121,18 @@ void deleteTopic_Success() { assertThat(topic.isDeleted()).isFalse(); //when - adminCommandService.deleteTopic(topic.getId()); + Long topicId = topic.getId(); + adminCommandService.deleteTopic(topicId); //then - Topic deletedTopic = topicRepository.findById(topic.getId()).get(); - - assertThat(deletedTopic.isDeleted()).isTrue(); + assertThat(topicRepository.existsById(topicId)).isFalse(); } @DisplayName("Admin은 토픽 이미지를 삭제할 수 있다.") @Test void deleteTopicImage_Success() { //given - TopicInfo topicInfo = topic.getTopicInfo(); - - topic.updateTopicInfo(topicInfo.getName(), topicInfo.getDescription(), "https://imageUrl.png"); + topic.updateTopicImageUrl("https://imageUrl.png"); assertThat(topic.getTopicInfo().getImageUrl()).isEqualTo("https://imageUrl.png"); @@ -167,9 +157,7 @@ void deletePin_Success() { adminCommandService.deletePin(pin.getId()); //then - Pin deletedPin = pinRepository.findById(pin.getId()).get(); - - assertThat(deletedPin.isDeleted()).isTrue(); + assertThat(pinRepository.existsById(pin.getId())).isFalse(); } @DisplayName("Admin인 경우, 핀 이미지를 삭제할 수 있다.") @@ -182,9 +170,7 @@ void deletePinImage_Success() { adminCommandService.deletePinImage(pinImage.getId()); //then - PinImage deletedPinImage = pinImageRepository.findById(pinImage.getId()).get(); - - assertThat(deletedPinImage.isDeleted()).isTrue(); + assertThat(pinImageRepository.findById(pinImage.getId())).isEmpty(); } } diff --git a/backend/src/test/java/com/mapbefine/mapbefine/admin/application/AdminQueryServiceTest.java b/backend/src/test/java/com/mapbefine/mapbefine/admin/application/AdminQueryServiceTest.java index a50dd3f0..fb167151 100644 --- a/backend/src/test/java/com/mapbefine/mapbefine/admin/application/AdminQueryServiceTest.java +++ b/backend/src/test/java/com/mapbefine/mapbefine/admin/application/AdminQueryServiceTest.java @@ -1,11 +1,10 @@ package com.mapbefine.mapbefine.admin.application; import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; +import com.mapbefine.mapbefine.TestDatabaseContainer; import com.mapbefine.mapbefine.admin.dto.AdminMemberDetailResponse; import com.mapbefine.mapbefine.admin.dto.AdminMemberResponse; -import com.mapbefine.mapbefine.auth.domain.AuthMember; import com.mapbefine.mapbefine.common.annotation.ServiceTest; import com.mapbefine.mapbefine.location.LocationFixture; import com.mapbefine.mapbefine.location.domain.Location; @@ -14,7 +13,6 @@ import com.mapbefine.mapbefine.member.domain.Member; import com.mapbefine.mapbefine.member.domain.MemberRepository; import com.mapbefine.mapbefine.member.domain.Role; -import com.mapbefine.mapbefine.permission.exception.PermissionException.PermissionForbiddenException; import com.mapbefine.mapbefine.pin.PinFixture; import com.mapbefine.mapbefine.pin.domain.Pin; import com.mapbefine.mapbefine.pin.domain.PinRepository; @@ -29,7 +27,7 @@ import org.springframework.beans.factory.annotation.Autowired; @ServiceTest -class AdminQueryServiceTest { +class AdminQueryServiceTest extends TestDatabaseContainer { @Autowired private AdminQueryService adminQueryService; @@ -72,6 +70,7 @@ void findMemberDetail_Success() { .ignoringFields("updatedAt") .isEqualTo(AdminMemberDetailResponse.of(member, List.of(topic), List.of(pin))); } + @Test @DisplayName("모든 사용자와 관련된 세부 정보를 모두 조회할 수 있다.") void findAllMemberDetails_Success() { diff --git a/backend/src/test/java/com/mapbefine/mapbefine/admin/presentation/AdminControllerTest.java b/backend/src/test/java/com/mapbefine/mapbefine/admin/presentation/AdminControllerTest.java index f46539b2..c15311f3 100644 --- a/backend/src/test/java/com/mapbefine/mapbefine/admin/presentation/AdminControllerTest.java +++ b/backend/src/test/java/com/mapbefine/mapbefine/admin/presentation/AdminControllerTest.java @@ -20,6 +20,7 @@ import org.junit.jupiter.api.Test; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; +import org.springframework.test.web.servlet.result.MockMvcResultMatchers; class AdminControllerTest extends RestDocsIntegration { @@ -74,7 +75,7 @@ class AdminControllerTest extends RestDocsIntegration { private AdminAuthInterceptor adminAuthInterceptor; @BeforeEach - void setAll() throws Exception { + void setAll() { given(adminAuthInterceptor.preHandle(any(), any(), any())).willReturn(true); } @@ -88,10 +89,10 @@ void findAllMemberDetails() throws Exception { given(adminQueryService.findAllMemberDetails()).willReturn(response); - mockMvc.perform( - MockMvcRequestBuilders.get("/admin/members") - .header(AUTHORIZATION, testAuthHeaderProvider.createAuthHeaderById(1L)) - ).andDo(restDocs.document()); + mockMvc.perform(MockMvcRequestBuilders.get("/admin/members") + .header(AUTHORIZATION, "testKey")) + .andExpect(MockMvcResultMatchers.status().isOk()) + .andDo(restDocs.document()); } @DisplayName("멤버 상세 조회") @@ -109,10 +110,10 @@ void findMember() throws Exception { given(adminQueryService.findMemberDetail(any())).willReturn(response); - mockMvc.perform( - MockMvcRequestBuilders.get("/admin/members/1") - .header(AUTHORIZATION, testAuthHeaderProvider.createAuthHeaderById(1L)) - ).andDo(restDocs.document()); + mockMvc.perform(MockMvcRequestBuilders.get("/admin/members/1") + .header(AUTHORIZATION, "testKey")) + .andExpect(MockMvcResultMatchers.status().isOk()) + .andDo(restDocs.document()); } @DisplayName("멤버 차단(블랙리스트)") @@ -120,10 +121,10 @@ void findMember() throws Exception { void deleteMember() throws Exception { doNothing().when(adminCommandService).blockMember(any()); - mockMvc.perform( - MockMvcRequestBuilders.delete("/admin/members/1") - .header(AUTHORIZATION, testAuthHeaderProvider.createAuthHeaderById(1L)) - ).andDo(restDocs.document()); + mockMvc.perform(MockMvcRequestBuilders.delete("/admin/members/1") + .header(AUTHORIZATION, "testKey")) + .andExpect(MockMvcResultMatchers.status().isNoContent()) + .andDo(restDocs.document()); } @DisplayName("토픽 삭제") @@ -131,10 +132,10 @@ void deleteMember() throws Exception { void deleteTopic() throws Exception { doNothing().when(adminCommandService).deleteTopic(any()); - mockMvc.perform( - MockMvcRequestBuilders.delete("/admin/topics/1") - .header(AUTHORIZATION, testAuthHeaderProvider.createAuthHeaderById(1L)) - ).andDo(restDocs.document()); + mockMvc.perform(MockMvcRequestBuilders.delete("/admin/topics/1") + .header(AUTHORIZATION, "testKey")) + .andExpect(MockMvcResultMatchers.status().isNoContent()) + .andDo(restDocs.document()); } @DisplayName("토픽 이미지 삭제") @@ -142,10 +143,10 @@ void deleteTopic() throws Exception { void deleteTopicImage() throws Exception { doNothing().when(adminCommandService).deleteTopicImage(any()); - mockMvc.perform( - MockMvcRequestBuilders.delete("/admin/topics/1/images") - .header(AUTHORIZATION, testAuthHeaderProvider.createAuthHeaderById(1L)) - ).andDo(restDocs.document()); + mockMvc.perform(MockMvcRequestBuilders.delete("/admin/topics/1/images") + .header(AUTHORIZATION, "testKey")) + .andExpect(MockMvcResultMatchers.status().isNoContent()) + .andDo(restDocs.document()); } @DisplayName("핀 삭제") @@ -153,10 +154,10 @@ void deleteTopicImage() throws Exception { void deletePin() throws Exception { doNothing().when(adminCommandService).deletePin(any()); - mockMvc.perform( - MockMvcRequestBuilders.delete("/admin/pins/1") - .header(AUTHORIZATION, testAuthHeaderProvider.createAuthHeaderById(1L)) - ).andDo(restDocs.document()); + mockMvc.perform(MockMvcRequestBuilders.delete("/admin/pins/1") + .header(AUTHORIZATION, "testKey")) + .andExpect(MockMvcResultMatchers.status().isNoContent()) + .andDo(restDocs.document()); } @DisplayName("토픽 이미지 삭제") @@ -164,9 +165,9 @@ void deletePin() throws Exception { void deletePinImage() throws Exception { doNothing().when(adminCommandService).deletePinImage(any()); - mockMvc.perform( - MockMvcRequestBuilders.delete("/admin/pins/images/1") - .header(AUTHORIZATION, testAuthHeaderProvider.createAuthHeaderById(1L)) - ).andDo(restDocs.document()); + mockMvc.perform(MockMvcRequestBuilders.delete("/admin/pins/images/1") + .header(AUTHORIZATION, "testKey")) + .andExpect(MockMvcResultMatchers.status().isNoContent()) + .andDo(restDocs.document()); } } diff --git a/backend/src/test/java/com/mapbefine/mapbefine/atlas/application/AtlasCommandServiceTest.java b/backend/src/test/java/com/mapbefine/mapbefine/atlas/application/AtlasCommandServiceTest.java index ca10b0b6..aaca3d37 100644 --- a/backend/src/test/java/com/mapbefine/mapbefine/atlas/application/AtlasCommandServiceTest.java +++ b/backend/src/test/java/com/mapbefine/mapbefine/atlas/application/AtlasCommandServiceTest.java @@ -3,6 +3,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; +import com.mapbefine.mapbefine.TestDatabaseContainer; import com.mapbefine.mapbefine.atlas.domain.Atlas; import com.mapbefine.mapbefine.atlas.domain.AtlasRepository; import com.mapbefine.mapbefine.atlas.exception.AtlasException.AtlasForbiddenException; @@ -27,7 +28,7 @@ import org.springframework.boot.test.autoconfigure.orm.jpa.TestEntityManager; @ServiceTest -class AtlasCommandServiceTest { +class AtlasCommandServiceTest extends TestDatabaseContainer { @Autowired private TopicRepository topicRepository; diff --git a/backend/src/test/java/com/mapbefine/mapbefine/atlas/application/AtlasQueryServiceTest.java b/backend/src/test/java/com/mapbefine/mapbefine/atlas/application/AtlasQueryServiceTest.java deleted file mode 100644 index b4280b45..00000000 --- a/backend/src/test/java/com/mapbefine/mapbefine/atlas/application/AtlasQueryServiceTest.java +++ /dev/null @@ -1,52 +0,0 @@ -package com.mapbefine.mapbefine.atlas.application; - -import com.mapbefine.mapbefine.atlas.domain.Atlas; -import com.mapbefine.mapbefine.atlas.domain.AtlasRepository; -import com.mapbefine.mapbefine.auth.domain.AuthMember; -import com.mapbefine.mapbefine.auth.domain.member.Admin; -import com.mapbefine.mapbefine.member.MemberFixture; -import com.mapbefine.mapbefine.member.domain.Member; -import com.mapbefine.mapbefine.member.domain.MemberRepository; -import com.mapbefine.mapbefine.member.domain.Role; -import com.mapbefine.mapbefine.topic.TopicFixture; -import com.mapbefine.mapbefine.topic.domain.Topic; -import com.mapbefine.mapbefine.topic.domain.TopicRepository; -import java.util.List; -import org.junit.jupiter.api.BeforeEach; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; - -@DataJpaTest -class AtlasQueryServiceTest { - - @Autowired - private AtlasRepository atlasRepository; - - @Autowired - private MemberRepository memberRepository; - - @Autowired - private TopicRepository topicRepository; - - private AuthMember authMember; - private List topics; - - @BeforeEach - void setUp() { - Member member = memberRepository.save(MemberFixture.create("member", "member@member.com", Role.ADMIN)); - authMember = new Admin(member.getId()); - - createTopics(member); - topics.forEach(topic -> atlasRepository.save(Atlas.createWithAssociatedMember(topic, member))); - } - - private void createTopics(Member member) { - topics = List.of( - TopicFixture.createPublicAndAllMembersTopic(member), - TopicFixture.createPublicAndAllMembersTopic(member), - TopicFixture.createPublicAndAllMembersTopic(member) - ); - topicRepository.saveAll(topics); - } - -} diff --git a/backend/src/test/java/com/mapbefine/mapbefine/atlas/presentation/AtlasControllerTest.java b/backend/src/test/java/com/mapbefine/mapbefine/atlas/presentation/AtlasControllerTest.java index 63a8a14b..86d856b3 100644 --- a/backend/src/test/java/com/mapbefine/mapbefine/atlas/presentation/AtlasControllerTest.java +++ b/backend/src/test/java/com/mapbefine/mapbefine/atlas/presentation/AtlasControllerTest.java @@ -10,6 +10,7 @@ import org.junit.jupiter.api.Test; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; +import org.springframework.test.web.servlet.result.MockMvcResultMatchers; class AtlasControllerTest extends RestDocsIntegration { @@ -23,10 +24,10 @@ void addTopicToAtlas() throws Exception { doNothing().when(atlasCommandService).addTopic(any(), any()); // then - mockMvc.perform( - MockMvcRequestBuilders.post("/atlas/topics?id=1") - .header(AUTHORIZATION, testAuthHeaderProvider.createAuthHeaderById(1L)) - ).andDo(restDocs.document()); + mockMvc.perform(MockMvcRequestBuilders.post("/atlas/topics?id=1") + .header(AUTHORIZATION, testAuthHeaderProvider.createAuthHeaderById(1L))) + .andExpect(MockMvcResultMatchers.status().isCreated()) + .andDo(restDocs.document()); } @Test @@ -35,10 +36,10 @@ void removeTopicFromAtlas() throws Exception { doNothing().when(atlasCommandService).removeTopic(any(), any()); // then - mockMvc.perform( - MockMvcRequestBuilders.delete("/atlas/topics?id=1") - .header(AUTHORIZATION, testAuthHeaderProvider.createAuthHeaderById(1L)) - ).andDo(restDocs.document()); + mockMvc.perform(MockMvcRequestBuilders.delete("/atlas/topics?id=1") + .header(AUTHORIZATION, testAuthHeaderProvider.createAuthHeaderById(1L))) + .andExpect(MockMvcResultMatchers.status().isNoContent()) + .andDo(restDocs.document()); } } diff --git a/backend/src/test/java/com/mapbefine/mapbefine/auth/application/AuthServiceTest.java b/backend/src/test/java/com/mapbefine/mapbefine/auth/application/AuthServiceTest.java new file mode 100644 index 00000000..54375ba8 --- /dev/null +++ b/backend/src/test/java/com/mapbefine/mapbefine/auth/application/AuthServiceTest.java @@ -0,0 +1,95 @@ +package com.mapbefine.mapbefine.auth.application; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import com.mapbefine.mapbefine.TestDatabaseContainer; +import com.mapbefine.mapbefine.admin.application.AdminCommandService; +import com.mapbefine.mapbefine.auth.domain.AuthMember; +import com.mapbefine.mapbefine.common.annotation.ServiceTest; +import com.mapbefine.mapbefine.common.exception.UnauthorizedException; +import com.mapbefine.mapbefine.member.MemberFixture; +import com.mapbefine.mapbefine.member.domain.Member; +import com.mapbefine.mapbefine.member.domain.MemberRepository; +import com.mapbefine.mapbefine.member.domain.Role; +import com.mapbefine.mapbefine.permission.domain.Permission; +import com.mapbefine.mapbefine.permission.domain.PermissionRepository; +import com.mapbefine.mapbefine.topic.TopicFixture; +import com.mapbefine.mapbefine.topic.domain.Topic; +import com.mapbefine.mapbefine.topic.domain.TopicRepository; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.transaction.annotation.Propagation; +import org.springframework.transaction.annotation.Transactional; + +@ServiceTest +class AuthServiceTest extends TestDatabaseContainer { + + @Autowired + private AuthService authService; + @Autowired + private AdminCommandService adminCommandService; + @Autowired + private MemberRepository memberRepository; + @Autowired + private TopicRepository topicRepository; + @Autowired + private PermissionRepository permissionRepository; + private Member member; + private Topic topicWithPermission; + private Topic createdTopic; + + + @BeforeEach + void setUp() { + Member admin = memberRepository.save(MemberFixture.create("admin", "admin@member.com", Role.ADMIN)); + member = memberRepository.save(MemberFixture.create("member", "member1@member.com", Role.USER)); + topicWithPermission = topicRepository.save(TopicFixture.createPrivateAndGroupOnlyTopic(admin)); + createdTopic = topicRepository.save(TopicFixture.createPublicAndAllMembersTopic(admin)); + permissionRepository.save(Permission.createPermissionAssociatedWithTopicAndMember(topicWithPermission, member)); + } + + @Transactional(propagation = Propagation.REQUIRES_NEW) + @Test + @DisplayName("인증된 회원 정보를 가져올 때, 차단 혹은 탈퇴한 회원의 정보는 가져오지 않는다.") + void findAuthMemberByMemberId_Success_notContainingNotNormalMember() { + // given + adminCommandService.blockMember(member.getId()); + + // when + // then + assertThatThrownBy(() -> authService.findAuthMemberByMemberId(member.getId())) + .isInstanceOf(UnauthorizedException.class); + } + + @Transactional(propagation = Propagation.REQUIRES_NEW) + @Test + @DisplayName("인증된 회원 정보를 가져올 때, 삭제된 지도에 대해서는 권한을 부여하지 않는다. (soft delete 반영)") + void findAuthMemberByMemberId_Success_notContainingSoftDeletedPermission() { + // given + adminCommandService.deleteTopic(topicWithPermission.getId()); + + // when + AuthMember authMember = authService.findAuthMemberByMemberId(member.getId()); + + // then + assertThat(authMember.canPinCreateOrUpdate(topicWithPermission)).isFalse(); + } + + @Transactional(propagation = Propagation.REQUIRES_NEW) + @Test + @DisplayName("인증된 회원 정보를 가져올 때, 삭제된 지도에 대해서는 생성자 여부를 확인하지 않는다. (soft delete 반영)") + void findAuthMemberByMemberId_Success_notContainingSoftDeletedCreatedTopic() { + // given + adminCommandService.deleteTopic(createdTopic.getId()); + + // when + AuthMember authMember = authService.findAuthMemberByMemberId(member.getId()); + + // then + assertThat(authMember.canTopicUpdate(createdTopic)).isFalse(); + } + +} diff --git a/backend/src/test/java/com/mapbefine/mapbefine/auth/application/TokenServiceTest.java b/backend/src/test/java/com/mapbefine/mapbefine/auth/application/TokenServiceTest.java index 527f6a2c..199d5d77 100644 --- a/backend/src/test/java/com/mapbefine/mapbefine/auth/application/TokenServiceTest.java +++ b/backend/src/test/java/com/mapbefine/mapbefine/auth/application/TokenServiceTest.java @@ -2,9 +2,11 @@ import static org.assertj.core.api.Assertions.assertThat; +import com.mapbefine.mapbefine.TestDatabaseContainer; import com.mapbefine.mapbefine.auth.domain.token.RefreshToken; import com.mapbefine.mapbefine.auth.domain.token.RefreshTokenRepository; import com.mapbefine.mapbefine.auth.dto.LoginTokens; +import com.mapbefine.mapbefine.common.annotation.ServiceTest; import com.mapbefine.mapbefine.member.MemberFixture; import com.mapbefine.mapbefine.member.domain.Member; import com.mapbefine.mapbefine.member.domain.MemberRepository; @@ -22,9 +24,9 @@ import java.util.Date; import java.util.Optional; -@DataJpaTest +@ServiceTest @TestPropertySource(locations = "classpath:application.yml") -class TokenServiceTest { +class TokenServiceTest extends TestDatabaseContainer { @Autowired private RefreshTokenRepository refreshTokenRepository; diff --git a/backend/src/test/java/com/mapbefine/mapbefine/auth/presentation/LoginControllerTest.java b/backend/src/test/java/com/mapbefine/mapbefine/auth/presentation/LoginControllerTest.java index cf4febe0..7da655ee 100644 --- a/backend/src/test/java/com/mapbefine/mapbefine/auth/presentation/LoginControllerTest.java +++ b/backend/src/test/java/com/mapbefine/mapbefine/auth/presentation/LoginControllerTest.java @@ -2,7 +2,6 @@ import static com.mapbefine.mapbefine.oauth.domain.OauthServerType.KAKAO; -import static org.apache.http.cookie.SM.COOKIE; import static org.mockito.ArgumentMatchers.any; import static org.mockito.BDDMockito.given; @@ -12,12 +11,14 @@ import com.mapbefine.mapbefine.common.RestDocsIntegration; import com.mapbefine.mapbefine.member.dto.response.MemberDetailResponse; import com.mapbefine.mapbefine.oauth.application.OauthService; +import jakarta.servlet.http.Cookie; import java.time.LocalDateTime; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.http.MediaType; import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; +import org.springframework.test.web.servlet.result.MockMvcResultMatchers; class LoginControllerTest extends RestDocsIntegration { @@ -41,9 +42,9 @@ void redirection() throws Exception { ); // when then - mockMvc.perform( - MockMvcRequestBuilders.get("/oauth/kakao") - ).andDo(restDocs.document()); + mockMvc.perform(MockMvcRequestBuilders.get("/oauth/kakao")) + .andExpect(MockMvcResultMatchers.status().isFound()) + .andDo(restDocs.document()); } @Test @@ -69,42 +70,42 @@ void login() throws Exception { given(tokenService.issueTokens(memberDetailResponse.id())).willReturn(loginTokens); // when then - mockMvc.perform( - MockMvcRequestBuilders.get("/oauth/login/kakao") - .param("code", code) - ).andDo(restDocs.document()); + mockMvc.perform(MockMvcRequestBuilders.get("/oauth/login/kakao") + .param("code", code)) + .andExpect(MockMvcResultMatchers.status().isOk()) + .andDo(restDocs.document()); } + @Test @DisplayName("만료된 Access Token과 유효한 RefreshToken인 경우, 새로운 토큰들을 발행한다.") void reissueTokens() throws Exception { - AccessToken expiredAccessToken = new AccessToken("만료된 액세스 토큰"); - String refreshToken = "리프레시 토큰"; + AccessToken expiredAccessToken = new AccessToken("expired-access-token"); + String refreshToken = "refresh-token"; - LoginTokens reissuedTokens = new LoginTokens("재발급된 액세스 토큰", "재발급된 리프레시 토큰"); + LoginTokens reissuedTokens = new LoginTokens("reissued-access-token", "reissued-refresh-token"); given(tokenService.reissueToken(any(), any())).willReturn(reissuedTokens); // then - mockMvc.perform( - MockMvcRequestBuilders.post("/refresh-token") - .header(COOKIE, refreshToken) + mockMvc.perform(MockMvcRequestBuilders.post("/refresh-token") + .cookie(new Cookie("refresh-token", refreshToken)) .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(expiredAccessToken)) - ).andDo(restDocs.document()); + .content(objectMapper.writeValueAsString(expiredAccessToken))) + .andExpect(MockMvcResultMatchers.status().isOk()) + .andDo(restDocs.document()); } @Test - @DisplayName("유효한 Access Token과 유효한 RefreshToken인 경우, RefreshTokenRepository에서 RefreshTokend을 삭제한다") + @DisplayName("유효한 Access Token과 유효한 RefreshToken으로 로그아웃을 요청한다.") void logout() throws Exception { - AccessToken accessToken = new AccessToken("access token"); + AccessToken accessToken = new AccessToken("access-token"); - mockMvc.perform( - MockMvcRequestBuilders.post("/logout") - .header(COOKIE, testAuthHeaderProvider.createRefreshToken()) + mockMvc.perform(MockMvcRequestBuilders.post("/logout") + .cookie(new Cookie("refresh-token", testAuthHeaderProvider.createRefreshToken())) .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(accessToken)) - ).andDo(restDocs.document()); - + .content(objectMapper.writeValueAsString(accessToken))) + .andExpect(MockMvcResultMatchers.status().isNoContent()) + .andDo(restDocs.document()); } } diff --git a/backend/src/test/java/com/mapbefine/mapbefine/bookmark/application/BookmarkCommandServiceTest.java b/backend/src/test/java/com/mapbefine/mapbefine/bookmark/application/BookmarkCommandServiceTest.java index e1329fa8..e49092a8 100644 --- a/backend/src/test/java/com/mapbefine/mapbefine/bookmark/application/BookmarkCommandServiceTest.java +++ b/backend/src/test/java/com/mapbefine/mapbefine/bookmark/application/BookmarkCommandServiceTest.java @@ -3,6 +3,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; +import com.mapbefine.mapbefine.TestDatabaseContainer; import com.mapbefine.mapbefine.auth.domain.AuthMember; import com.mapbefine.mapbefine.bookmark.domain.Bookmark; import com.mapbefine.mapbefine.bookmark.domain.BookmarkRepository; @@ -21,7 +22,7 @@ import org.springframework.boot.test.autoconfigure.orm.jpa.TestEntityManager; @ServiceTest -class BookmarkCommandServiceTest { +class BookmarkCommandServiceTest extends TestDatabaseContainer { @Autowired private BookmarkCommandService bookmarkCommandService; diff --git a/backend/src/test/java/com/mapbefine/mapbefine/bookmark/presentation/BookmarkControllerTest.java b/backend/src/test/java/com/mapbefine/mapbefine/bookmark/presentation/BookmarkControllerTest.java index 36ff312e..a509b53a 100644 --- a/backend/src/test/java/com/mapbefine/mapbefine/bookmark/presentation/BookmarkControllerTest.java +++ b/backend/src/test/java/com/mapbefine/mapbefine/bookmark/presentation/BookmarkControllerTest.java @@ -11,23 +11,23 @@ import org.junit.jupiter.api.Test; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; +import org.springframework.test.web.servlet.result.MockMvcResultMatchers; class BookmarkControllerTest extends RestDocsIntegration { @MockBean private BookmarkCommandService bookmarkCommandService; - @Test @DisplayName("토픽을 회원의 즐겨찾기에 추가") void addTopicInBookmark() throws Exception { given(bookmarkCommandService.addTopicInBookmark(any(), any())).willReturn(1L); - mockMvc.perform( - MockMvcRequestBuilders.post("/bookmarks/topics") + mockMvc.perform(MockMvcRequestBuilders.post("/bookmarks/topics") .queryParam("id", String.valueOf(1)) - .header(AUTHORIZATION, testAuthHeaderProvider.createAuthHeaderById(1L)) - ).andDo(restDocs.document()); + .header(AUTHORIZATION, testAuthHeaderProvider.createAuthHeaderById(1L))) + .andExpect(MockMvcResultMatchers.status().isCreated()) + .andDo(restDocs.document()); } @Test @@ -35,11 +35,11 @@ void addTopicInBookmark() throws Exception { void deleteTopicInBookmark() throws Exception { doNothing().when(bookmarkCommandService).deleteTopicInBookmark(any(), any()); - mockMvc.perform( - MockMvcRequestBuilders.delete("/bookmarks/topics") + mockMvc.perform(MockMvcRequestBuilders.delete("/bookmarks/topics") .queryParam("id", String.valueOf(1L)) - .header(AUTHORIZATION, testAuthHeaderProvider.createAuthHeaderById(1L)) - ).andDo(restDocs.document()); + .header(AUTHORIZATION, testAuthHeaderProvider.createAuthHeaderById(1L))) + .andExpect(MockMvcResultMatchers.status().isNoContent()) + .andDo(restDocs.document()); } } diff --git a/backend/src/test/java/com/mapbefine/mapbefine/common/IntegrationTest.java b/backend/src/test/java/com/mapbefine/mapbefine/common/IntegrationTest.java index 9a91e94a..cce9d185 100644 --- a/backend/src/test/java/com/mapbefine/mapbefine/common/IntegrationTest.java +++ b/backend/src/test/java/com/mapbefine/mapbefine/common/IntegrationTest.java @@ -1,6 +1,7 @@ package com.mapbefine.mapbefine.common; import com.mapbefine.mapbefine.DatabaseCleanup; +import com.mapbefine.mapbefine.TestDatabaseContainer; import io.restassured.RestAssured; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; @@ -9,7 +10,7 @@ import org.springframework.boot.test.web.server.LocalServerPort; @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) -public class IntegrationTest { +public class IntegrationTest extends TestDatabaseContainer { @Autowired protected TestAuthHeaderProvider testAuthHeaderProvider; diff --git a/backend/src/test/java/com/mapbefine/mapbefine/common/RestDocsIntegration.java b/backend/src/test/java/com/mapbefine/mapbefine/common/RestDocsIntegration.java index 633c3876..690af3d8 100644 --- a/backend/src/test/java/com/mapbefine/mapbefine/common/RestDocsIntegration.java +++ b/backend/src/test/java/com/mapbefine/mapbefine/common/RestDocsIntegration.java @@ -5,6 +5,7 @@ import static org.springframework.restdocs.operation.preprocess.Preprocessors.prettyPrint; import com.fasterxml.jackson.databind.ObjectMapper; +import com.mapbefine.mapbefine.TestDatabaseContainer; import com.mapbefine.mapbefine.common.interceptor.AuthInterceptor; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.extension.ExtendWith; @@ -26,7 +27,7 @@ @SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) @AutoConfigureMockMvc @AutoConfigureRestDocs -public abstract class RestDocsIntegration { +public abstract class RestDocsIntegration extends TestDatabaseContainer { @Autowired protected ObjectMapper objectMapper; diff --git a/backend/src/test/java/com/mapbefine/mapbefine/common/annotation/RepositoryTest.java b/backend/src/test/java/com/mapbefine/mapbefine/common/annotation/RepositoryTest.java new file mode 100644 index 00000000..0adceb33 --- /dev/null +++ b/backend/src/test/java/com/mapbefine/mapbefine/common/annotation/RepositoryTest.java @@ -0,0 +1,21 @@ +package com.mapbefine.mapbefine.common.annotation; + +import com.mapbefine.mapbefine.TestDatabaseContainer; +import com.mapbefine.mapbefine.common.config.JpaConfig; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; +import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase.Replace; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.context.annotation.Import; + +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Import(JpaConfig.class) +@DataJpaTest +@AutoConfigureTestDatabase(replace = Replace.NONE) +public @interface RepositoryTest { + +} diff --git a/backend/src/test/java/com/mapbefine/mapbefine/common/annotation/ServiceTest.java b/backend/src/test/java/com/mapbefine/mapbefine/common/annotation/ServiceTest.java index 7993e891..9d9a9e0c 100644 --- a/backend/src/test/java/com/mapbefine/mapbefine/common/annotation/ServiceTest.java +++ b/backend/src/test/java/com/mapbefine/mapbefine/common/annotation/ServiceTest.java @@ -5,6 +5,8 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; +import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase.Replace; import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; import org.springframework.context.annotation.ComponentScan.Filter; import org.springframework.context.annotation.FilterType; @@ -14,6 +16,7 @@ @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Import(JpaConfig.class) +@AutoConfigureTestDatabase(replace = Replace.NONE) @DataJpaTest( includeFilters = { @Filter(type = FilterType.ANNOTATION, value = Service.class), @@ -25,9 +28,10 @@ ), @Filter( type = FilterType.REGEX, - pattern = "com.mapbefine.mapbefine.auth.application.*" + pattern = "com.mapbefine.mapbefine.auth.application.TokenService" ) } ) public @interface ServiceTest { + } diff --git a/backend/src/test/java/com/mapbefine/mapbefine/history/application/PinHistoryCommandServiceTest.java b/backend/src/test/java/com/mapbefine/mapbefine/history/application/PinHistoryCommandServiceTest.java new file mode 100644 index 00000000..a1348174 --- /dev/null +++ b/backend/src/test/java/com/mapbefine/mapbefine/history/application/PinHistoryCommandServiceTest.java @@ -0,0 +1,88 @@ +package com.mapbefine.mapbefine.history.application; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.mapbefine.mapbefine.TestDatabaseContainer; +import com.mapbefine.mapbefine.common.annotation.ServiceTest; +import com.mapbefine.mapbefine.history.domain.PinHistory; +import com.mapbefine.mapbefine.history.domain.PinHistoryRepository; +import com.mapbefine.mapbefine.location.LocationFixture; +import com.mapbefine.mapbefine.location.domain.Location; +import com.mapbefine.mapbefine.location.domain.LocationRepository; +import com.mapbefine.mapbefine.member.MemberFixture; +import com.mapbefine.mapbefine.member.domain.Member; +import com.mapbefine.mapbefine.member.domain.MemberRepository; +import com.mapbefine.mapbefine.member.domain.Role; +import com.mapbefine.mapbefine.pin.PinFixture; +import com.mapbefine.mapbefine.pin.domain.Pin; +import com.mapbefine.mapbefine.pin.domain.PinRepository; +import com.mapbefine.mapbefine.pin.event.PinUpdateEvent; +import com.mapbefine.mapbefine.topic.TopicFixture; +import com.mapbefine.mapbefine.topic.domain.Topic; +import com.mapbefine.mapbefine.topic.domain.TopicRepository; +import java.time.LocalDateTime; +import java.util.List; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationEventPublisher; + +@ServiceTest +class PinHistoryCommandServiceTest extends TestDatabaseContainer { + + @Autowired + private PinHistoryRepository pinHistoryRepository; + @Autowired + private TopicRepository topicRepository; + @Autowired + private LocationRepository locationRepository; + @Autowired + private PinRepository pinRepository; + @Autowired + private MemberRepository memberRepository; + @Autowired + private ApplicationEventPublisher applicationEventPublisher; + + @Test + @DisplayName("핀 변경 이벤트가 발생하면, 핀을 수정한 사람, 핀 정보를 포함한 정보 이력을 저장한다.") + void saveHistory_Success() { + // given + Member member = memberRepository.save(MemberFixture.create("핀 변경한 사람", "pinUpdateBy@gmail.com", Role.USER)); + Topic topic = topicRepository.save(TopicFixture.createPublicAndAllMembersTopic(member)); + Location location = locationRepository.save(LocationFixture.create()); + Pin pin = pinRepository.save(PinFixture.create(location, topic, member)); + + // when + applicationEventPublisher.publishEvent(new PinUpdateEvent(pin, member)); + + // then + List histories = pinHistoryRepository.findAllByPinId(pin.getId()); + PinHistory expected = new PinHistory(pin, member); + PinHistory actual = histories.get(0); + assertThat(actual).usingRecursiveComparison() + .ignoringFields("id") + .ignoringFieldsOfTypes(LocalDateTime.class) + .isEqualTo(expected); + } + + @Test + @DisplayName("핀 정보 이력에는 해당 정보가 수정된 일시를 함께 저장한다.") + void saveHistory_Success_createdAt() { + // given + Member member = memberRepository.save(MemberFixture.create("핀 변경한 사람", "pinUpdateBy@gmail.com", Role.USER)); + Topic topic = topicRepository.save(TopicFixture.createPublicAndAllMembersTopic(member)); + Location location = locationRepository.save(LocationFixture.create()); + Pin pin = pinRepository.save(PinFixture.create(location, topic, member)); + + // when + applicationEventPublisher.publishEvent(new PinUpdateEvent(pin, member)); + + // then + List histories = pinHistoryRepository.findAllByPinId(pin.getId()); + LocalDateTime expectedPinUpdatedAt = pin.getUpdatedAt(); + PinHistory actual = histories.get(0); + LocalDateTime actualPinUpdatedAt = actual.getPinUpdatedAt(); + assertThat(expectedPinUpdatedAt).isEqualTo(actualPinUpdatedAt); + } + +} diff --git a/backend/src/test/java/com/mapbefine/mapbefine/image/FileFixture.java b/backend/src/test/java/com/mapbefine/mapbefine/image/FileFixture.java index 47e5edcd..c8f0bea4 100644 --- a/backend/src/test/java/com/mapbefine/mapbefine/image/FileFixture.java +++ b/backend/src/test/java/com/mapbefine/mapbefine/image/FileFixture.java @@ -1,16 +1,17 @@ package com.mapbefine.mapbefine.image; +import static org.springframework.http.MediaType.IMAGE_PNG_VALUE; + import org.springframework.mock.web.MockMultipartFile; -import org.springframework.web.multipart.MultipartFile; public class FileFixture { - public static MultipartFile createFile() { + public static MockMultipartFile createFile() { return new MockMultipartFile( - "test", - "test.png", - "img/png", - "image".getBytes() + "image", + "image.png", + IMAGE_PNG_VALUE, + "data".getBytes() ); } diff --git a/backend/src/test/java/com/mapbefine/mapbefine/location/application/LocationQueryServiceTest.java b/backend/src/test/java/com/mapbefine/mapbefine/location/application/LocationQueryServiceTest.java index f97542e6..b35eb8c8 100644 --- a/backend/src/test/java/com/mapbefine/mapbefine/location/application/LocationQueryServiceTest.java +++ b/backend/src/test/java/com/mapbefine/mapbefine/location/application/LocationQueryServiceTest.java @@ -4,6 +4,7 @@ import static com.mapbefine.mapbefine.location.LocationFixture.BASE_COORDINATE; import static org.assertj.core.api.Assertions.assertThat; +import com.mapbefine.mapbefine.TestDatabaseContainer; import com.mapbefine.mapbefine.auth.domain.AuthMember; import com.mapbefine.mapbefine.auth.domain.member.Admin; import com.mapbefine.mapbefine.common.annotation.ServiceTest; @@ -16,6 +17,8 @@ import com.mapbefine.mapbefine.member.domain.MemberRepository; import com.mapbefine.mapbefine.member.domain.Role; import com.mapbefine.mapbefine.pin.PinFixture; +import com.mapbefine.mapbefine.pin.domain.Pin; +import com.mapbefine.mapbefine.pin.domain.PinRepository; import com.mapbefine.mapbefine.topic.TopicFixture; import com.mapbefine.mapbefine.topic.domain.Topic; import com.mapbefine.mapbefine.topic.domain.TopicRepository; @@ -30,7 +33,7 @@ import org.springframework.beans.factory.annotation.Autowired; @ServiceTest -class LocationQueryServiceTest { +class LocationQueryServiceTest extends TestDatabaseContainer { @Autowired private LocationRepository locationRepository; @@ -38,6 +41,8 @@ class LocationQueryServiceTest { private MemberRepository memberRepository; @Autowired private TopicRepository topicRepository; + @Autowired + private PinRepository pinRepository; @Autowired private LocationQueryService locationQueryService; @@ -56,9 +61,9 @@ void setup() { locationRepository.save(allPinsLocation); topics = List.of( - createAndSaveTopic("준팍의 또간집", 1), - createAndSaveTopic("도이의 또간집", 2), - createAndSaveTopic("패트릭의 또간집", 3) + createAndSaveTopic("준팍의 또간집", 2), + createAndSaveTopic("도이의 또간집", 3), + createAndSaveTopic("패트릭의 또간집", 4) ); } @@ -68,12 +73,13 @@ private Topic createAndSaveTopic(String topicName, int pinCounts) { for (int i = 0; i < pinCounts; i++) { PinFixture.create(allPinsLocation, topic, member); } + return topicRepository.save(topic); } @Test @DisplayName("주어진 좌표의 3KM 이내 Topic들을 Pin 개수의 내림차순으로 정렬하여 조회한다.") - void findNearbyTopicsSortedByPinCount() { + void findNearbyTopicsSortedByPinCount_Success() { // given Coordinate baseCoordinate = BASE_COORDINATE; @@ -93,4 +99,34 @@ void findNearbyTopicsSortedByPinCount() { assertThat(currentTopics).isEqualTo(expected); } + @Test + @DisplayName("반경 내 핀을 찾을 때, soft delete 된 핀은 제외한다.") + void findNearbyTopicsSortedByPinCount_Success_notContainingSoftDeletedPins() { + // given + Coordinate baseCoordinate = BASE_COORDINATE; + Topic second = topics.get(1); + deletePins(second, 2); + + // when + List currentTopics = locationQueryService.findNearbyTopicsSortedByPinCount( + authMember, + baseCoordinate.getLatitude(), + baseCoordinate.getLongitude() + ); + + // then + assertThat(currentTopics) + .extracting("id") + .containsExactly(topics.get(2).getId(), topics.get(0).getId(), topics.get(1).getId()); + } + + private void deletePins(Topic topic, int deleteCounts) { + /// TODO: 2023/10/05 Topic의 pinCount를 줄이는 로직을 삭제 로직과 통합하지 못해 테스트에서 세부 구현이 드러나는 문제가 있음 + for (int i = 0; i < deleteCounts; i++) { + Pin delete = topic.getPins().get(i); + delete.decreaseTopicPinCount(); + pinRepository.deleteById(delete.getId()); + } + } + } diff --git a/backend/src/test/java/com/mapbefine/mapbefine/location/domain/CoordinateTest.java b/backend/src/test/java/com/mapbefine/mapbefine/location/domain/CoordinateTest.java index a4018d53..a72d9b14 100644 --- a/backend/src/test/java/com/mapbefine/mapbefine/location/domain/CoordinateTest.java +++ b/backend/src/test/java/com/mapbefine/mapbefine/location/domain/CoordinateTest.java @@ -1,17 +1,12 @@ package com.mapbefine.mapbefine.location.domain; import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.assertj.core.api.AssertionsForClassTypes.assertThat; import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; -import com.mapbefine.mapbefine.location.LocationFixture; import com.mapbefine.mapbefine.location.exception.LocationException.LocationBadRequestException; -import java.util.stream.Stream; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.Arguments; -import org.junit.jupiter.params.provider.MethodSource; import org.junit.jupiter.params.provider.ValueSource; class CoordinateTest { @@ -51,34 +46,4 @@ void createCoordinate_FailByInvalidLongitude(double longitude) { } - @ParameterizedTest - @MethodSource(value = "calculateDistanceProvider") - @DisplayName("좌표 사이의 거리를 계산한다.") - void calculateDistance(Coordinate coordinate, Coordinate other, long expected) { - // when - double result = coordinate.calculateDistanceInMeters(other); - - // then - assertThat(Math.floor(result)).isEqualTo(expected); - } - - static Stream calculateDistanceProvider() { - return Stream.of( - Arguments.of( - LocationFixture.BASE_COORDINATE, - LocationFixture.SEVEN_METER, - 7 - ), - Arguments.of( - LocationFixture.BASE_COORDINATE, - LocationFixture.EIGHTEEN_FORTY_SIX_METER, - 1846 - ), - Arguments.of( - LocationFixture.BASE_COORDINATE, - LocationFixture.SIXTY_FOUR_THIRTY_METER, - 6430 - ) - ); - } } diff --git a/backend/src/test/java/com/mapbefine/mapbefine/location/presentation/LocationControllerTest.java b/backend/src/test/java/com/mapbefine/mapbefine/location/presentation/LocationControllerTest.java index b7cb901b..0ff62389 100644 --- a/backend/src/test/java/com/mapbefine/mapbefine/location/presentation/LocationControllerTest.java +++ b/backend/src/test/java/com/mapbefine/mapbefine/location/presentation/LocationControllerTest.java @@ -16,6 +16,7 @@ import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.http.MediaType; import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; +import org.springframework.test.web.servlet.result.MockMvcResultMatchers; class LocationControllerTest extends RestDocsIntegration { @@ -63,13 +64,13 @@ void findNearbyTopicsSortedByPinCount() throws Exception { .willReturn(responses); //then - mockMvc.perform( - MockMvcRequestBuilders.get("/locations/bests") + mockMvc.perform(MockMvcRequestBuilders.get("/locations/bests") .header(AUTHORIZATION, testAuthHeaderProvider.createAuthHeaderById(1L)) .contentType(MediaType.APPLICATION_JSON) .param("latitude", String.valueOf(latitude)) - .param("longitude", String.valueOf(longitude)) - ).andDo(restDocs.document()); + .param("longitude", String.valueOf(longitude))) + .andExpect(MockMvcResultMatchers.status().isOk()) + .andDo(restDocs.document()); } } diff --git a/backend/src/test/java/com/mapbefine/mapbefine/member/MemberIntegrationTest.java b/backend/src/test/java/com/mapbefine/mapbefine/member/MemberIntegrationTest.java index aaf288b2..fb3fd938 100644 --- a/backend/src/test/java/com/mapbefine/mapbefine/member/MemberIntegrationTest.java +++ b/backend/src/test/java/com/mapbefine/mapbefine/member/MemberIntegrationTest.java @@ -79,8 +79,7 @@ void findAllMember() { .then().log().all() .extract(); - List memberResponses = response.as(new TypeRef<>() { - }); + List memberResponses = response.as(new TypeRef<>() {}); // then assertThat(response.statusCode()).isEqualTo(HttpStatus.OK.value()); @@ -94,13 +93,13 @@ void findAllMember() { } @Test - @DisplayName("회원을 단일 조회한다.") - void findMemberById() { + @DisplayName("로그인 회원의 상세 정보를 단일 조회한다.") + void findMyProfile() { // given, when ExtractableResponse response = given().log().all() .header(AUTHORIZATION, user1AuthHeader) .contentType(MediaType.APPLICATION_JSON_VALUE) - .when().get("/members/" + user1.getId()) + .when().get("/members/my/profiles") .then().log().all() .extract(); @@ -192,5 +191,5 @@ void updateMemberInfo_Success() { // then assertThat(response.statusCode()).isEqualTo(HttpStatus.OK.value()); } - + } diff --git a/backend/src/test/java/com/mapbefine/mapbefine/member/application/MemberCommandServiceTest.java b/backend/src/test/java/com/mapbefine/mapbefine/member/application/MemberCommandServiceTest.java index 388a52b9..be3ae613 100644 --- a/backend/src/test/java/com/mapbefine/mapbefine/member/application/MemberCommandServiceTest.java +++ b/backend/src/test/java/com/mapbefine/mapbefine/member/application/MemberCommandServiceTest.java @@ -4,6 +4,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; +import com.mapbefine.mapbefine.TestDatabaseContainer; import com.mapbefine.mapbefine.auth.domain.AuthMember; import com.mapbefine.mapbefine.auth.domain.member.User; import com.mapbefine.mapbefine.common.annotation.ServiceTest; @@ -22,7 +23,7 @@ import org.springframework.beans.factory.annotation.Autowired; @ServiceTest -class MemberCommandServiceTest { +class MemberCommandServiceTest extends TestDatabaseContainer { @Autowired private MemberCommandService memberCommandService; diff --git a/backend/src/test/java/com/mapbefine/mapbefine/member/application/MemberQueryServiceTest.java b/backend/src/test/java/com/mapbefine/mapbefine/member/application/MemberQueryServiceTest.java index 2e0d8a4c..3b7a4f85 100644 --- a/backend/src/test/java/com/mapbefine/mapbefine/member/application/MemberQueryServiceTest.java +++ b/backend/src/test/java/com/mapbefine/mapbefine/member/application/MemberQueryServiceTest.java @@ -1,12 +1,15 @@ package com.mapbefine.mapbefine.member.application; +import static com.mapbefine.mapbefine.member.domain.Role.USER; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; +import com.mapbefine.mapbefine.TestDatabaseContainer; import com.mapbefine.mapbefine.atlas.domain.Atlas; import com.mapbefine.mapbefine.atlas.domain.AtlasRepository; import com.mapbefine.mapbefine.auth.domain.AuthMember; import com.mapbefine.mapbefine.auth.domain.member.Admin; +import com.mapbefine.mapbefine.auth.domain.member.User; import com.mapbefine.mapbefine.bookmark.domain.Bookmark; import com.mapbefine.mapbefine.bookmark.domain.BookmarkRepository; import com.mapbefine.mapbefine.common.annotation.ServiceTest; @@ -28,6 +31,7 @@ import com.mapbefine.mapbefine.topic.domain.Topic; import com.mapbefine.mapbefine.topic.domain.TopicRepository; import com.mapbefine.mapbefine.topic.dto.response.TopicResponse; +import java.util.Collections; import java.util.List; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; @@ -35,7 +39,7 @@ import org.springframework.beans.factory.annotation.Autowired; @ServiceTest -class MemberQueryServiceTest { +class MemberQueryServiceTest extends TestDatabaseContainer { @Autowired private MemberQueryService memberQueryService; @@ -81,10 +85,10 @@ private void createTopics(Member member) { void findAllMember() { // given Member member2 = memberRepository.save( - MemberFixture.create("member2", "member2@member.com", Role.USER) + MemberFixture.create("member2", "member2@member.com", USER) ); Member member3 = memberRepository.save( - MemberFixture.create("member3", "member3@member.com", Role.USER) + MemberFixture.create("member3", "member3@member.com", USER) ); // when @@ -100,15 +104,11 @@ void findAllMember() { } @Test - @DisplayName("회원을 단일 조회한다.") - void findMemberById() { + @DisplayName("로그인 회원의 상세 정보를 조회한다.") + void findMyProfile() { // given - Member member = memberRepository.save( - MemberFixture.create("member", "member@naver.com", Role.USER) - ); - // when - MemberDetailResponse response = memberQueryService.findById(member.getId()); + MemberDetailResponse response = memberQueryService.findMemberDetail(authMember); // then assertThat(response).usingRecursiveComparison() @@ -116,14 +116,16 @@ void findMemberById() { } @Test - @DisplayName("조회하려는 회원이 없는 경우 예외를 반환한다.") - void findMemberById_whenNoneExists_thenFail() { - // given when then - assertThatThrownBy(() -> memberQueryService.findById(Long.MAX_VALUE)) + @DisplayName("로그인한 식별값(ID)에 해당하는 회원 정보를 찾을 수 없는 경우 예외를 반환한다.") + void findMyProfile_whenNoneExists_thenFail() { + // given + AuthMember otherAuthMember = new User(Long.MAX_VALUE, Collections.emptyList(), Collections.emptyList()); + + // when then + assertThatThrownBy(() -> memberQueryService.findMemberDetail(otherAuthMember)) .isInstanceOf(MemberNotFoundException.class); } - @Test @DisplayName("즐겨찾기 목록에 추가 된 토픽을 조회할 수 있다") void findAllTopicsInBookmark_success() { @@ -158,7 +160,7 @@ void findAtlasByMember_Success() { } @Test - @DisplayName("") + @DisplayName("로그인한 회원의 모든 지도를 가져올 수 있다.") void findMyAllTopics_Success() { //when List myAllTopics = memberQueryService.findMyAllTopics(authMember); @@ -173,6 +175,26 @@ void findMyAllTopics_Success() { .isEqualTo(topicIds); } + @Test + @DisplayName("로그인한 회원의 모든 지도를 가져올 때, 삭제된 지도는 제외한다. (soft delete 반영)") + void findMyAllTopics_Success_notContainingSoftDeleted() { + // given + List topicIds = topics.stream() + .map(Topic::getId) + .toList(); + + // when + Long deleted = topicIds.get(0); + topicRepository.deleteById(deleted); + topicRepository.flush(); + List myAllTopics = memberQueryService.findMyAllTopics(authMember); + + // then + assertThat(myAllTopics).hasSize(topics.size() - 1); + assertThat(myAllTopics).extractingResultOf("id") + .doesNotContain(deleted); + } + @Test @DisplayName("로그인한 회원이 생성한 모든 핀을 가져올 수 있다.") void findMyAllPins_Success() { @@ -198,5 +220,5 @@ void findMyAllPins_Success() { .isEqualTo(pinIds); } - + } diff --git a/backend/src/test/java/com/mapbefine/mapbefine/member/domain/MemberResponseTest.java b/backend/src/test/java/com/mapbefine/mapbefine/member/domain/MemberResponseTest.java index 1af18864..b5b0cc74 100644 --- a/backend/src/test/java/com/mapbefine/mapbefine/member/domain/MemberResponseTest.java +++ b/backend/src/test/java/com/mapbefine/mapbefine/member/domain/MemberResponseTest.java @@ -25,8 +25,6 @@ void createMemberResponse_success() { // then assertThat(memberResponse.nickName()) .isEqualTo(member.getMemberInfo().getNickName()); - assertThat(memberResponse.email()) - .isEqualTo(member.getMemberInfo().getEmail()); } } diff --git a/backend/src/test/java/com/mapbefine/mapbefine/member/presentation/MemberControllerTest.java b/backend/src/test/java/com/mapbefine/mapbefine/member/presentation/MemberControllerTest.java index df12531d..5d14d59b 100644 --- a/backend/src/test/java/com/mapbefine/mapbefine/member/presentation/MemberControllerTest.java +++ b/backend/src/test/java/com/mapbefine/mapbefine/member/presentation/MemberControllerTest.java @@ -6,6 +6,7 @@ import com.mapbefine.mapbefine.common.RestDocsIntegration; import com.mapbefine.mapbefine.location.LocationFixture; +import com.mapbefine.mapbefine.member.application.MemberCommandService; import com.mapbefine.mapbefine.member.application.MemberQueryService; import com.mapbefine.mapbefine.member.dto.request.MemberUpdateRequest; import com.mapbefine.mapbefine.member.dto.response.MemberDetailResponse; @@ -19,11 +20,14 @@ import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.http.MediaType; import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; +import org.springframework.test.web.servlet.result.MockMvcResultMatchers; class MemberControllerTest extends RestDocsIntegration { @MockBean private MemberQueryService memberQueryService; + @MockBean + private MemberCommandService memberCommandService; @Test @DisplayName("회원 목록 조회") @@ -31,27 +35,25 @@ void findAllMember() throws Exception { List memberResponses = List.of( new MemberResponse( 1L, - "member1", - "member1@member.com" + "member1" ), new MemberResponse( 2L, - "member2", - "member2@member.com" + "member2" ) ); given(memberQueryService.findAll()).willReturn(memberResponses); - mockMvc.perform( - MockMvcRequestBuilders.get("/members") - .header(AUTHORIZATION, testAuthHeaderProvider.createAuthHeaderById(1L)) - ).andDo(restDocs.document()); + mockMvc.perform(MockMvcRequestBuilders.get("/members") + .header(AUTHORIZATION, testAuthHeaderProvider.createAuthHeaderById(1L))) + .andExpect(MockMvcResultMatchers.status().isOk()) + .andDo(restDocs.document()); } @Test - @DisplayName("회원 단일 조회") - void findMemberById() throws Exception { + @DisplayName("회원 상세 정보 조회") + void findMyProfile() throws Exception { MemberDetailResponse memberDetailResponse = new MemberDetailResponse( 1L, "member", @@ -60,12 +62,12 @@ void findMemberById() throws Exception { LocalDateTime.now() ); - given(memberQueryService.findById(any())).willReturn(memberDetailResponse); + given(memberQueryService.findMemberDetail(any())).willReturn(memberDetailResponse); - mockMvc.perform( - MockMvcRequestBuilders.get("/members/1") - .header(AUTHORIZATION, testAuthHeaderProvider.createAuthHeaderById(1L)) - ).andDo(restDocs.document()); + mockMvc.perform(MockMvcRequestBuilders.get("/members/my/profiles") + .header(AUTHORIZATION, testAuthHeaderProvider.createAuthHeaderById(1L))) + .andExpect(MockMvcResultMatchers.status().isOk()) + .andDo(restDocs.document()); } @Test @@ -97,10 +99,10 @@ void findAllTopicsInAtlas() throws Exception { given(memberQueryService.findAllTopicsInAtlas(any())).willReturn(responses); - mockMvc.perform( - MockMvcRequestBuilders.get("/members/my/atlas") - .header(AUTHORIZATION, testAuthHeaderProvider.createAuthHeaderById(1L)) - ).andDo(restDocs.document()); + mockMvc.perform(MockMvcRequestBuilders.get("/members/my/atlas") + .header(AUTHORIZATION, testAuthHeaderProvider.createAuthHeaderById(1L))) + .andExpect(MockMvcResultMatchers.status().isOk()) + .andDo(restDocs.document()); } @Test @@ -132,10 +134,10 @@ void findAllTopicsInBookmark() throws Exception { given(memberQueryService.findAllTopicsInBookmark(any())).willReturn(responses); - mockMvc.perform( - MockMvcRequestBuilders.get("/members/my/bookmarks") - .header(AUTHORIZATION, testAuthHeaderProvider.createAuthHeaderById(1L)) - ).andDo(restDocs.document()); + mockMvc.perform(MockMvcRequestBuilders.get("/members/my/bookmarks") + .header(AUTHORIZATION, testAuthHeaderProvider.createAuthHeaderById(1L))) + .andExpect(MockMvcResultMatchers.status().isOk()) + .andDo(restDocs.document()); } @Test @@ -167,10 +169,10 @@ void findMyAllTopics() throws Exception { given(memberQueryService.findMyAllTopics(any())).willReturn(responses); - mockMvc.perform( - MockMvcRequestBuilders.get("/members/my/topics") - .header(AUTHORIZATION, testAuthHeaderProvider.createAuthHeaderById(1L)) - ).andDo(restDocs.document()); + mockMvc.perform(MockMvcRequestBuilders.get("/members/my/topics") + .header(AUTHORIZATION, testAuthHeaderProvider.createAuthHeaderById(1L))) + .andExpect(MockMvcResultMatchers.status().isOk()) + .andDo(restDocs.document()); } @Test @@ -198,10 +200,10 @@ void findMyAllPins() throws Exception { given(memberQueryService.findMyAllPins(any())).willReturn(responses); - mockMvc.perform( - MockMvcRequestBuilders.get("/members/my/pins") - .header(AUTHORIZATION, testAuthHeaderProvider.createAuthHeaderById(1L)) - ).andDo(restDocs.document()); + mockMvc.perform(MockMvcRequestBuilders.get("/members/my/pins") + .header(AUTHORIZATION, testAuthHeaderProvider.createAuthHeaderById(1L))) + .andExpect(MockMvcResultMatchers.status().isOk()) + .andDo(restDocs.document()); } @Test @@ -209,12 +211,12 @@ void findMyAllPins() throws Exception { void updateMyInfo() throws Exception { MemberUpdateRequest request = new MemberUpdateRequest("새로운 닉네임"); - mockMvc.perform( - MockMvcRequestBuilders.patch("/members/my/profiles") + mockMvc.perform(MockMvcRequestBuilders.patch("/members/my/profiles") .header(AUTHORIZATION, testAuthHeaderProvider.createAuthHeaderById(1L)) .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(request)) - ).andDo(restDocs.document()); + .content(objectMapper.writeValueAsString(request))) + .andExpect(MockMvcResultMatchers.status().isOk()) + .andDo(restDocs.document()); } } diff --git a/backend/src/test/java/com/mapbefine/mapbefine/oauth/application/OauthServiceTest.java b/backend/src/test/java/com/mapbefine/mapbefine/oauth/application/OauthServiceTest.java index 4f23f219..119bd110 100644 --- a/backend/src/test/java/com/mapbefine/mapbefine/oauth/application/OauthServiceTest.java +++ b/backend/src/test/java/com/mapbefine/mapbefine/oauth/application/OauthServiceTest.java @@ -6,6 +6,7 @@ import static org.mockito.BDDMockito.given; import com.mapbefine.mapbefine.DatabaseCleanup; +import com.mapbefine.mapbefine.TestDatabaseContainer; import com.mapbefine.mapbefine.member.domain.Member; import com.mapbefine.mapbefine.member.domain.MemberRepository; import com.mapbefine.mapbefine.member.dto.response.MemberDetailResponse; @@ -25,7 +26,7 @@ import org.springframework.boot.test.mock.mockito.MockBean; @SpringBootTest -class OauthServiceTest { +class OauthServiceTest extends TestDatabaseContainer { private static final OauthMember oauthMember = OauthMember.of( "12345678901234567890", diff --git a/backend/src/test/java/com/mapbefine/mapbefine/permission/application/PermissionCommandServiceTest.java b/backend/src/test/java/com/mapbefine/mapbefine/permission/application/PermissionCommandServiceTest.java index 73a86dc3..1b0ad83f 100644 --- a/backend/src/test/java/com/mapbefine/mapbefine/permission/application/PermissionCommandServiceTest.java +++ b/backend/src/test/java/com/mapbefine/mapbefine/permission/application/PermissionCommandServiceTest.java @@ -4,6 +4,7 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import com.mapbefine.mapbefine.TestDatabaseContainer; import com.mapbefine.mapbefine.auth.domain.AuthMember; import com.mapbefine.mapbefine.auth.domain.member.Admin; import com.mapbefine.mapbefine.auth.domain.member.Guest; @@ -28,7 +29,7 @@ import org.springframework.beans.factory.annotation.Autowired; @ServiceTest -class PermissionCommandServiceTest { +class PermissionCommandServiceTest extends TestDatabaseContainer { @Autowired private PermissionCommandService permissionCommandService; diff --git a/backend/src/test/java/com/mapbefine/mapbefine/permission/application/PermissionQueryServiceTest.java b/backend/src/test/java/com/mapbefine/mapbefine/permission/application/PermissionQueryServiceTest.java index 43afe105..a287dbc7 100644 --- a/backend/src/test/java/com/mapbefine/mapbefine/permission/application/PermissionQueryServiceTest.java +++ b/backend/src/test/java/com/mapbefine/mapbefine/permission/application/PermissionQueryServiceTest.java @@ -3,6 +3,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; +import com.mapbefine.mapbefine.TestDatabaseContainer; import com.mapbefine.mapbefine.common.annotation.ServiceTest; import com.mapbefine.mapbefine.member.MemberFixture; import com.mapbefine.mapbefine.member.domain.Member; @@ -25,7 +26,7 @@ import org.springframework.beans.factory.annotation.Autowired; @ServiceTest -class PermissionQueryServiceTest { +class PermissionQueryServiceTest extends TestDatabaseContainer { @Autowired private MemberRepository memberRepository; diff --git a/backend/src/test/java/com/mapbefine/mapbefine/permission/presentation/PermissionControllerTest.java b/backend/src/test/java/com/mapbefine/mapbefine/permission/presentation/PermissionControllerTest.java index e2311f47..8f5e8be5 100644 --- a/backend/src/test/java/com/mapbefine/mapbefine/permission/presentation/PermissionControllerTest.java +++ b/backend/src/test/java/com/mapbefine/mapbefine/permission/presentation/PermissionControllerTest.java @@ -7,6 +7,7 @@ import com.mapbefine.mapbefine.common.RestDocsIntegration; import com.mapbefine.mapbefine.member.dto.response.MemberDetailResponse; import com.mapbefine.mapbefine.member.dto.response.MemberResponse; +import com.mapbefine.mapbefine.permission.application.PermissionCommandService; import com.mapbefine.mapbefine.permission.application.PermissionQueryService; import com.mapbefine.mapbefine.permission.dto.request.PermissionRequest; import com.mapbefine.mapbefine.permission.dto.response.PermissionMemberDetailResponse; @@ -20,49 +21,53 @@ import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.http.MediaType; import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; +import org.springframework.test.web.servlet.result.MockMvcResultMatchers; class PermissionControllerTest extends RestDocsIntegration { @MockBean private PermissionQueryService permissionQueryService; + @MockBean + private PermissionCommandService permissionCommandService; + @Test @DisplayName("권한 추가") void addPermission() throws Exception { PermissionRequest request = new PermissionRequest(1L, List.of(1L, 2L, 3L)); - mockMvc.perform( - MockMvcRequestBuilders.post("/permissions") + mockMvc.perform(MockMvcRequestBuilders.post("/permissions") .header(AUTHORIZATION, testAuthHeaderProvider.createAuthHeaderById(1L)) .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(request)) - ).andDo(restDocs.document()); + .content(objectMapper.writeValueAsString(request))) + .andExpect(MockMvcResultMatchers.status().isCreated()) + .andDo(restDocs.document()); } @Test @DisplayName("권한 삭제") void deletePermission() throws Exception { - mockMvc.perform( - MockMvcRequestBuilders.delete("/permissions/1") - .header(AUTHORIZATION, testAuthHeaderProvider.createAuthHeaderById(1L)) - ).andDo(restDocs.document()); + mockMvc.perform(MockMvcRequestBuilders.delete("/permissions/1") + .header(AUTHORIZATION, testAuthHeaderProvider.createAuthHeaderById(1L))) + .andExpect(MockMvcResultMatchers.status().isNoContent()) + .andDo(restDocs.document()); } @Test @DisplayName("특정 토픽 접근 정보 조회(권한 회원 목록, 공개 여부)") void findTopicAccessDetailByTopicId() throws Exception { List permissionedMembers = List.of( - new PermissionedMemberResponse(1L, new MemberResponse(1L, "member", "member@naver.com")), - new PermissionedMemberResponse(1L, new MemberResponse(2L, "memberr", "memberr@naver.com")) + new PermissionedMemberResponse(1L, new MemberResponse(1L, "member")), + new PermissionedMemberResponse(1L, new MemberResponse(2L, "memberr")) ); TopicAccessDetailResponse response = new TopicAccessDetailResponse(Publicity.PUBLIC, permissionedMembers); given(permissionQueryService.findTopicAccessDetailById(any())).willReturn(response); - mockMvc.perform( - MockMvcRequestBuilders.get("/permissions/topics/1") - .header(AUTHORIZATION, testAuthHeaderProvider.createAuthHeaderById(1L)) - ).andDo(restDocs.document()); + mockMvc.perform(MockMvcRequestBuilders.get("/permissions/topics/1") + .header(AUTHORIZATION, testAuthHeaderProvider.createAuthHeaderById(1L))) + .andExpect(MockMvcResultMatchers.status().isOk()) + .andDo(restDocs.document()); } @Test @@ -82,10 +87,10 @@ void findPermissionById() throws Exception { given(permissionQueryService.findPermissionById(any())).willReturn(permissionMemberDetailResponse); - mockMvc.perform( - MockMvcRequestBuilders.get("/permissions/1") - .header(AUTHORIZATION, testAuthHeaderProvider.createAuthHeaderById(1L)) - ).andDo(restDocs.document()); + mockMvc.perform(MockMvcRequestBuilders.get("/permissions/1") + .header(AUTHORIZATION, testAuthHeaderProvider.createAuthHeaderById(1L))) + .andExpect(MockMvcResultMatchers.status().isOk()) + .andDo(restDocs.document()); } } diff --git a/backend/src/test/java/com/mapbefine/mapbefine/pin/PinCommentFixture.java b/backend/src/test/java/com/mapbefine/mapbefine/pin/PinCommentFixture.java new file mode 100644 index 00000000..a151203a --- /dev/null +++ b/backend/src/test/java/com/mapbefine/mapbefine/pin/PinCommentFixture.java @@ -0,0 +1,33 @@ +package com.mapbefine.mapbefine.pin; + +import com.mapbefine.mapbefine.member.domain.Member; +import com.mapbefine.mapbefine.pin.domain.Pin; +import com.mapbefine.mapbefine.pin.domain.PinComment; + +public class PinCommentFixture { + + public static PinComment createParentComment( + Pin pin, + Member creator + ) { + return PinComment.ofParentPinComment( + pin, + creator, + "댓글" + ); + } + + public static PinComment createChildComment( + Pin pin, + Member creator, + PinComment savedParentPinComment + ) { + return PinComment.ofChildPinComment( + pin, + savedParentPinComment, + creator, + "댓글" + ); + } + +} diff --git a/backend/src/test/java/com/mapbefine/mapbefine/pin/PinImageFixture.java b/backend/src/test/java/com/mapbefine/mapbefine/pin/PinImageFixture.java index 597772d1..8b6f13c6 100644 --- a/backend/src/test/java/com/mapbefine/mapbefine/pin/PinImageFixture.java +++ b/backend/src/test/java/com/mapbefine/mapbefine/pin/PinImageFixture.java @@ -8,4 +8,5 @@ public class PinImageFixture { public static PinImage create(Pin pin) { return PinImage.createPinImageAssociatedWithPin("https://example.com/image.jpg", pin); } + } diff --git a/backend/src/test/java/com/mapbefine/mapbefine/pin/PinIntegrationTest.java b/backend/src/test/java/com/mapbefine/mapbefine/pin/PinIntegrationTest.java index 031f0da8..b4833c74 100644 --- a/backend/src/test/java/com/mapbefine/mapbefine/pin/PinIntegrationTest.java +++ b/backend/src/test/java/com/mapbefine/mapbefine/pin/PinIntegrationTest.java @@ -2,8 +2,11 @@ import static org.apache.http.HttpHeaders.AUTHORIZATION; import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doThrow; import com.mapbefine.mapbefine.common.IntegrationTest; +import com.mapbefine.mapbefine.history.application.PinHistoryCommandService; import com.mapbefine.mapbefine.location.LocationFixture; import com.mapbefine.mapbefine.location.domain.Location; import com.mapbefine.mapbefine.location.domain.LocationRepository; @@ -11,21 +14,34 @@ import com.mapbefine.mapbefine.member.domain.Member; import com.mapbefine.mapbefine.member.domain.MemberRepository; import com.mapbefine.mapbefine.member.domain.Role; +import com.mapbefine.mapbefine.pin.domain.Pin; +import com.mapbefine.mapbefine.pin.domain.PinComment; +import com.mapbefine.mapbefine.pin.domain.PinCommentRepository; +import com.mapbefine.mapbefine.pin.domain.PinRepository; +import com.mapbefine.mapbefine.pin.dto.request.PinCommentCreateRequest; +import com.mapbefine.mapbefine.pin.dto.request.PinCommentUpdateRequest; import com.mapbefine.mapbefine.pin.dto.request.PinCreateRequest; -import com.mapbefine.mapbefine.pin.dto.response.PinDetailResponse; +import com.mapbefine.mapbefine.pin.dto.request.PinUpdateRequest; +import com.mapbefine.mapbefine.pin.dto.response.PinCommentResponse; import com.mapbefine.mapbefine.pin.dto.response.PinImageResponse; import com.mapbefine.mapbefine.pin.dto.response.PinResponse; +import com.mapbefine.mapbefine.pin.event.PinUpdateEvent; import com.mapbefine.mapbefine.topic.TopicFixture; import com.mapbefine.mapbefine.topic.domain.Topic; import com.mapbefine.mapbefine.topic.domain.TopicRepository; -import io.restassured.*; -import io.restassured.response.*; +import io.restassured.RestAssured; +import io.restassured.common.mapper.TypeRef; +import io.restassured.response.ExtractableResponse; +import io.restassured.response.Response; import java.io.File; +import java.time.LocalDateTime; import java.util.List; 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.boot.test.mock.mockito.MockBean; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; @@ -40,6 +56,9 @@ class PinIntegrationTest extends IntegrationTest { private PinCreateRequest createRequestNoDuplicateLocation; private PinCreateRequest createRequestNoDuplicateLocation2; + + @MockBean + private PinHistoryCommandService pinHistoryCommandService; @Autowired private MemberRepository memberRepository; @@ -49,6 +68,12 @@ class PinIntegrationTest extends IntegrationTest { @Autowired private LocationRepository locationRepository; + @Autowired + private PinRepository pinRepository; + + @Autowired + private PinCommentRepository pinCommentRepository; + @BeforeEach void saveTopicAndLocation() { member = memberRepository.save(MemberFixture.create("member", "member@naver.com", Role.ADMIN)); @@ -114,6 +139,16 @@ private ExtractableResponse createPin(PinCreateRequest request) { .extract(); } + private ExtractableResponse updatePin(PinUpdateRequest request, long pinId) { + return RestAssured.given().log().all() + .header(AUTHORIZATION, authHeader) + .contentType(MediaType.APPLICATION_JSON_VALUE) + .body(request) + .when().put("/pins/" + pinId) + .then().log().all() + .extract(); + } + @Test @DisplayName("Image List 없이 Pin 을 정상적으로 생성한다.") void addIfNonExistImageList_Success() { @@ -142,6 +177,22 @@ void addIfNotExistDuplicateLocation_Success() { assertThat(response.statusCode()).isEqualTo(HttpStatus.CREATED.value()); } + @Test + @DisplayName("Pin을 수정하면 200을 반환한다.") + void updatePin_Success() { + //given + ExtractableResponse createResponse = createPin(createRequestNoDuplicateLocation); + + // when + PinUpdateRequest request = new PinUpdateRequest("핀 수정", "수정 설명"); + String pinLocation = createResponse.header("Location"); + long pinId = Long.parseLong(pinLocation.replace("/pins/", "")); + ExtractableResponse response = updatePin(request, pinId); + + //then + assertThat(response.statusCode()).isEqualTo(HttpStatus.OK.value()); + } + @Test @DisplayName("Pin 목록을 조회하면 저장된 Pin의 목록과 200을 반환한다.") void findAll_Success() { @@ -176,8 +227,6 @@ void findDetail_Success() { // when ExtractableResponse response = findById(pinId); - PinDetailResponse as = response.as(PinDetailResponse.class); - // then assertThat(response.jsonPath().getString("name")) .isEqualTo(createRequestNoDuplicateLocation.name()); @@ -277,5 +326,194 @@ void findAllPinsByMemberId_Success() { assertThat(pinResponses).hasSize(1); } + @Test + @DisplayName("핀 댓글을 조회하면 200을 반환한다.") + void findPinCommentPinId_Success() { + //given + long pinId = createPinAndGetId(createRequestDuplicateLocation); + Pin pin = pinRepository.findById(pinId).get(); + PinComment parentPinComment = pinCommentRepository.save( + PinCommentFixture.createParentComment(pin, member) + ); + PinComment childPinComment = pinCommentRepository.save( + PinCommentFixture.createChildComment(pin, member, parentPinComment) + ); + List expected = List.of( + PinCommentResponse.of(parentPinComment, true), + PinCommentResponse.of(childPinComment, true) + ); + + // when + ExtractableResponse response = RestAssured + .given().log().all() + .header(AUTHORIZATION, authHeader) + .accept(MediaType.APPLICATION_JSON_VALUE) + .when().get("/pins/ + "+ pinId + "/comments") + .then().log().all() + .extract(); + + // then + List pinCommentResponses = response.as(new TypeRef<>() {}); + assertThat(response.statusCode()).isEqualTo(HttpStatus.OK.value()); + assertThat(pinCommentResponses).hasSize(2); + assertThat(pinCommentResponses).usingRecursiveComparison() + .ignoringFieldsOfTypes(LocalDateTime.class) + .isEqualTo(expected); + } + + @Test + @DisplayName("핀 댓글을 생성하면 201 을 반환한다.") + void addParentPinComment_Success() { + //given + long pinId = createPinAndGetId(createRequestDuplicateLocation); + PinCommentCreateRequest request = new PinCommentCreateRequest( + pinId, + null, + "댓글" + ); + + // when + ExtractableResponse response = RestAssured + .given().log().all() + .header(AUTHORIZATION, authHeader) + .contentType(MediaType.APPLICATION_JSON_VALUE) + .body(request) + .when().post("/pins/comments") + .then().log().all() + .extract(); + + // then + assertThat(response.statusCode()).isEqualTo(HttpStatus.CREATED.value()); + } + + @Test + @DisplayName("핀 대댓글을 생성하면 201 을 반환한다.") + void addChildPinComment_Success() { + //given + long pinId = createPinAndGetId(createRequestDuplicateLocation); + Long parentPinCommentId = createParentPinComment(pinId); + PinCommentCreateRequest childPinCommentRequest = new PinCommentCreateRequest( + pinId, + parentPinCommentId, + "대댓글" + ); + + // when + ExtractableResponse response = RestAssured + .given().log().all() + .header(AUTHORIZATION, authHeader) + .contentType(MediaType.APPLICATION_JSON_VALUE) + .body(childPinCommentRequest) + .when().post("/pins/comments") + .then().log().all() + .extract(); + + // then + assertThat(response.statusCode()).isEqualTo(HttpStatus.CREATED.value()); + } + + private Long createParentPinComment(long pinId) { + PinCommentCreateRequest parentPinCommentRequest = new PinCommentCreateRequest( + pinId, + null, + "댓글" + ); + + ExtractableResponse createResponse = RestAssured.given().log().all() + .header(AUTHORIZATION, authHeader) + .contentType(MediaType.APPLICATION_JSON_VALUE) + .body(parentPinCommentRequest) + .accept(MediaType.APPLICATION_JSON_VALUE) + .when().post("/pins/comments") + .then().log().all() + .extract(); + + String locationHeader = createResponse.header("Location"); + return Long.parseLong(locationHeader.split("/")[3]); + } + + @Test + @DisplayName("핀 댓글을 수정하면 201 을 반환한다.") + void updatePinComment_Success() { + //given + long pinId = createPinAndGetId(createRequestDuplicateLocation); + PinCommentUpdateRequest request = new PinCommentUpdateRequest( + "댓그으으을" + ); + long pinCommentId = createParentPinComment(pinId); + + // when + ExtractableResponse response = RestAssured + .given().log().all() + .header(AUTHORIZATION, authHeader) + .contentType(MediaType.APPLICATION_JSON_VALUE) + .body(request) + .when().put("/pins/comments/" + pinCommentId) + .then().log().all() + .extract(); + + // then + assertThat(response.statusCode()).isEqualTo(HttpStatus.CREATED.value()); + } + + @Test + @DisplayName("핀 댓글을 삭제하면 204 을 반환한다.") + void removePinComment_Success() { + //given + long pinId = createPinAndGetId(createRequestDuplicateLocation); + Long parentPinCommentId = createParentPinComment(pinId); + + // when + ExtractableResponse response = RestAssured + .given().log().all() + .header(AUTHORIZATION, authHeader) + .when().delete("/pins/comments/" + parentPinCommentId) + .then().log().all() + .extract(); + + // then + assertThat(response.statusCode()).isEqualTo(HttpStatus.NO_CONTENT.value()); + } + + @Nested + class EventListenerTest { + + @Test + @DisplayName("Pin 저장 시 변경 이력 저장에 예외가 발생하면, 변경 사항을 함께 롤백한다.") + void savePin_FailBySaveHistory_Rollback() { + //given + doThrow(new IllegalStateException()).when(pinHistoryCommandService).saveHistory(any(PinUpdateEvent.class)); + + // when + ExtractableResponse response = createPin(createRequestNoDuplicateLocation); + + //then + assertThat(response.statusCode()).isEqualTo(HttpStatus.INTERNAL_SERVER_ERROR.value()); + assertThat(pinRepository.findAll()).isEmpty(); + } + + @Test + @DisplayName("Pin 수정 시 변경 이력 저장에 예외가 발생하면, 변경 사항을 함께 롤백한다.") + void updatePin_FailBySaveHistory_Rollback() { + //given + ExtractableResponse createResponse = createPin(createRequestNoDuplicateLocation); + String pinLocation = createResponse.header("Location"); + long pinId = Long.parseLong(pinLocation.replace("/pins/", "")); + doThrow(new IllegalStateException()).when(pinHistoryCommandService).saveHistory(any(PinUpdateEvent.class)); + + // when + PinUpdateRequest request = new PinUpdateRequest("pin update", "description"); + ExtractableResponse response = updatePin(request, pinId); + + //then + assertThat(response.statusCode()).isEqualTo(HttpStatus.INTERNAL_SERVER_ERROR.value()); + assertThat(pinRepository.findById(pinId)).isPresent() + .usingRecursiveComparison() + .withEqualsForFields(Object::equals, "name", "description") + .isNotEqualTo(request); + } + + } + } diff --git a/backend/src/test/java/com/mapbefine/mapbefine/pin/application/PinCommandServiceTest.java b/backend/src/test/java/com/mapbefine/mapbefine/pin/application/PinCommandServiceTest.java index d85b05b3..4636eb69 100644 --- a/backend/src/test/java/com/mapbefine/mapbefine/pin/application/PinCommandServiceTest.java +++ b/backend/src/test/java/com/mapbefine/mapbefine/pin/application/PinCommandServiceTest.java @@ -1,12 +1,22 @@ package com.mapbefine.mapbefine.pin.application; +import static com.mapbefine.mapbefine.topic.domain.PermissionType.ALL_MEMBERS; +import static com.mapbefine.mapbefine.topic.domain.PermissionType.GROUP_ONLY; +import static com.mapbefine.mapbefine.topic.domain.Publicity.PRIVATE; +import static com.mapbefine.mapbefine.topic.domain.Publicity.PUBLIC; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import com.mapbefine.mapbefine.TestDatabaseContainer; import com.mapbefine.mapbefine.auth.domain.AuthMember; import com.mapbefine.mapbefine.auth.domain.member.Admin; import com.mapbefine.mapbefine.auth.domain.member.Guest; import com.mapbefine.mapbefine.common.annotation.ServiceTest; +import com.mapbefine.mapbefine.history.application.PinHistoryCommandService; import com.mapbefine.mapbefine.image.FileFixture; import com.mapbefine.mapbefine.image.exception.ImageException.ImageBadRequestException; import com.mapbefine.mapbefine.location.LocationFixture; @@ -16,38 +26,58 @@ import com.mapbefine.mapbefine.member.domain.Member; import com.mapbefine.mapbefine.member.domain.MemberRepository; import com.mapbefine.mapbefine.member.domain.Role; +import com.mapbefine.mapbefine.pin.PinCommentFixture; +import com.mapbefine.mapbefine.pin.PinFixture; import com.mapbefine.mapbefine.pin.domain.Pin; +import com.mapbefine.mapbefine.pin.domain.PinComment; +import com.mapbefine.mapbefine.pin.domain.PinCommentRepository; import com.mapbefine.mapbefine.pin.domain.PinImage; import com.mapbefine.mapbefine.pin.domain.PinImageRepository; import com.mapbefine.mapbefine.pin.domain.PinRepository; +import com.mapbefine.mapbefine.pin.dto.request.PinCommentCreateRequest; +import com.mapbefine.mapbefine.pin.dto.request.PinCommentUpdateRequest; import com.mapbefine.mapbefine.pin.dto.request.PinCreateRequest; import com.mapbefine.mapbefine.pin.dto.request.PinImageCreateRequest; import com.mapbefine.mapbefine.pin.dto.request.PinUpdateRequest; import com.mapbefine.mapbefine.pin.dto.response.PinDetailResponse; import com.mapbefine.mapbefine.pin.dto.response.PinImageResponse; +import com.mapbefine.mapbefine.pin.event.PinUpdateEvent; +import com.mapbefine.mapbefine.pin.exception.PinCommentException.PinCommentBadRequestException; +import com.mapbefine.mapbefine.pin.exception.PinCommentException.PinCommentForbiddenException; import com.mapbefine.mapbefine.pin.exception.PinException.PinForbiddenException; import com.mapbefine.mapbefine.topic.TopicFixture; +import com.mapbefine.mapbefine.topic.domain.PermissionType; +import com.mapbefine.mapbefine.topic.domain.Publicity; import com.mapbefine.mapbefine.topic.domain.Topic; import com.mapbefine.mapbefine.topic.domain.TopicRepository; +import java.time.LocalDateTime; +import java.util.Collections; +import java.util.List; +import java.util.stream.Stream; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.web.multipart.MultipartFile; -import java.util.List; - @ServiceTest -class PinCommandServiceTest { +class PinCommandServiceTest extends TestDatabaseContainer { private static final MultipartFile BASE_IMAGE_FILE = FileFixture.createFile(); private static final String BASE_IMAGE = "https://mapbefine.github.io/favicon.png"; + @MockBean + private PinHistoryCommandService pinHistoryCommandService; @Autowired private PinCommandService pinCommandService; @Autowired private PinQueryService pinQueryService; + @Autowired private PinRepository pinRepository; @Autowired @@ -58,16 +88,20 @@ class PinCommandServiceTest { private MemberRepository memberRepository; @Autowired private PinImageRepository pinImageRepository; + @Autowired + private PinCommentRepository pinCommentRepository; private Location location; private Topic topic; - private Member member; + private Member user; private AuthMember authMember; private PinCreateRequest createRequest; @BeforeEach void setUp() { - member = memberRepository.save(MemberFixture.create("user1", "userfirst@naver.com", Role.ADMIN)); + Member member = memberRepository.save(MemberFixture.create("user1", "userfirst@naver.com", Role.ADMIN)); + user = memberRepository.save(MemberFixture.create("user2", "usersecond@naver.com", Role.USER)); + location = locationRepository.save(LocationFixture.create()); topic = topicRepository.save(TopicFixture.createByName("topic", member)); @@ -151,6 +185,27 @@ void save_Success_UpdateLastPinAddedAt() { ); } + @Test + @DisplayName("핀을 추가하면 핀 정보 이력을 저장한다.") + void save_Success_SaveHistory() { + // when + pinCommandService.save(authMember, List.of(BASE_IMAGE_FILE), createRequest); + + // then + verify(pinHistoryCommandService, times(1)).saveHistory(any(PinUpdateEvent.class)); + } + + @Test + @DisplayName("핀 추가 시 예외가 발생하면, 정보 이력도 저장하지 않는다.") + void save_Fail_DoNotSaveHistory() { + // when + assertThatThrownBy(() -> pinCommandService.save(new Guest(), Collections.emptyList(), createRequest)) + .isInstanceOf(PinForbiddenException.class); + + // then + verify(pinHistoryCommandService, never()).saveHistory(any(PinUpdateEvent.class)); + } + @Test @DisplayName("권한이 없는 토픽에 핀을 저장하면 예외를 발생시킨다.") void save_FailByForbidden() { @@ -179,6 +234,34 @@ void update_Success_UpdateLastPinsAddedAt() { ); } + @Test + @DisplayName("핀을 변경하면 핀 정보 이력을 저장한다.") + void update_Success_SaveHistory() { + // given + long pinId = pinCommandService.save(authMember, List.of(BASE_IMAGE_FILE), createRequest); + + // when + pinCommandService.update(authMember, pinId, new PinUpdateRequest("name", "update")); + + // then + verify(pinHistoryCommandService, times(2)).saveHistory(any(PinUpdateEvent.class)); + } + + @Test + @DisplayName("핀 수정 시 예외가 발생하면, 정보 이력도 저장하지 않는다.") + void update_Fail_DoNotSaveHistory() { + // given + long illegalPinId = -1L; + + // when + assertThatThrownBy( + () -> pinCommandService.update(new Guest(), illegalPinId, new PinUpdateRequest("name", "update")) + ).isInstanceOf(PinForbiddenException.class); + + // then + verify(pinHistoryCommandService, never()).saveHistory(any(PinUpdateEvent.class)); + } + @Test @DisplayName("권한이 없는 토픽에 핀을 수정하면 예외를 발생시킨다.") void update_FailByForbidden() { @@ -200,9 +283,9 @@ void removeById_Success() { pinCommandService.removeById(authMember, pinId); // then - assertThat(pinRepository.findByIdAndIsDeletedFalse(pinId)) + assertThat(pinRepository.findById(pinId)) .isEmpty(); - assertThat(pinImageRepository.findByIdAndIsDeletedFalse(pinId)) + assertThat(pinImageRepository.findById(pinId)) .isEmpty(); } @@ -268,7 +351,7 @@ void removeImageById_Success() { pinCommandService.removeImageById(authMember, pinImageId); // then - assertThat(pinImageRepository.findByIdAndIsDeletedFalse(pinImageId)) + assertThat(pinImageRepository.findById(pinImageId)) .isEmpty(); } @@ -292,4 +375,302 @@ void removeImageById_FailByForbidden() { .isInstanceOf(PinForbiddenException.class); } + @Test + @DisplayName("Guest 인 경우 핀 댓글을 생성하면 예외가 발생된다.") + void savePinComment_Fail_ByGuest() { + // given + Pin savedPin = pinRepository.save(PinFixture.create(location, topic, user)); + PinCommentCreateRequest request = new PinCommentCreateRequest(savedPin.getId(), null, "댓글"); + + // when then + assertThatThrownBy(() -> pinCommandService.savePinComment(new Guest(), request)) + .isInstanceOf(PinCommentForbiddenException.class); + } + + @ParameterizedTest + @MethodSource("publicAndPrivateTopicsStatus") + @DisplayName("일반 회원인 경우 공개 지도, 비공개 지도이지만 권한을 가진 지도에는 핀 댓글을 생성할 수 있다.") + void savePinComment_Success_ByCreator(Publicity publicity, PermissionType permissionType) { + // given + Topic topic = TopicFixture.createByPublicityAndPermissionTypeAndCreator(publicity, permissionType, user); + topicRepository.save(topic); + Pin savedPin = pinRepository.save(PinFixture.create(location, topic, user)); + PinCommentCreateRequest request = new PinCommentCreateRequest( + savedPin.getId(), null, "댓글" + ); + AuthMember creatorUser = MemberFixture.createUser(user); + + // when + Long pinCommentId = pinCommandService.savePinComment(creatorUser, request); + + // then + PinComment actual = pinCommentRepository.findById(pinCommentId).get(); + PinComment expected = PinComment.ofParentPinComment(savedPin, user, "댓글"); + + assertThat(actual) + .usingRecursiveComparison() + .ignoringFieldsOfTypes(LocalDateTime.class) + .ignoringFields("id") + .isEqualTo(expected); + } + + @Test + @DisplayName("일반 회원인 경우 비공개 지도이면서 권한을 가지고 있지 않은 지도에 핀 댓글을 생성할 수 없다.") + void savePinComment_Fail_ByNonCreator() { + // given + Topic topic = TopicFixture.createPrivateAndGroupOnlyTopic(user); + topicRepository.save(topic); + Pin savedPin = pinRepository.save(PinFixture.create(location, topic, user)); + PinCommentCreateRequest request = new PinCommentCreateRequest( + savedPin.getId(), null, "댓글" + ); + Member nonCreator = memberRepository.save( + MemberFixture.create("nonCreator", "nonCreator@naver.com", Role.USER) + ); + AuthMember nonCreatorUser = MemberFixture.createUser(nonCreator); + + // when then + assertThatThrownBy(() -> pinCommandService.savePinComment(nonCreatorUser, request)) + .isInstanceOf(PinCommentForbiddenException.class); + } + + @Test + @DisplayName("핀 대댓글에는 대댓글을 달 수 없다. (depth 2 이상)") + void savePinComment_Fail_ByIllegalDepth() { + // given + Pin savedPin = pinRepository.save(PinFixture.create(location, topic, user)); + PinComment parentPinComment = pinCommentRepository.save(PinCommentFixture.createParentComment(savedPin, user)); + PinComment childPinComment = pinCommentRepository.save( + PinCommentFixture.createChildComment(savedPin, user, parentPinComment) + ); + PinCommentCreateRequest request = new PinCommentCreateRequest( + savedPin.getId(), childPinComment.getId(), "대대댓글" + ); + AuthMember creatorUser = MemberFixture.createUser(user); + + // when then + assertThatThrownBy(() -> pinCommandService.savePinComment(creatorUser, request)) + .isInstanceOf(PinCommentBadRequestException.class); + } + + @ParameterizedTest + @MethodSource("publicAndPrivateTopicsStatus") + @DisplayName("Admin 인 경우 어떠한 유형의 지도라도 핀 댓글을 생성할 수 있다.") + void savePinComment_Success_ByAdmin(Publicity publicity, PermissionType permissionType) { + // given + Topic topic = TopicFixture.createByPublicityAndPermissionTypeAndCreator(publicity, permissionType, user); + topicRepository.save(topic); + Pin savedPin = pinRepository.save(PinFixture.create(location, topic, user)); + PinCommentCreateRequest request = new PinCommentCreateRequest( + savedPin.getId(), null, "댓글" + ); + Member nonCreator = memberRepository.save( + MemberFixture.create("admin", "admin@naver.com", Role.ADMIN) + ); + AuthMember nonCreatorAdmin = MemberFixture.createUser(nonCreator); + + // when + Long pinCommentId = pinCommandService.savePinComment(nonCreatorAdmin, request); + + // then + PinComment actual = pinCommentRepository.findById(pinCommentId).get(); + PinComment expected = PinComment.ofParentPinComment(savedPin, nonCreator, "댓글"); + assertThat(actual) + .usingRecursiveComparison() + .ignoringFieldsOfTypes(LocalDateTime.class) + .ignoringFields("id") + .isEqualTo(expected); + } + + @Test + @DisplayName("Guest 인 경우 핀 댓글을 수정할 수 없다.") + void updatePinComment_Fail_ByGuest() { + // given + Pin savedPin = pinRepository.save(PinFixture.create(location, topic, user)); + PinComment pinComment = pinCommentRepository.save(PinCommentFixture.createParentComment(savedPin, user)); + PinCommentUpdateRequest request = new PinCommentUpdateRequest( + "댓글 수정!" + ); + + // when then + assertThatThrownBy(() -> pinCommandService.updatePinComment(new Guest(), pinComment.getId(), request)) + .isInstanceOf(PinCommentForbiddenException.class); + } + + @Test + @DisplayName("일반 회원인 경우 본인이 단 핀 댓글을 수정할 수 있다.") + void updatePinComment_Success_ByCreator() { + // given + Pin savedPin = pinRepository.save(PinFixture.create(location, topic, user)); + PinComment pinComment = pinCommentRepository.save(PinCommentFixture.createParentComment(savedPin, user)); + PinCommentUpdateRequest request = new PinCommentUpdateRequest( + "댓글 수정!" + ); + AuthMember creatorUser = MemberFixture.createUser(user); + + // when + pinCommandService.updatePinComment(creatorUser, pinComment.getId(), request); + + // then + PinComment actual = pinCommentRepository.findById(pinComment.getId()).get(); + PinComment expected = PinComment.ofParentPinComment(savedPin, user, "댓글 수정!"); + assertThat(actual) + .usingRecursiveComparison() + .ignoringFieldsOfTypes(LocalDateTime.class) + .ignoringFields("id") + .isEqualTo(expected); + } + + @Test + @DisplayName("일반 회원인 경우 본인이 달지 않은 핀 댓글을 수정할 수 없다.") + void updatePinComment_Fail_ByNonCreator() { + // given + Pin savedPin = pinRepository.save(PinFixture.create(location, topic, user)); + PinComment pinComment = pinCommentRepository.save(PinCommentFixture.createParentComment(savedPin, user)); + PinCommentUpdateRequest request = new PinCommentUpdateRequest( + "댓글 수정!" + ); + Member nonCreator = memberRepository.save( + MemberFixture.create("nonCreator", "nonCreator@naver.com", Role.USER) + ); + AuthMember nonCreatorUser = MemberFixture.createUser(nonCreator); + + // when then + assertThatThrownBy(() -> pinCommandService.updatePinComment(nonCreatorUser, pinComment.getId(), request)) + .isInstanceOf(PinCommentForbiddenException.class); + } + + @Test + @DisplayName("Admin 인 경우 본인이 단 핀 댓글을 수정할 수 있다.") + void updatePinComment_Success_ByAdmin() { + // given + Pin savedPin = pinRepository.save(PinFixture.create(location, topic, user)); + PinComment pinComment = pinCommentRepository.save(PinCommentFixture.createParentComment(savedPin, user)); + PinCommentUpdateRequest request = new PinCommentUpdateRequest( + "댓글 수정!" + ); + AuthMember creatorAdmin = new Admin(user.getId()); + + // when + pinCommandService.updatePinComment(creatorAdmin, pinComment.getId(), request); + + // then + PinComment actual = pinCommentRepository.findById(pinComment.getId()).get(); + PinComment expected = PinComment.ofParentPinComment(savedPin, user, "댓글 수정!"); + assertThat(actual) + .usingRecursiveComparison() + .ignoringFieldsOfTypes(LocalDateTime.class) + .ignoringFields("id") + .isEqualTo(expected); + } + + @Test + @DisplayName("Admin 인 경우 본인이 달지 않은 핀 댓글을 수정할 수 있다.") + void updatePinComment_Success_ByNonCreatorAdmin() { + // given + Pin savedPin = pinRepository.save(PinFixture.create(location, topic, user)); + PinComment pinComment = pinCommentRepository.save(PinCommentFixture.createParentComment(savedPin, user)); + PinCommentUpdateRequest request = new PinCommentUpdateRequest( + "댓글 수정!" + ); + Member nonCreator = memberRepository.save( + MemberFixture.create("nonCreator", "nonCreator@naver.com", Role.ADMIN) + ); + AuthMember nonCreatorAdmin = MemberFixture.createUser(nonCreator); + + // when + pinCommandService.updatePinComment(nonCreatorAdmin, pinComment.getId(), request); + + // then + PinComment actual = pinCommentRepository.findById(pinComment.getId()).get(); + PinComment expected = PinComment.ofParentPinComment(savedPin, user, "댓글 수정!"); + assertThat(actual) + .usingRecursiveComparison() + .ignoringFieldsOfTypes(LocalDateTime.class) + .ignoringFields("id") + .isEqualTo(expected); + } + + @Test + @DisplayName("Guest 인 경우 핀 댓글을 삭제할 수 없다.") + void deletePinComment_Fail_ByGuest() { + // given + Pin savedPin = pinRepository.save(PinFixture.create(location, topic, user)); + PinComment pinComment = pinCommentRepository.save(PinCommentFixture.createParentComment(savedPin, user)); + + // when then + assertThatThrownBy(() -> pinCommandService.deletePinComment(new Guest(), pinComment.getId())) + .isInstanceOf(PinCommentForbiddenException.class); + } + + @Test + @DisplayName("일반 회원인 경우 본인이 단 핀 댓글을 삭제할 수 있다.") + void deletePinComment_Success_ByCreator() { + // given + Pin savedPin = pinRepository.save(PinFixture.create(location, topic, user)); + PinComment pinComment = pinCommentRepository.save(PinCommentFixture.createParentComment(savedPin, user)); + AuthMember creatorUser = MemberFixture.createUser(user); + + // when + pinCommandService.deletePinComment(creatorUser, pinComment.getId()); + + // then + assertThat(pinCommentRepository.existsById(pinComment.getId())).isFalse(); + } + + @Test + @DisplayName("일반 회원인 경우 본인이 달지 않은 핀 댓글을 삭제할 수 없다.") + void deletePinComment_Fail_ByNonCreator() { + // given + Pin savedPin = pinRepository.save(PinFixture.create(location, topic, user)); + PinComment pinComment = pinCommentRepository.save(PinCommentFixture.createParentComment(savedPin, user)); + Member nonCreator = memberRepository.save( + MemberFixture.create("nonCreator", "nonCreator@naver.com", Role.USER) + ); + AuthMember nonCreatorUser = MemberFixture.createUser(nonCreator); + + // when then + assertThatThrownBy(() -> pinCommandService.deletePinComment(nonCreatorUser, pinComment.getId())) + .isInstanceOf(PinCommentForbiddenException.class); + } + + @Test + @DisplayName("Admin 인 경우 본인이 단 핀 댓글을 삭제할 수 있다.") + void deletePinComment_Success_ByAdmin() { + // given + Pin savedPin = pinRepository.save(PinFixture.create(location, topic, user)); + PinComment pinComment = pinCommentRepository.save(PinCommentFixture.createParentComment(savedPin, user)); + AuthMember creatorAdmin = new Admin(user.getId()); + + // when + pinCommandService.deletePinComment(creatorAdmin, pinComment.getId()); + + // then + assertThat(pinCommentRepository.existsById(pinComment.getId())).isFalse(); + } + + @Test + @DisplayName("Admin 인 경우 본인이 달지 않은 핀 댓글을 삭제할 수 있다.") + void deletePinComment_Success_ByNonCreatorAdmin() { + // given + Pin savedPin = pinRepository.save(PinFixture.create(location, topic, user)); + PinComment pinComment = pinCommentRepository.save(PinCommentFixture.createParentComment(savedPin, user)); + Member nonCreator = MemberFixture.create("nonCreator", "nonCreator@naver.com", Role.ADMIN); + AuthMember nonCreatorAdmin = MemberFixture.createUser(nonCreator); + + // when + pinCommandService.deletePinComment(nonCreatorAdmin, pinComment.getId()); + + // then + assertThat(pinCommentRepository.existsById(pinComment.getId())).isFalse(); + } + + static Stream publicAndPrivateTopicsStatus() { + return Stream.of( + Arguments.of(PUBLIC, ALL_MEMBERS), + Arguments.of(PUBLIC, GROUP_ONLY), + Arguments.of(PRIVATE, GROUP_ONLY) + ); + } + } diff --git a/backend/src/test/java/com/mapbefine/mapbefine/pin/application/PinQueryServiceTest.java b/backend/src/test/java/com/mapbefine/mapbefine/pin/application/PinQueryServiceTest.java index 8237c48e..4969730e 100644 --- a/backend/src/test/java/com/mapbefine/mapbefine/pin/application/PinQueryServiceTest.java +++ b/backend/src/test/java/com/mapbefine/mapbefine/pin/application/PinQueryServiceTest.java @@ -1,9 +1,15 @@ package com.mapbefine.mapbefine.pin.application; +import static com.mapbefine.mapbefine.topic.domain.PermissionType.ALL_MEMBERS; +import static com.mapbefine.mapbefine.topic.domain.PermissionType.GROUP_ONLY; +import static com.mapbefine.mapbefine.topic.domain.Publicity.PRIVATE; +import static com.mapbefine.mapbefine.topic.domain.Publicity.PUBLIC; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; +import com.mapbefine.mapbefine.TestDatabaseContainer; import com.mapbefine.mapbefine.auth.domain.AuthMember; +import com.mapbefine.mapbefine.auth.domain.member.Guest; import com.mapbefine.mapbefine.auth.domain.member.User; import com.mapbefine.mapbefine.common.annotation.ServiceTest; import com.mapbefine.mapbefine.location.LocationFixture; @@ -13,27 +19,36 @@ import com.mapbefine.mapbefine.member.domain.Member; import com.mapbefine.mapbefine.member.domain.MemberRepository; import com.mapbefine.mapbefine.member.domain.Role; +import com.mapbefine.mapbefine.pin.PinCommentFixture; import com.mapbefine.mapbefine.pin.PinFixture; import com.mapbefine.mapbefine.pin.PinImageFixture; import com.mapbefine.mapbefine.pin.domain.Pin; +import com.mapbefine.mapbefine.pin.domain.PinComment; +import com.mapbefine.mapbefine.pin.domain.PinCommentRepository; import com.mapbefine.mapbefine.pin.domain.PinRepository; +import com.mapbefine.mapbefine.pin.dto.response.PinCommentResponse; import com.mapbefine.mapbefine.pin.dto.response.PinDetailResponse; import com.mapbefine.mapbefine.pin.dto.response.PinResponse; import com.mapbefine.mapbefine.pin.exception.PinException.PinForbiddenException; import com.mapbefine.mapbefine.pin.exception.PinException.PinNotFoundException; import com.mapbefine.mapbefine.topic.TopicFixture; +import com.mapbefine.mapbefine.topic.domain.PermissionType; +import com.mapbefine.mapbefine.topic.domain.Publicity; import com.mapbefine.mapbefine.topic.domain.Topic; import com.mapbefine.mapbefine.topic.domain.TopicRepository; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Stream; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; import org.springframework.beans.factory.annotation.Autowired; -import java.util.ArrayList; -import java.util.List; - @ServiceTest -class PinQueryServiceTest { +class PinQueryServiceTest extends TestDatabaseContainer { @Autowired private PinQueryService pinQueryService; @@ -45,6 +60,8 @@ class PinQueryServiceTest { private LocationRepository locationRepository; @Autowired private MemberRepository memberRepository; + @Autowired + private PinCommentRepository pinCommentRepository; private Location location; private Topic publicUser1Topic; @@ -189,4 +206,110 @@ void findAllPinsByMemberId_success() { assertThat(actual).extractingResultOf("id") .isEqualTo(pinIds); } + + @ParameterizedTest + @MethodSource("publicTopicsStatus") + @DisplayName("공개 지도인 경우, Guest 는 핀 댓글을 조회에 성공한다.") + void findAllPinCommentGuest_Success(Publicity publicity, PermissionType permissionType) { + // given + Topic topic = TopicFixture.createByPublicityAndPermissionTypeAndCreator(publicity, permissionType, user1); + Topic savedTopic = topicRepository.save(topic); + Pin savedPin = pinRepository.save(PinFixture.create(location, savedTopic, user1)); + PinComment savedPinComment = pinCommentRepository.save(PinCommentFixture.createParentComment(savedPin, user1)); + PinCommentResponse expected = PinCommentResponse.of(savedPinComment, false); + + // when + List actual = pinQueryService.findAllPinCommentsByPinId(new Guest(), savedPin.getId()); + + // then + assertThat(actual.get(0)).usingRecursiveComparison() + .isEqualTo(expected); + } + + @Test + @DisplayName("비공개 지도인 경우, Guest 는 핀 댓글을 조회를 할 수 없다.") + void findAllPinCommentGuest_Fail() { + // given + Topic topic = TopicFixture.createPrivateAndGroupOnlyTopic(user1); + Topic savedTopic = topicRepository.save(topic); + Pin savedPin = pinRepository.save(PinFixture.create(location, savedTopic, user1)); + + // when then + assertThatThrownBy(() -> pinQueryService.findAllPinCommentsByPinId(new Guest(), savedPin.getId())) + .isInstanceOf(PinForbiddenException.class); + } + + @ParameterizedTest + @MethodSource("publicAndPrivateTopicsStatus") + @DisplayName("일반 회원은 공개 지도인 경우와, 비공개 지도이면서 본인이 권한을 가진 지도의 핀 댓글을 조회할 수 있다.") + void findAllPinCommentUser_Success(Publicity publicity, PermissionType permissionType) { + // given + Topic topic = TopicFixture.createByPublicityAndPermissionTypeAndCreator(publicity, permissionType, user1); + Topic savedTopic = topicRepository.save(topic); + Pin savedPin = pinRepository.save(PinFixture.create(location, savedTopic, user1)); + PinComment savedPinComment = pinCommentRepository.save(PinCommentFixture.createParentComment(savedPin, user1)); + PinCommentResponse expected = PinCommentResponse.of(savedPinComment, true); + AuthMember creatorUser = MemberFixture.createUser(user1); + + // when + List actual = pinQueryService.findAllPinCommentsByPinId(creatorUser, savedPin.getId()); + + // then + assertThat(actual.get(0)).usingRecursiveComparison() + .isEqualTo(expected); + } + + @Test + @DisplayName("일반 회원인 경우 비공개 지도이면서 권한을 가지지 않은 지도에 핀 댓글을 조회할 수 없다.") + void findAllPinCommentUser_Fail() { + // given + Topic topic = TopicFixture.createPrivateAndGroupOnlyTopic(user1); + Topic savedTopic = topicRepository.save(topic); + Pin savedPin = pinRepository.save(PinFixture.create(location, savedTopic, user1)); + pinCommentRepository.save(PinCommentFixture.createParentComment(savedPin, user1)); + AuthMember nonCreatorUser = MemberFixture.createUser(user2); + + // when then + assertThatThrownBy(() -> pinQueryService.findAllPinCommentsByPinId(nonCreatorUser, savedPin.getId())) + .isInstanceOf(PinForbiddenException.class); + } + + @ParameterizedTest + @MethodSource("publicAndPrivateTopicsStatus") + @DisplayName("ADMIN 은 어떠한 유형의 지도의 핀 댓글을 조회할 수 있다.") + void findAllPinCommentAdmin_Success(Publicity publicity, PermissionType permissionType) { + // given + Topic topic = TopicFixture.createByPublicityAndPermissionTypeAndCreator(publicity, permissionType, user1); + Topic savedTopic = topicRepository.save(topic); + Pin savedPin = pinRepository.save(PinFixture.create(location, savedTopic, user1)); + PinComment savedPinComment = pinCommentRepository.save(PinCommentFixture.createParentComment(savedPin, user1)); + PinCommentResponse expected = PinCommentResponse.of(savedPinComment, true); + Member nonCreator = memberRepository.save( + MemberFixture.create("nonCreator", "nonCreator@naver.com", Role.ADMIN) + ); + AuthMember nonCreatorAdmin = MemberFixture.createUser(nonCreator); + + // when + List actual = pinQueryService.findAllPinCommentsByPinId(nonCreatorAdmin, savedPin.getId()); + + // then + assertThat(actual.get(0)).usingRecursiveComparison() + .isEqualTo(expected); + } + + static Stream publicTopicsStatus() { + return Stream.of( + Arguments.of(PUBLIC, ALL_MEMBERS), + Arguments.of(PUBLIC, GROUP_ONLY) + ); + } + + static Stream publicAndPrivateTopicsStatus() { + return Stream.of( + Arguments.of(PUBLIC, ALL_MEMBERS), + Arguments.of(PUBLIC, GROUP_ONLY), + Arguments.of(PRIVATE, GROUP_ONLY) + ); + } + } diff --git a/backend/src/test/java/com/mapbefine/mapbefine/pin/domain/PinCommentTest.java b/backend/src/test/java/com/mapbefine/mapbefine/pin/domain/PinCommentTest.java new file mode 100644 index 00000000..83c54e4a --- /dev/null +++ b/backend/src/test/java/com/mapbefine/mapbefine/pin/domain/PinCommentTest.java @@ -0,0 +1,96 @@ +package com.mapbefine.mapbefine.pin.domain; + +import static com.mapbefine.mapbefine.pin.domain.PinComment.ofParentPinComment; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import com.mapbefine.mapbefine.location.LocationFixture; +import com.mapbefine.mapbefine.location.domain.Location; +import com.mapbefine.mapbefine.member.MemberFixture; +import com.mapbefine.mapbefine.member.domain.Member; +import com.mapbefine.mapbefine.member.domain.Role; +import com.mapbefine.mapbefine.pin.PinFixture; +import com.mapbefine.mapbefine.pin.exception.PinCommentException.PinCommentBadRequestException; +import com.mapbefine.mapbefine.topic.TopicFixture; +import com.mapbefine.mapbefine.topic.domain.Topic; +import java.util.stream.Stream; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +class PinCommentTest { + + private Pin pin; + private Member creator; + + @BeforeEach + void beforeEach() { + Location location = LocationFixture.create(); + creator = MemberFixture.create("member", "member@naver.com", Role.USER); + Topic topic = TopicFixture.createPublicAndAllMembersTopic("https://imageUrl", creator); + pin = PinFixture.create(location, topic, creator); + } + + @ParameterizedTest + @MethodSource("validCommentContent") + @DisplayName("유효한 댓글 내용일 경우 핀 댓글 생성에 성공한다.") + void createPinComment_Success(String content) { + // when + PinComment pinComment = ofParentPinComment(pin, creator, content); + + // then + assertThat(pinComment.getContent()).isEqualTo(content); + } + + static Stream validCommentContent() { + return Stream.of( + Arguments.of("댓"), + Arguments.of("댓".repeat(1000)) + ); + } + + @ParameterizedTest + @MethodSource("invalidCommentContent") + @DisplayName("유효하지 않은 핀 댓글 생성에 실패한다.") + void createPinComment_Fail(String content) { + // when then + assertThatThrownBy(() -> ofParentPinComment(pin, creator, content)) + .isInstanceOf(PinCommentBadRequestException.class); + } + + static Stream invalidCommentContent() { + return Stream.of( + Arguments.of(""), + Arguments.of("댓".repeat(1001)) + ); + } + + @ParameterizedTest + @MethodSource("validCommentContent") + @DisplayName("유효한 댓글 내용일 경우 핀 댓글 내용 수정에 성공한다.") + void updatePinComment_Success(String content) { + // given + PinComment pinComment = ofParentPinComment(pin, creator, "댓글 수정 전"); + + // when + pinComment.updateContent(content); + + // then + assertThat(pinComment.getContent()).isEqualTo(content); + } + + + @ParameterizedTest + @MethodSource("invalidCommentContent") + @DisplayName("유효하지 않은 댓글 내용일 경우 핀 댓글 수정에 실패한다.") + void updatePinComment_Fail(String content) { + PinComment pinComment = ofParentPinComment(pin, creator, "댓글 수정 전"); + + // when then + assertThatThrownBy(() -> pinComment.updateContent(content)) + .isInstanceOf(PinCommentBadRequestException.class); + } + +} diff --git a/backend/src/test/java/com/mapbefine/mapbefine/pin/domain/PinImageRepositoryTest.java b/backend/src/test/java/com/mapbefine/mapbefine/pin/domain/PinImageRepositoryTest.java index 50b6983e..5ea952b4 100644 --- a/backend/src/test/java/com/mapbefine/mapbefine/pin/domain/PinImageRepositoryTest.java +++ b/backend/src/test/java/com/mapbefine/mapbefine/pin/domain/PinImageRepositoryTest.java @@ -2,6 +2,8 @@ import static org.assertj.core.api.Assertions.assertThat; +import com.mapbefine.mapbefine.TestDatabaseContainer; +import com.mapbefine.mapbefine.common.annotation.RepositoryTest; import com.mapbefine.mapbefine.location.LocationFixture; import com.mapbefine.mapbefine.location.domain.Location; import com.mapbefine.mapbefine.location.domain.LocationRepository; @@ -14,16 +16,15 @@ import com.mapbefine.mapbefine.topic.TopicFixture; import com.mapbefine.mapbefine.topic.domain.Topic; import com.mapbefine.mapbefine.topic.domain.TopicRepository; +import java.util.List; import org.junit.jupiter.api.BeforeEach; 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.orm.jpa.DataJpaTest; -import java.util.List; - -@DataJpaTest -class PinImageRepositoryTest { +@RepositoryTest +class PinImageRepositoryTest extends TestDatabaseContainer { @Autowired private PinImageRepository pinImageRepository; @@ -62,7 +63,7 @@ void deleteById_Success() { pinImageRepository.deleteById(pinImageId); //then - assertThat(pinImageRepository.findByIdAndIsDeletedFalse(pinImageId)) + assertThat(pinImageRepository.findById(pinImageId)) .isEmpty(); } @@ -81,7 +82,7 @@ void deleteAllByPinId_Success() { pinImageRepository.deleteAllByPinId(pin.getId()); //then - assertThat(pinImageRepository.findByIdAndIsDeletedFalse(pin.getId())) + assertThat(pinImageRepository.findById(pin.getId())) .isEmpty(); } @@ -102,9 +103,9 @@ void deleteAllByMemberId_Success() { pinImageRepository.deleteAllByPinIds(List.of(pin.getId(), otherPin.getId())); //then - assertThat(pinImageRepository.findByIdAndIsDeletedFalse(pin.getId())) + assertThat(pinImageRepository.findById(pin.getId())) .isEmpty(); - assertThat(pinImageRepository.findAllByPinIdAndIsDeletedFalse(otherPin.getId())) + assertThat(pinImageRepository.findAllByPinId(otherPin.getId())) .isEmpty(); } diff --git a/backend/src/test/java/com/mapbefine/mapbefine/pin/domain/PinRepositoryTest.java b/backend/src/test/java/com/mapbefine/mapbefine/pin/domain/PinRepositoryTest.java index 24bf7cde..0d60bf28 100644 --- a/backend/src/test/java/com/mapbefine/mapbefine/pin/domain/PinRepositoryTest.java +++ b/backend/src/test/java/com/mapbefine/mapbefine/pin/domain/PinRepositoryTest.java @@ -2,6 +2,9 @@ import static org.assertj.core.api.Assertions.assertThat; +import com.mapbefine.mapbefine.TestDatabaseContainer; +import com.mapbefine.mapbefine.common.annotation.RepositoryTest; +import com.mapbefine.mapbefine.common.config.JpaConfig; import com.mapbefine.mapbefine.location.LocationFixture; import com.mapbefine.mapbefine.location.domain.Location; import com.mapbefine.mapbefine.location.domain.LocationRepository; @@ -13,16 +16,15 @@ import com.mapbefine.mapbefine.topic.TopicFixture; import com.mapbefine.mapbefine.topic.domain.Topic; import com.mapbefine.mapbefine.topic.domain.TopicRepository; -import java.util.List; -import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; 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.orm.jpa.DataJpaTest; +import org.springframework.context.annotation.Import; -@DataJpaTest -class PinRepositoryTest { +@RepositoryTest +class PinRepositoryTest extends TestDatabaseContainer { @Autowired private TopicRepository topicRepository; @@ -57,11 +59,7 @@ void deleteById_Success() { pinRepository.deleteById(pinId); // then - pinRepository.findById(pinId) - .ifPresentOrElse( - found -> assertThat(found.isDeleted()).isTrue(), - Assertions::fail - ); + assertThat(pinRepository.existsById(pinId)); } @Test @@ -75,12 +73,11 @@ void deleteAllByTopicId_Success() { // when assertThat(topic.getPins()).extractingResultOf("isDeleted") .containsOnly(false); - pinRepository.deleteAllByTopicId(topic.getId()); + Long topicId = topic.getId(); + pinRepository.deleteAllByTopicId(topicId); // then - List deletedPins = pinRepository.findAllByTopicId(topic.getId()); - assertThat(deletedPins).extractingResultOf("isDeleted") - .containsOnly(true); + assertThat(pinRepository.findAllByTopicId(topicId).isEmpty()); } @@ -96,18 +93,17 @@ void deleteAllByMemberId_Success() { assertThat(member.getCreatedPins()).hasSize(10) .extractingResultOf("isDeleted") .containsOnly(false); - pinRepository.deleteAllByMemberId(member.getId()); + Long memberId = member.getId(); + pinRepository.deleteAllByMemberId(memberId); //then - List deletedPins = pinRepository.findAllByCreatorId(member.getId()); - assertThat(deletedPins).extractingResultOf("isDeleted") - .containsOnly(true); + assertThat(pinRepository.findAllByCreatorId(memberId)).isEmpty(); } @Test @DisplayName("다른 토픽에 존재하는 핀들이여도, Member ID로 모든 핀을 soft-delete 할 수 있다.") void deleteAllByMemberIdInOtherTopics_Success() { - //given + // given Topic otherTopic = TopicFixture.createByName("otherTopic", member); topicRepository.save(otherTopic); @@ -116,16 +112,15 @@ void deleteAllByMemberIdInOtherTopics_Success() { pinRepository.save(PinFixture.create(location, otherTopic, member)); } - //when + // when assertThat(member.getCreatedPins()).hasSize(20) .extractingResultOf("isDeleted") .containsOnly(false); - pinRepository.deleteAllByMemberId(member.getId()); + Long MemberId = member.getId(); + pinRepository.deleteAllByMemberId(MemberId); - //then - List deletedPins = pinRepository.findAllByCreatorId(member.getId()); - assertThat(deletedPins).extractingResultOf("isDeleted") - .containsOnly(true); + // then + assertThat(pinRepository.findAllByCreatorId(MemberId)).isEmpty(); } } diff --git a/backend/src/test/java/com/mapbefine/mapbefine/pin/presentation/PinControllerTest.java b/backend/src/test/java/com/mapbefine/mapbefine/pin/presentation/PinControllerTest.java index a283aff6..ff59c12c 100644 --- a/backend/src/test/java/com/mapbefine/mapbefine/pin/presentation/PinControllerTest.java +++ b/backend/src/test/java/com/mapbefine/mapbefine/pin/presentation/PinControllerTest.java @@ -1,29 +1,33 @@ package com.mapbefine.mapbefine.pin.presentation; import static org.apache.http.HttpHeaders.AUTHORIZATION; +import static org.apache.http.HttpHeaders.LOCATION; import static org.mockito.ArgumentMatchers.any; import static org.mockito.BDDMockito.given; -import static org.springframework.restdocs.headers.HeaderDocumentation.headerWithName; -import static org.springframework.restdocs.headers.HeaderDocumentation.requestHeaders; +import static org.springframework.http.HttpMethod.POST; +import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; +import static org.springframework.http.MediaType.IMAGE_PNG_VALUE; import com.mapbefine.mapbefine.common.RestDocsIntegration; import com.mapbefine.mapbefine.pin.application.PinCommandService; import com.mapbefine.mapbefine.pin.application.PinQueryService; +import com.mapbefine.mapbefine.pin.dto.request.PinCommentCreateRequest; +import com.mapbefine.mapbefine.pin.dto.request.PinCommentUpdateRequest; import com.mapbefine.mapbefine.pin.dto.request.PinCreateRequest; import com.mapbefine.mapbefine.pin.dto.request.PinUpdateRequest; +import com.mapbefine.mapbefine.pin.dto.response.PinCommentResponse; import com.mapbefine.mapbefine.pin.dto.response.PinDetailResponse; import com.mapbefine.mapbefine.pin.dto.response.PinImageResponse; import com.mapbefine.mapbefine.pin.dto.response.PinResponse; -import java.io.File; import java.time.LocalDateTime; import java.util.List; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.http.MediaType; +import org.springframework.mock.web.MockMultipartFile; import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; -import org.springframework.util.LinkedMultiValueMap; -import org.springframework.util.MultiValueMap; +import org.springframework.test.web.servlet.result.MockMvcResultMatchers; class PinControllerTest extends RestDocsIntegration { @@ -37,16 +41,11 @@ class PinControllerTest extends RestDocsIntegration { private PinQueryService pinQueryService; @Test - @DisplayName("핀 추가") + @DisplayName("핀 생성") void add() throws Exception { given(pinCommandService.save(any(), any(), any())).willReturn(1L); - File mockFile = new File(getClass() - .getClassLoader() - .getResource("test.png") - .getPath()); - MultiValueMap param = new LinkedMultiValueMap<>(); - PinCreateRequest pinCreateRequest = new PinCreateRequest( + PinCreateRequest request = new PinCreateRequest( 1L, "매튜의 산스장", "매튜가 사랑하는 산스장", @@ -55,16 +54,17 @@ void add() throws Exception { 37, 127 ); + MockMultipartFile requestJson = new MockMultipartFile("request", "request", APPLICATION_JSON_VALUE, + objectMapper.writeValueAsBytes(request)); + MockMultipartFile image = new MockMultipartFile("image", "test.png", IMAGE_PNG_VALUE, "data".getBytes()); - param.add("images", List.of(mockFile)); - param.add("request", pinCreateRequest); - - mockMvc.perform( - MockMvcRequestBuilders.post("/pins") + mockMvc.perform(MockMvcRequestBuilders.multipart(POST, "/pins") + .file(image) + .file(requestJson) .header(AUTHORIZATION, testAuthHeaderProvider.createAuthHeaderById(1L)) - .contentType(MediaType.MULTIPART_FORM_DATA_VALUE) - .content(objectMapper.writeValueAsString(param)) - ).andDo(restDocs.document()); + .contentType(MediaType.MULTIPART_FORM_DATA_VALUE)) + .andExpect(MockMvcResultMatchers.status().isCreated()) + .andDo(restDocs.document()); } @Test @@ -75,21 +75,21 @@ void update() throws Exception { "매튜가 다신 안 갈 집" ); - mockMvc.perform( - MockMvcRequestBuilders.put("/pins/1") + mockMvc.perform(MockMvcRequestBuilders.put("/pins/1") .header(AUTHORIZATION, testAuthHeaderProvider.createAuthHeaderById(1L)) .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(pinUpdateRequest)) - ).andDo(restDocs.document()); + .content(objectMapper.writeValueAsString(pinUpdateRequest))) + .andExpect(MockMvcResultMatchers.status().isOk()) + .andDo(restDocs.document()); } @Test @DisplayName("핀 삭제") void delete() throws Exception { - mockMvc.perform( - MockMvcRequestBuilders.delete("/pins/1") - .header(AUTHORIZATION, testAuthHeaderProvider.createAuthHeaderById(1L)) - ).andDo(restDocs.document()); + mockMvc.perform(MockMvcRequestBuilders.delete("/pins/1") + .header(AUTHORIZATION, testAuthHeaderProvider.createAuthHeaderById(1L))) + .andExpect(MockMvcResultMatchers.status().isNoContent()) + .andDo(restDocs.document()); } @Test @@ -107,16 +107,12 @@ void findById() throws Exception { LocalDateTime.now(), List.of(new PinImageResponse(1L, BASE_IMAGES.get(0))) ); - given(pinQueryService.findDetailById(any(), any())).willReturn(pinDetailResponse); - mockMvc.perform( - MockMvcRequestBuilders.get("/pins/1") - .header(AUTHORIZATION, testAuthHeaderProvider.createAuthHeaderById(1L)) - ).andDo(restDocs.document( - requestHeaders( - headerWithName(AUTHORIZATION).optional().description("Optional") - ))); + mockMvc.perform(MockMvcRequestBuilders.get("/pins/1") + .header(AUTHORIZATION, testAuthHeaderProvider.createAuthHeaderById(1L))) + .andExpect(MockMvcResultMatchers.status().isOk()) + .andDo(restDocs.document()); } @@ -145,43 +141,37 @@ void findAll() throws Exception { given(pinQueryService.findAllReadable(any())).willReturn(pinResponses); - mockMvc.perform( - MockMvcRequestBuilders.get("/pins") - .header(AUTHORIZATION, testAuthHeaderProvider.createAuthHeaderById(1L)) - ).andDo(restDocs.document( - requestHeaders( - headerWithName(AUTHORIZATION).optional().description("Optional") - ))); + mockMvc.perform(MockMvcRequestBuilders.get("/pins") + .header(AUTHORIZATION, testAuthHeaderProvider.createAuthHeaderById(1L))) + .andExpect(MockMvcResultMatchers.status().isOk()) + .andDo(restDocs.document()); } @Test @DisplayName("핀 이미지 추가") void addImage() throws Exception { - String imageFilePath = getClass().getClassLoader() - .getResource("test.png") - .getPath(); - File mockFile = new File(imageFilePath); + MockMultipartFile requestJson = new MockMultipartFile("pinId", "pinId", APPLICATION_JSON_VALUE, + objectMapper.writeValueAsBytes(1)); + MockMultipartFile image = new MockMultipartFile("image", "test.png", IMAGE_PNG_VALUE, "data".getBytes()); - mockMvc.perform( - MockMvcRequestBuilders.post("/pins/images") - .header(AUTHORIZATION, testAuthHeaderProvider.createAuthHeaderById(1L)) - .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(1L)) - .content(objectMapper.writeValueAsString(mockFile)) - ).andDo(restDocs.document()); + mockMvc.perform(MockMvcRequestBuilders.multipart(POST, "/pins/images") + .file(image) + .file(requestJson) + .header(AUTHORIZATION, testAuthHeaderProvider.createAuthHeaderById(1L))) + .andExpect(MockMvcResultMatchers.status().isCreated()) + .andDo(restDocs.document()); } @Test @DisplayName("핀 이미지 삭제") void removeImage() throws Exception { - mockMvc.perform( - MockMvcRequestBuilders.delete("/pins/images/1") + mockMvc.perform(MockMvcRequestBuilders.delete("/pins/images/1") .header(AUTHORIZATION, testAuthHeaderProvider.createAuthHeaderById(1L)) - .contentType(MediaType.APPLICATION_JSON) - ).andDo(restDocs.document()); + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(MockMvcResultMatchers.status().isNoContent()) + .andDo(restDocs.document()); } - @Test @DisplayName("회원 Id를 입력하면 해당 회원이 만든 핀 목록을 조회할 수 있다.") void findAllPinsByMemberId() throws Exception { @@ -207,11 +197,105 @@ void findAllPinsByMemberId() throws Exception { given(pinQueryService.findAllPinsByMemberId(any(), any())).willReturn(pinResponses); - mockMvc.perform( - MockMvcRequestBuilders.get("/pins/members?id=1") + mockMvc.perform(MockMvcRequestBuilders.get("/pins/members?id=1") + .header(AUTHORIZATION, testAuthHeaderProvider.createAuthHeaderById(1L))) + .andExpect(MockMvcResultMatchers.status().isOk()) + .andDo(restDocs.document()); + } + + @Test + @DisplayName("핀 댓글 생성") + void createParentPinComment() throws Exception { + PinCommentCreateRequest pinCommentCreateRequest = new PinCommentCreateRequest( + 1L, + null, + "댓글" + ); + given(pinCommandService.savePinComment(any(), any())).willReturn(1L); + mockMvc.perform(MockMvcRequestBuilders.post("/pins/comments") .header(AUTHORIZATION, testAuthHeaderProvider.createAuthHeaderById(1L)) - ).andDo(restDocs.document()); + .header(LOCATION, "/pins/comments/1") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(pinCommentCreateRequest))) + .andExpect(MockMvcResultMatchers.status().isCreated()) + .andDo(restDocs.document()); + } + + @Test + @DisplayName("핀 댓글 생성") + void createChildPinComment() throws Exception { + PinCommentCreateRequest pinCommentCreateRequest = new PinCommentCreateRequest( + 1L, + 1L, + "댓글" + ); + given(pinCommandService.savePinComment(any(), any())).willReturn(1L); + + mockMvc.perform(MockMvcRequestBuilders.post("/pins/comments") + .header(AUTHORIZATION, testAuthHeaderProvider.createAuthHeaderById(1L)) + .header(LOCATION, "/pins/comments/1") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(pinCommentCreateRequest))) + .andExpect(MockMvcResultMatchers.status().isCreated()) + .andDo(restDocs.document()); + } + + @Test + @DisplayName("핀 댓글 조회") + void findAllPinCommentByPinId() throws Exception { + List pinCommentResponses = List.of( + new PinCommentResponse( + 1L, + "댓글", + "creator", + "https://creatorImageUrl", + null, + true, + LocalDateTime.now() + ), + new PinCommentResponse( + 2L, + "대댓글", + "creator", + "https://creatorImageUrl", + 1L, + true, + LocalDateTime.now() + ) + ); + given(pinQueryService.findAllPinCommentsByPinId(any(), any())).willReturn(pinCommentResponses); + + mockMvc.perform(MockMvcRequestBuilders.get("/pins/1/comments") + .header(AUTHORIZATION, testAuthHeaderProvider.createAuthHeaderById(1L)) + .contentType(APPLICATION_JSON_VALUE)) + .andExpect(MockMvcResultMatchers.status().isOk()) + .andDo(restDocs.document()); + } + + @Test + @DisplayName("핀 댓글 수정") + void updatePinComment() throws Exception { + PinCommentUpdateRequest pinCommentUpdateRequest = new PinCommentUpdateRequest( + "댓글 수정" + ); + + mockMvc.perform(MockMvcRequestBuilders.put("/pins/comments/1") + .header(AUTHORIZATION, testAuthHeaderProvider.createAuthHeaderById(1L)) + .header(LOCATION, "/pins/comments/1") + .contentType(APPLICATION_JSON_VALUE) + .content(objectMapper.writeValueAsString(pinCommentUpdateRequest))) + .andExpect(MockMvcResultMatchers.status().isCreated()) + .andDo(restDocs.document()); + } + + @Test + @DisplayName("핀 댓글 삭제") + void removePinComment() throws Exception { + mockMvc.perform(MockMvcRequestBuilders.delete("/pins/comments/1") + .header(AUTHORIZATION, testAuthHeaderProvider.createAuthHeaderById(1L))) + .andExpect(MockMvcResultMatchers.status().isNoContent()) + .andDo(restDocs.document()); } } diff --git a/backend/src/test/java/com/mapbefine/mapbefine/topic/TopicFixture.java b/backend/src/test/java/com/mapbefine/mapbefine/topic/TopicFixture.java index 4dab6572..d364100c 100644 --- a/backend/src/test/java/com/mapbefine/mapbefine/topic/TopicFixture.java +++ b/backend/src/test/java/com/mapbefine/mapbefine/topic/TopicFixture.java @@ -9,11 +9,27 @@ import com.mapbefine.mapbefine.topic.dto.request.TopicCreateRequest; import com.mapbefine.mapbefine.topic.dto.request.TopicMergeRequest; import java.util.List; +import org.apache.http.conn.util.PublicSuffixList; public class TopicFixture { private static final String IMAGE_URL = "https://map-befine-official.github.io/favicon.png"; + public static Topic createByPublicityAndPermissionTypeAndCreator( + Publicity publicity, + PermissionType permissionType, + Member creator + ) { + return Topic.createTopicAssociatedWithCreator( + "토픽", + "토픽의 Publicity, PermissionType 이 동적으로 정해집니다.", + IMAGE_URL, + publicity, + permissionType, + creator + ); + } + public static Topic createPrivateAndGroupOnlyTopic(Member member) { return Topic.createTopicAssociatedWithCreator( "토픽 회원만 읽을 수 있는 토픽", @@ -36,6 +52,28 @@ public static Topic createPublicAndAllMembersTopic(Member member) { ); } + public static Topic createPublicAndGroupOnlyTopic(Member member) { + return Topic.createTopicAssociatedWithCreator( + "아무나 읽을 수 있는 토픽", + "아무나 읽지만 아무나 생성할 수는 없습니다.", + IMAGE_URL, + Publicity.PUBLIC, + PermissionType.GROUP_ONLY, + member + ); + } + + public static Topic createPublicAndAllMembersTopic(String imageUrl, Member member) { + return Topic.createTopicAssociatedWithCreator( + "아무나 읽을 수 있는 토픽", + "아무나 읽을 수 있습니다.", + imageUrl, + Publicity.PUBLIC, + PermissionType.ALL_MEMBERS, + member + ); + } + public static Topic createByName(String name, Member member) { return Topic.createTopicAssociatedWithCreator( name, @@ -96,4 +134,5 @@ public static TopicMergeRequest createPublicAndAllMembersMergeRequestWithTopics( topicIds ); } + } diff --git a/backend/src/test/java/com/mapbefine/mapbefine/topic/TopicIntegrationTest.java b/backend/src/test/java/com/mapbefine/mapbefine/topic/TopicIntegrationTest.java index 5a03a8c2..7b7a53aa 100644 --- a/backend/src/test/java/com/mapbefine/mapbefine/topic/TopicIntegrationTest.java +++ b/backend/src/test/java/com/mapbefine/mapbefine/topic/TopicIntegrationTest.java @@ -279,7 +279,6 @@ void updateTopic_Success() { // when TopicUpdateRequest 송파_데이트코스 = new TopicUpdateRequest( "송파 데이트코스", - "https://map-befine-official.github.io/favicon.png", "수정한 토픽", Publicity.PUBLIC, PermissionType.ALL_MEMBERS @@ -517,4 +516,33 @@ void findAllTopicsWhenGuest() { assertThat(response.statusCode()).isEqualTo(HttpStatus.OK.value()); } + @Test + @DisplayName("Topic의 이미지를 변경하면 200을 반환한다") + void updateTopicImage_Success() { + ExtractableResponse newTopic = createNewTopic( + new TopicCreateRequestWithoutImage( + "준팍의 또간집", + "준팍이 두번 간집", + Publicity.PUBLIC, + PermissionType.ALL_MEMBERS, + Collections.emptyList() + ), + authHeader + ); + long topicId = Long.parseLong(newTopic.header("Location").split("/")[2]); + + // when + ExtractableResponse response = RestAssured + .given().log().all() + .header(AUTHORIZATION, authHeader) + .contentType(MediaType.MULTIPART_FORM_DATA_VALUE) + .multiPart("image", mockFile) + .when().put("/topics/images/{id}", topicId) + .then().log().all() + .extract(); + + // then + assertThat(response.statusCode()).isEqualTo(HttpStatus.OK.value()); + } + } diff --git a/backend/src/test/java/com/mapbefine/mapbefine/topic/application/TopicCommandServiceTest.java b/backend/src/test/java/com/mapbefine/mapbefine/topic/application/TopicCommandServiceTest.java index bde489c8..1d65e3dc 100644 --- a/backend/src/test/java/com/mapbefine/mapbefine/topic/application/TopicCommandServiceTest.java +++ b/backend/src/test/java/com/mapbefine/mapbefine/topic/application/TopicCommandServiceTest.java @@ -1,12 +1,17 @@ package com.mapbefine.mapbefine.topic.application; +import static com.mapbefine.mapbefine.image.FileFixture.createFile; +import static org.assertj.core.api.Assertions.*; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertAll; +import com.mapbefine.mapbefine.TestDatabaseContainer; import com.mapbefine.mapbefine.auth.domain.AuthMember; import com.mapbefine.mapbefine.auth.domain.member.Admin; import com.mapbefine.mapbefine.auth.domain.member.Guest; import com.mapbefine.mapbefine.common.annotation.ServiceTest; +import com.mapbefine.mapbefine.image.FileFixture; import com.mapbefine.mapbefine.location.LocationFixture; import com.mapbefine.mapbefine.location.domain.Location; import com.mapbefine.mapbefine.location.domain.LocationRepository; @@ -29,16 +34,16 @@ import com.mapbefine.mapbefine.topic.dto.response.TopicDetailResponse; import com.mapbefine.mapbefine.topic.exception.TopicException.TopicBadRequestException; import com.mapbefine.mapbefine.topic.exception.TopicException.TopicForbiddenException; +import java.util.Collections; +import java.util.List; +import java.util.NoSuchElementException; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; -import java.util.Collections; -import java.util.List; - @ServiceTest -class TopicCommandServiceTest { +class TopicCommandServiceTest extends TestDatabaseContainer { @Autowired private MemberRepository memberRepository; @@ -334,7 +339,6 @@ void updateTopicInfo_Success() { AuthMember user = MemberFixture.createUser(member); TopicUpdateRequest request = new TopicUpdateRequest( "수정된 이름", - "https://map-befine-official.github.io/favicon.png", "수정된 설명", Publicity.PRIVATE, PermissionType.GROUP_ONLY @@ -366,7 +370,6 @@ void updateTopicInfo_Fail() { //when then TopicUpdateRequest request = new TopicUpdateRequest( "수정된 이름", - "https://map-befine-official.github.io/favicon.png", "수정된 설명", Publicity.PUBLIC, PermissionType.ALL_MEMBERS @@ -398,9 +401,7 @@ void delete_Success() { topicCommandService.delete(adminAuthMember, topic.getId()); //then - Topic deletedTopic = topicRepository.findById(topic.getId()).get(); - - assertThat(deletedTopic.isDeleted()).isTrue(); + assertThat(topicRepository.existsById(topic.getId())).isFalse(); } @Test @@ -414,4 +415,82 @@ void delete_Fail() { assertThatThrownBy(() -> topicCommandService.delete(user, topic.getId())) .isInstanceOf(TopicForbiddenException.class); } + + @Test + @DisplayName("Guest는 토픽의 이미지를 변경할 수 없다.") + void updateTopicImage_FailByGuest() { + // given + Topic topic = TopicFixture.createPublicAndAllMembersTopic(member); + topicRepository.save(topic); + + // when then + assertThatThrownBy(() -> topicCommandService.updateTopicImage(guest, topic.getId(), createFile())) + .isInstanceOf(TopicForbiddenException.class); + } + + @Test + @DisplayName("토픽 생성자가 아니면 토픽의 이미지를 변경할 수 없다.") + void updateTopicImage_FailByNotCreator() { + // given + Topic topic = TopicFixture.createPublicAndAllMembersTopic(member); + topicRepository.save(topic); + Member otherMember = MemberFixture.create("토픽을 만든자가 아님", "member@naver.com", Role.USER); + AuthMember otherAuthMember = MemberFixture.createUser(otherMember); + + // when then + assertThatThrownBy(() -> topicCommandService.updateTopicImage(otherAuthMember, topic.getId(), createFile())) + .isInstanceOf(TopicForbiddenException.class); + } + + @Test + @DisplayName("토픽의 생성자인 경우 이미지를 변경할 수 있다.") + void updateTopicImage_SuccessByCreator() { + // given + String originalImageUrl = "https://originalImageUrl"; + Topic topic = TopicFixture.createPublicAndAllMembersTopic(originalImageUrl, member); + topicRepository.save(topic); + AuthMember authMember = MemberFixture.createUser(member); + + // when + topicCommandService.updateTopicImage(authMember, topic.getId(), createFile()); + Topic changedTopic = topicRepository.findById(topic.getId()) + .orElseThrow(NoSuchElementException::new); + + // then + assertAll( + () -> assertThat(topic).usingRecursiveComparison() + .ignoringFields("imageUrl") + .isEqualTo(changedTopic), + () -> assertThat(topic.getTopicInfo().getImageUrl()) + .isEqualTo("https://map-befine-official.github.io/favicon.png") + ); + } + + @Test + @DisplayName("Admin인 경우 이미지를 변경할 수 있다.") + void updateTopicImage_SuccessByAdmin() { + // given + String originalImageUrl = "https://originalImageUrl"; + Topic topic = TopicFixture.createPublicAndAllMembersTopic(originalImageUrl, member); + topicRepository.save(topic); + Member adminMember = memberRepository.save( + MemberFixture.create("관리자", "admin@naver.com", Role.ADMIN) + ); + AuthMember adminAuthMember = new Admin(adminMember.getId()); + + // when + topicCommandService.updateTopicImage(adminAuthMember, topic.getId(), createFile()); + Topic changedTopic = topicRepository.findById(topic.getId()) + .orElseThrow(NoSuchElementException::new); + + // then + assertAll( + () -> assertThat(topic).usingRecursiveComparison() + .ignoringFields("imageUrl") + .isEqualTo(changedTopic), + () -> assertThat(topic.getTopicInfo().getImageUrl()) + .isEqualTo("https://map-befine-official.github.io/favicon.png") + ); + } + } diff --git a/backend/src/test/java/com/mapbefine/mapbefine/topic/application/TopicQueryServiceTest.java b/backend/src/test/java/com/mapbefine/mapbefine/topic/application/TopicQueryServiceTest.java index 3657dbb5..98790d4b 100644 --- a/backend/src/test/java/com/mapbefine/mapbefine/topic/application/TopicQueryServiceTest.java +++ b/backend/src/test/java/com/mapbefine/mapbefine/topic/application/TopicQueryServiceTest.java @@ -3,6 +3,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; +import com.mapbefine.mapbefine.TestDatabaseContainer; import com.mapbefine.mapbefine.auth.domain.AuthMember; import com.mapbefine.mapbefine.auth.domain.member.Admin; import com.mapbefine.mapbefine.auth.domain.member.Guest; @@ -27,16 +28,15 @@ import com.mapbefine.mapbefine.topic.dto.response.TopicResponse; import com.mapbefine.mapbefine.topic.exception.TopicException.TopicForbiddenException; import com.mapbefine.mapbefine.topic.exception.TopicException.TopicNotFoundException; +import java.util.Collections; +import java.util.List; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; -import java.util.Collections; -import java.util.List; - @ServiceTest -class TopicQueryServiceTest { +class TopicQueryServiceTest extends TestDatabaseContainer { @Autowired private TopicRepository topicRepository; @@ -206,7 +206,7 @@ void findAllReadableWithoutBookmark_Success() { assertThat(topics).extractingResultOf("isBookmarked") .containsExactlyInAnyOrder(Boolean.FALSE, Boolean.FALSE); } - + @Test @DisplayName("권한이 있는 토픽을 ID로 조회한다.") void findDetailById_Success() { @@ -225,6 +225,25 @@ void findDetailById_Success() { assertThat(detail.name()).isEqualTo("아무나 읽을 수 있는 토픽"); } + @Test + @DisplayName("토픽 상세 조회 시 삭제된 핀은 볼 수 없다. (soft delete 반영)") + void findDetailById_Success_getPinsOnlyNotDeleted() { + //given + Topic topic = TopicFixture.createPublicAndAllMembersTopic(member); + Location location = LocationFixture.create(); + Pin pin = PinFixture.create(location, topic, member); + locationRepository.save(location); + topicRepository.save(topic); + pinRepository.save(pin); + pinRepository.deleteById(pin.getId()); + + //when + TopicDetailResponse response = topicQueryService.findDetailById(new Admin(member.getId()), topic.getId()); + + //then + assertThat(response.pins()).isEmpty(); + } + @Test @DisplayName("토픽 상세 조회 시 토픽의 변경일자는 핀의 최신 변경 일자이다.") void findDetailById_Success_lastPinUpdatedAt() { diff --git a/backend/src/test/java/com/mapbefine/mapbefine/topic/domain/TopicRepositoryTest.java b/backend/src/test/java/com/mapbefine/mapbefine/topic/domain/TopicRepositoryTest.java index c50e5f4d..92c8299d 100644 --- a/backend/src/test/java/com/mapbefine/mapbefine/topic/domain/TopicRepositoryTest.java +++ b/backend/src/test/java/com/mapbefine/mapbefine/topic/domain/TopicRepositoryTest.java @@ -2,6 +2,8 @@ import static org.assertj.core.api.Assertions.assertThat; +import com.mapbefine.mapbefine.TestDatabaseContainer; +import com.mapbefine.mapbefine.common.annotation.RepositoryTest; import com.mapbefine.mapbefine.member.MemberFixture; import com.mapbefine.mapbefine.member.domain.Member; import com.mapbefine.mapbefine.member.domain.MemberRepository; @@ -13,10 +15,8 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; -import java.util.List; - -@DataJpaTest -class TopicRepositoryTest { +@RepositoryTest +class TopicRepositoryTest extends TestDatabaseContainer { @Autowired private TopicRepository topicRepository; @@ -43,8 +43,7 @@ void deleteById_Success() { topicRepository.deleteById(topic.getId()); //then - Topic deletedTopic = topicRepository.findById(topic.getId()).get(); - assertThat(deletedTopic.isDeleted()).isTrue(); + assertThat(topicRepository.existsById(topic.getId())).isFalse(); } @Test @@ -62,9 +61,7 @@ void deleteAllByMemberId_Success() { topicRepository.deleteAllByMemberId(member.getId()); //then - List deletedTopics = topicRepository.findAllByCreatorId(member.getId()); - assertThat(deletedTopics).hasSize(10) - .extractingResultOf("isDeleted") - .containsOnly(true); + assertThat(topicRepository.findAllByCreatorId(member.getId())) + .isEmpty(); } } diff --git a/backend/src/test/java/com/mapbefine/mapbefine/topic/domain/TopicTest.java b/backend/src/test/java/com/mapbefine/mapbefine/topic/domain/TopicTest.java index 3d98d2dc..722c2e16 100644 --- a/backend/src/test/java/com/mapbefine/mapbefine/topic/domain/TopicTest.java +++ b/backend/src/test/java/com/mapbefine/mapbefine/topic/domain/TopicTest.java @@ -1,6 +1,7 @@ package com.mapbefine.mapbefine.topic.domain; import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; import com.mapbefine.mapbefine.location.LocationFixture; import com.mapbefine.mapbefine.member.MemberFixture; @@ -35,25 +36,44 @@ void setUp() { } @Test - @DisplayName("토픽 정보를 변경한다.") + @DisplayName("이미지를 제외한 토픽 정보를 변경한다.") void updateTopicInfo() { //given String name = "New Topic"; String description = "New Description"; - String imageUrl = "https://example.com/image.png"; //when topic.updateTopicInfo( name, - description, - imageUrl + description ); TopicInfo topicInfo = topic.getTopicInfo(); //then assertThat(topicInfo.getName()).isEqualTo(name); assertThat(topicInfo.getDescription()).isEqualTo(description); - assertThat(topicInfo.getImageUrl()).isEqualTo(imageUrl); + } + + @Test + @DisplayName("토픽의 이미지를 변경한다.") + void updateTopicImageUrl() { + //given + String imageUrl = "https://changedImageUrl"; + String originalName = topic.getTopicInfo().getName(); + String originalDescription = topic.getTopicInfo().getDescription(); + + //when + topic.updateTopicImageUrl( + imageUrl + ); + TopicInfo topicInfo = topic.getTopicInfo(); + + //then + assertAll( + () -> assertThat(topicInfo.getImageUrl()).isEqualTo(imageUrl), + () -> assertThat(topicInfo.getName()).isEqualTo(originalName), + () -> assertThat(topicInfo.getDescription()).isEqualTo(originalDescription) + ); } @Test diff --git a/backend/src/test/java/com/mapbefine/mapbefine/topic/presentation/TopicControllerTest.java b/backend/src/test/java/com/mapbefine/mapbefine/topic/presentation/TopicControllerTest.java index 261e558d..e1c94953 100644 --- a/backend/src/test/java/com/mapbefine/mapbefine/topic/presentation/TopicControllerTest.java +++ b/backend/src/test/java/com/mapbefine/mapbefine/topic/presentation/TopicControllerTest.java @@ -3,8 +3,13 @@ import static org.apache.http.HttpHeaders.AUTHORIZATION; import static org.mockito.ArgumentMatchers.any; import static org.mockito.BDDMockito.given; +import static org.springframework.http.HttpMethod.POST; +import static org.springframework.http.HttpMethod.PUT; +import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; +import static org.springframework.http.MediaType.IMAGE_PNG_VALUE; import com.mapbefine.mapbefine.common.RestDocsIntegration; +import com.mapbefine.mapbefine.image.FileFixture; import com.mapbefine.mapbefine.pin.dto.response.PinResponse; import com.mapbefine.mapbefine.topic.application.TopicCommandService; import com.mapbefine.mapbefine.topic.application.TopicQueryService; @@ -15,19 +20,17 @@ import com.mapbefine.mapbefine.topic.dto.request.TopicUpdateRequest; import com.mapbefine.mapbefine.topic.dto.response.TopicDetailResponse; import com.mapbefine.mapbefine.topic.dto.response.TopicResponse; -import java.io.File; import java.time.LocalDateTime; import java.util.List; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.http.MediaType; +import org.springframework.mock.web.MockMultipartFile; import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; -import org.springframework.util.LinkedMultiValueMap; -import org.springframework.util.MultiValueMap; +import org.springframework.test.web.servlet.result.MockMvcResultMatchers; -class TopicControllerTest extends RestDocsIntegration { // TODO: 2023/07/25 Image 칼람 추가됨으로 인해 수정 필요 +class TopicControllerTest extends RestDocsIntegration { private static final List RESPONSES = List.of(new TopicResponse( 1L, @@ -51,30 +54,15 @@ class TopicControllerTest extends RestDocsIntegration { // TODO: 2023/07/25 Imag LocalDateTime.now() )); - private File mockFile; - @MockBean private TopicCommandService topicCommandService; @MockBean private TopicQueryService topicQueryService; - @BeforeEach - void setUp() { - mockFile = new File( - getClass().getClassLoader() - .getResource("test.png") - .getPath() - ); - } - - @Test @DisplayName("토픽 새로 생성") void create() throws Exception { given(topicCommandService.saveTopic(any(), any())).willReturn(1L); - File mockFile = new File(getClass().getClassLoader().getResource("test.png").getPath()); - MultiValueMap param = new LinkedMultiValueMap<>(); - TopicCreateRequestWithoutImage request = new TopicCreateRequestWithoutImage( "준팍의 안갈집", "준팍이 두번 다시 안갈집", @@ -82,16 +70,17 @@ void create() throws Exception { PermissionType.ALL_MEMBERS, List.of(1L, 2L, 3L) ); + MockMultipartFile requestJson = new MockMultipartFile("request", "request", APPLICATION_JSON_VALUE, + objectMapper.writeValueAsBytes(request)); + MockMultipartFile image = new MockMultipartFile("image", "test.png", IMAGE_PNG_VALUE, "data".getBytes()); - param.add("image", mockFile); - param.add("request", request); - - mockMvc.perform( - MockMvcRequestBuilders.post("/topics/new") + mockMvc.perform(MockMvcRequestBuilders.multipart(POST, "/topics/new") + .file(image) + .file(requestJson) .header(AUTHORIZATION, testAuthHeaderProvider.createAuthHeaderById(1L)) - .contentType(MediaType.MULTIPART_FORM_DATA_VALUE) - .content(objectMapper.writeValueAsString(param)) - ).andDo(restDocs.document()); + .contentType(MediaType.MULTIPART_FORM_DATA_VALUE)) + .andExpect(MockMvcResultMatchers.status().isCreated()) + .andDo(restDocs.document()); } @Test @@ -99,9 +88,6 @@ void create() throws Exception { void mergeAndCreate() throws Exception { given(topicCommandService.merge(any(), any())).willReturn(1L); - MultiValueMap param = new LinkedMultiValueMap<>(); - - TopicMergeRequestWithoutImage request = new TopicMergeRequestWithoutImage( "준팍의 안갈집", "준팍이 두번 다시 안갈집", @@ -109,26 +95,25 @@ void mergeAndCreate() throws Exception { PermissionType.ALL_MEMBERS, List.of(1L, 2L, 3L) ); - - param.add("image", mockFile); - param.add("request", request); - - mockMvc.perform( - MockMvcRequestBuilders.post("/topics/merge") - .header(AUTHORIZATION, testAuthHeaderProvider.createAuthHeaderById(1L)) - .contentType(MediaType.MULTIPART_FORM_DATA_VALUE) - .content(objectMapper.writeValueAsString(param)) - ).andDo(restDocs.document()); + MockMultipartFile requestJson = new MockMultipartFile("request", "request", APPLICATION_JSON_VALUE, + objectMapper.writeValueAsBytes(request)); + MockMultipartFile image = new MockMultipartFile("image", "test.png", IMAGE_PNG_VALUE, "data".getBytes()); + + mockMvc.perform(MockMvcRequestBuilders.multipart(POST, "/topics/merge") + .file(image) + .file(requestJson) + .header(AUTHORIZATION, testAuthHeaderProvider.createAuthHeaderById(1L))) + .andExpect(MockMvcResultMatchers.status().isCreated()) + .andDo(restDocs.document()); } @Test @DisplayName("핀을 권한이 있는 토픽에 복사할 수 있다.") void copyPin() throws Exception { - - mockMvc.perform( - MockMvcRequestBuilders.post("/topics/1/copy?pinIds=1,2,3") - .header(AUTHORIZATION, testAuthHeaderProvider.createAuthHeaderById(1L)) - ).andDo(restDocs.document()); + mockMvc.perform(MockMvcRequestBuilders.post("/topics/1/copy?pinIds=1,2,3") + .header(AUTHORIZATION, testAuthHeaderProvider.createAuthHeaderById(1L))) + .andExpect(MockMvcResultMatchers.status().isOk()) + .andDo(restDocs.document()); } @Test @@ -136,27 +121,39 @@ void copyPin() throws Exception { void update() throws Exception { TopicUpdateRequest topicUpdateRequest = new TopicUpdateRequest( "준팍의 안갈집", - "https://map-befine-official.github.io/favicon.png", "준팍이 두번 다시 안갈집", Publicity.PUBLIC, PermissionType.ALL_MEMBERS ); - mockMvc.perform( - MockMvcRequestBuilders.put("/topics/1") + mockMvc.perform(MockMvcRequestBuilders.put("/topics/1") .header(AUTHORIZATION, testAuthHeaderProvider.createAuthHeaderById(1L)) .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(topicUpdateRequest)) - ).andDo(restDocs.document()); + .content(objectMapper.writeValueAsString(topicUpdateRequest))) + .andExpect(MockMvcResultMatchers.status().isOk()) + .andDo(restDocs.document()); + } + + @Test + @DisplayName("토픽 이미지 수정") + void updateImage() throws Exception { + MockMultipartFile image = FileFixture.createFile(); + + mockMvc.perform(MockMvcRequestBuilders.multipart(PUT, "/topics/images/1") + .file(image) + .header(AUTHORIZATION, testAuthHeaderProvider.createAuthHeaderById(1L)) + .contentType(MediaType.MULTIPART_FORM_DATA_VALUE)) + .andExpect(MockMvcResultMatchers.status().isOk()) + .andDo(restDocs.document()); } @Test @DisplayName("토픽 삭제") void delete() throws Exception { - mockMvc.perform( - MockMvcRequestBuilders.delete("/topics/1") - .header(AUTHORIZATION, testAuthHeaderProvider.createAuthHeaderById(1L)) - ).andDo(restDocs.document()); + mockMvc.perform(MockMvcRequestBuilders.delete("/topics/1") + .header(AUTHORIZATION, testAuthHeaderProvider.createAuthHeaderById(1L))) + .andExpect(MockMvcResultMatchers.status().isNoContent()) + .andDo(restDocs.document()); } @Test @@ -164,10 +161,10 @@ void delete() throws Exception { void findAll() throws Exception { given(topicQueryService.findAllReadable(any())).willReturn(RESPONSES); - mockMvc.perform( - MockMvcRequestBuilders.get("/topics") - .header(AUTHORIZATION, testAuthHeaderProvider.createAuthHeaderById(1L)) - ).andDo(restDocs.document()); + mockMvc.perform(MockMvcRequestBuilders.get("/topics") + .header(AUTHORIZATION, testAuthHeaderProvider.createAuthHeaderById(1L))) + .andExpect(MockMvcResultMatchers.status().isOk()) + .andDo(restDocs.document()); } @Test @@ -207,10 +204,10 @@ void findById() throws Exception { ); given(topicQueryService.findDetailById(any(), any())).willReturn(topicDetailResponse); - mockMvc.perform( - MockMvcRequestBuilders.get("/topics/1") - .header(AUTHORIZATION, testAuthHeaderProvider.createAuthHeaderById(1L)) - ).andDo(restDocs.document()); + mockMvc.perform(MockMvcRequestBuilders.get("/topics/1") + .header(AUTHORIZATION, testAuthHeaderProvider.createAuthHeaderById(1L))) + .andExpect(MockMvcResultMatchers.status().isOk()) + .andDo(restDocs.document()); } @Test @@ -218,10 +215,10 @@ void findById() throws Exception { void findAllByOrderByUpdatedAtDesc() throws Exception { given(topicQueryService.findAllByOrderByUpdatedAtDesc(any())).willReturn(RESPONSES); - mockMvc.perform( - MockMvcRequestBuilders.get("/topics/newest") - .header(AUTHORIZATION, testAuthHeaderProvider.createAuthHeaderById(1L)) - ).andDo(restDocs.document()); + mockMvc.perform(MockMvcRequestBuilders.get("/topics/newest") + .header(AUTHORIZATION, testAuthHeaderProvider.createAuthHeaderById(1L))) + .andExpect(MockMvcResultMatchers.status().isOk()) + .andDo(restDocs.document()); } @Test @@ -229,10 +226,10 @@ void findAllByOrderByUpdatedAtDesc() throws Exception { void findAllTopicsByMemberId() throws Exception { given(topicQueryService.findAllTopicsByMemberId(any(), any())).willReturn(RESPONSES); - mockMvc.perform( - MockMvcRequestBuilders.get("/topics/members?id=1") - .header(AUTHORIZATION, testAuthHeaderProvider.createAuthHeaderById(1L)) - ).andDo(restDocs.document()); + mockMvc.perform(MockMvcRequestBuilders.get("/topics/members?id=1") + .header(AUTHORIZATION, testAuthHeaderProvider.createAuthHeaderById(1L))) + .andExpect(MockMvcResultMatchers.status().isOk()) + .andDo(restDocs.document()); } @Test @@ -240,10 +237,10 @@ void findAllTopicsByMemberId() throws Exception { void findAllBestTopics() throws Exception { given(topicQueryService.findAllBestTopics(any())).willReturn(RESPONSES); - mockMvc.perform( - MockMvcRequestBuilders.get("/topics/bests") - .header(AUTHORIZATION, testAuthHeaderProvider.createAuthHeaderById(1L)) - ).andDo(restDocs.document()); + mockMvc.perform(MockMvcRequestBuilders.get("/topics/bests") + .header(AUTHORIZATION, testAuthHeaderProvider.createAuthHeaderById(1L))) + .andExpect(MockMvcResultMatchers.status().isOk()) + .andDo(restDocs.document()); } }