diff --git a/.github/workflows/be-merge-dev.yml b/.github/workflows/be-merge-dev.yml index 33b2781d..03c2969a 100644 --- a/.github/workflows/be-merge-dev.yml +++ b/.github/workflows/be-merge-dev.yml @@ -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..2a716106 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-dev . + + - 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/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..6ba556c1 --- /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 e957fd79..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 @@ -54,6 +54,7 @@ public AdminCommandService( public void blockMember(Long memberId) { Member member = findMemberById(memberId); member.updateStatus(Status.BLOCKED); + memberRepository.flush(); deleteAllRelatedMember(member); } @@ -63,11 +64,8 @@ private void deleteAllRelatedMember(Member member) { Long memberId = member.getId(); permissionRepository.deleteAllByMemberId(memberId); - permissionRepository.flush(); atlasRepository.deleteAllByMemberId(memberId); - atlasRepository.flush(); bookmarkRepository.deleteAllByMemberId(memberId); - bookmarkRepository.flush(); pinImageRepository.deleteAllByPinIds(pinIds); pinRepository.deleteAllByMemberId(memberId); topicRepository.deleteAllByMemberId(memberId); @@ -90,11 +88,8 @@ public void deleteTopic(Long topicId) { List pinIds = extractPinIdsByTopic(topic); permissionRepository.deleteAllByTopicId(topicId); - permissionRepository.flush(); atlasRepository.deleteAllByTopicId(topicId); - atlasRepository.flush(); bookmarkRepository.deleteAllByTopicId(topicId); - bookmarkRepository.flush(); pinImageRepository.deleteAllByPinIds(pinIds); pinRepository.deleteAllByTopicId(topicId); topicRepository.deleteById(topicId); 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/domain/AtlasRepository.java b/backend/src/main/java/com/mapbefine/mapbefine/atlas/domain/AtlasRepository.java index ede90f46..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,6 +1,9 @@ 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 @@ -10,7 +13,11 @@ public interface AtlasRepository extends JpaRepository { 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); - void deleteAllByTopicId(Long topicId); + @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/domain/AuthMember.java b/backend/src/main/java/com/mapbefine/mapbefine/auth/domain/AuthMember.java index 3bfc0767..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 @@ -27,6 +27,8 @@ protected AuthMember( public abstract boolean canTopicUpdate(Topic topic); public abstract boolean canPinCreateOrUpdate(Topic topic); + + public abstract boolean canPinCommentCreate(Topic topic); public Long getMemberId() { return memberId; @@ -36,4 +38,10 @@ 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 3d97522c..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 @@ -34,4 +34,24 @@ public boolean canPinCreateOrUpdate(Topic topic) { return true; } + @Override + 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 6c8c0edc..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 @@ -36,4 +36,24 @@ public boolean canPinCreateOrUpdate(Topic topic) { return false; } + @Override + 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 d2444d49..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 @@ -22,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()); } @@ -39,19 +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); + } + + @Override + public boolean isAdmin() { + return false; } - private boolean isGroup(Long topicId) { - return isCreator(topicId) || hasPermission(topicId); + @Override + public boolean isUser() { + return true; + } + + @Override + public boolean isGuest() { + return false; + } + + private boolean isCreator(Long topicId) { + return createdTopic.contains(topicId); } private boolean hasPermission(Long topicId) { - return createdTopic.contains(topicId) || topicsWithPermission.contains(topicId); + return isCreator(topicId) || topicsWithPermission.contains(topicId); } } 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 bb97144c..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 @@ -2,6 +2,9 @@ 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; public interface BookmarkRepository extends JpaRepository { @@ -9,8 +12,12 @@ public interface BookmarkRepository extends JpaRepository { boolean existsByMemberIdAndTopicId(Long memberId, Long topicId); - void deleteAllByMemberId(Long memberId); + @Modifying(clearAutomatically = true) + @Query("delete from Bookmark b where b.member.id = :memberId") + void deleteAllByMemberId(@Param("memberId") Long memberId); - void deleteAllByTopicId(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/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/MemberQueryService.java b/backend/src/main/java/com/mapbefine/mapbefine/member/application/MemberQueryService.java index 688a6db5..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 @@ -9,7 +9,6 @@ import com.mapbefine.mapbefine.member.dto.response.MemberDetailResponse; import com.mapbefine.mapbefine.member.dto.response.MemberResponse; import com.mapbefine.mapbefine.member.exception.MemberErrorCode; -import com.mapbefine.mapbefine.member.exception.MemberException.MemberForbiddenException; import com.mapbefine.mapbefine.member.exception.MemberException.MemberNotFoundException; import com.mapbefine.mapbefine.pin.dto.response.PinResponse; import com.mapbefine.mapbefine.topic.domain.Topic; @@ -41,12 +40,10 @@ public MemberQueryService( this.topicRepository = topicRepository; } - public MemberDetailResponse findById(AuthMember authMember, Long id) { - if (authMember.isSameMember(id)) { - Member member = findMemberById(id); - return MemberDetailResponse.from(member); - } - throw new MemberForbiddenException(MemberErrorCode.FORBIDDEN_ACCESS, id); + public MemberDetailResponse findMemberDetail(AuthMember authMember) { + Member member = findMemberById(authMember.getMemberId()); + + return MemberDetailResponse.from(member); } private Member findMemberById(Long id) { @@ -77,13 +74,6 @@ 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); } @@ -101,6 +91,13 @@ 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); } 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 46d6c712..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(AuthMember authMember, @PathVariable Long memberId) { - MemberDetailResponse response = memberQueryService.findById(authMember, 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/domain/PermissionRepository.java b/backend/src/main/java/com/mapbefine/mapbefine/permission/domain/PermissionRepository.java index c8300ce4..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,8 +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); - void deleteAllByTopicId(Long topicId); + @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/pin/application/PinCommandService.java b/backend/src/main/java/com/mapbefine/mapbefine/pin/application/PinCommandService.java index 1f3344a3..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,12 +20,19 @@ 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; @@ -30,37 +41,46 @@ 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( @@ -81,8 +101,8 @@ public long save( ); addPinImagesToPin(images, pin); - pinRepository.save(pin); + eventPublisher.publishEvent(new PinUpdateEvent(pin, member)); return pin.getId(); } @@ -114,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() @@ -138,10 +159,12 @@ 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) { @@ -192,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 b2f4a1c0..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,13 +5,17 @@ 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; @@ -20,8 +24,10 @@ 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; } @@ -56,4 +62,29 @@ public List findAllPinsByMemberId(AuthMember authMember, Long membe .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 bfabaa86..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 @@ -48,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) @@ -138,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/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 5c86b3b7..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 @@ -223,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()); } @@ -252,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/domain/Topic.java b/backend/src/main/java/com/mapbefine/mapbefine/topic/domain/Topic.java index c61f5cc7..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 @@ -105,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) { @@ -119,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; } 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 1c37b567..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 @@ -146,6 +146,21 @@ public ResponseEntity> findAllBestTopics(AuthMember authMemb 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}") 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/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 1e45c96f..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,7 +27,6 @@ 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; @@ -35,40 +35,31 @@ 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; @Autowired TestEntityManager testEntityManager; - private Location location; - private Member admin; private Member member; private Topic topic; private Pin pin; @@ -76,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 @@ -96,16 +86,15 @@ void blockMember_Success() { atlasRepository.save(atlas); permissionRepository.save(permission); - assertAll( - () -> assertThat(member.getMemberInfo().getStatus()).isEqualTo(Status.NORMAL), - () -> assertThat(topic.isDeleted()).isFalse(), - () -> assertThat(pin.isDeleted()).isFalse(), - () -> assertThat(pinImage.isDeleted()).isFalse(), - () -> assertThat(bookmarkRepository.existsByMemberIdAndTopicId(member.getId(), topic.getId())).isTrue(), - () -> assertThat(atlasRepository.existsByMemberIdAndTopicId(member.getId(), topic.getId())).isTrue(), - () -> assertThat(permissionRepository.existsByTopicIdAndMemberId(topic.getId(), member.getId())) - .isTrue() - ); + assertSoftly(softly -> { + assertThat(member.getMemberInfo().getStatus()).isEqualTo(Status.NORMAL); + assertThat(topic.isDeleted()).isFalse(); + assertThat(pin.isDeleted()).isFalse(); + assertThat(pinImage.isDeleted()).isFalse(); + assertThat(bookmarkRepository.existsByMemberIdAndTopicId(member.getId(), topic.getId())).isTrue(); + assertThat(atlasRepository.existsByMemberIdAndTopicId(member.getId(), topic.getId())).isTrue(); + assertThat(permissionRepository.existsByTopicIdAndMemberId(topic.getId(), member.getId())).isTrue(); + }); //when testEntityManager.clear(); @@ -114,17 +103,15 @@ void blockMember_Success() { //then Member blockedMember = memberRepository.findById(member.getId()).get(); - assertAll( - () -> 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() - ); + 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(); + }); } @DisplayName("Admin은 토픽을 삭제시킬 수 있다.") @@ -145,9 +132,7 @@ void deleteTopic_Success() { @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"); 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 fbef9e48..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 @@ -2,6 +2,7 @@ import static org.assertj.core.api.Assertions.assertThat; +import com.mapbefine.mapbefine.TestDatabaseContainer; import com.mapbefine.mapbefine.admin.dto.AdminMemberDetailResponse; import com.mapbefine.mapbefine.admin.dto.AdminMemberResponse; import com.mapbefine.mapbefine.common.annotation.ServiceTest; @@ -26,7 +27,7 @@ import org.springframework.beans.factory.annotation.Autowired; @ServiceTest -class AdminQueryServiceTest { +class AdminQueryServiceTest extends TestDatabaseContainer { @Autowired private AdminQueryService adminQueryService; 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 index aafc4e1b..54375ba8 100644 --- a/backend/src/test/java/com/mapbefine/mapbefine/auth/application/AuthServiceTest.java +++ b/backend/src/test/java/com/mapbefine/mapbefine/auth/application/AuthServiceTest.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.admin.application.AdminCommandService; import com.mapbefine.mapbefine.auth.domain.AuthMember; import com.mapbefine.mapbefine.common.annotation.ServiceTest; @@ -24,7 +25,7 @@ import org.springframework.transaction.annotation.Transactional; @ServiceTest -class AuthServiceTest { +class AuthServiceTest extends TestDatabaseContainer { @Autowired private AuthService authService; 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 2c80c964..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), @@ -30,4 +33,5 @@ } ) 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 dd83e8da..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; @@ -32,7 +33,7 @@ import org.springframework.beans.factory.annotation.Autowired; @ServiceTest -class LocationQueryServiceTest { +class LocationQueryServiceTest extends TestDatabaseContainer { @Autowired private LocationRepository locationRepository; 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 3e631be9..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 @@ -4,10 +4,12 @@ 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; @@ -20,7 +22,7 @@ import com.mapbefine.mapbefine.member.domain.Role; import com.mapbefine.mapbefine.member.dto.response.MemberDetailResponse; import com.mapbefine.mapbefine.member.dto.response.MemberResponse; -import com.mapbefine.mapbefine.member.exception.MemberException.MemberForbiddenException; +import com.mapbefine.mapbefine.member.exception.MemberException.MemberNotFoundException; import com.mapbefine.mapbefine.pin.PinFixture; import com.mapbefine.mapbefine.pin.domain.Pin; import com.mapbefine.mapbefine.pin.domain.PinRepository; @@ -29,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; @@ -36,7 +39,7 @@ import org.springframework.beans.factory.annotation.Autowired; @ServiceTest -class MemberQueryServiceTest { +class MemberQueryServiceTest extends TestDatabaseContainer { @Autowired private MemberQueryService memberQueryService; @@ -101,11 +104,11 @@ void findAllMember() { } @Test - @DisplayName("회원을 단일 조회한다.") - void findMemberById() { + @DisplayName("로그인 회원의 상세 정보를 조회한다.") + void findMyProfile() { // given // when - MemberDetailResponse response = memberQueryService.findById(authMember, member.getId()); + MemberDetailResponse response = memberQueryService.findMemberDetail(authMember); // then assertThat(response).usingRecursiveComparison() @@ -113,19 +116,14 @@ void findMemberById() { } @Test - @DisplayName("조회하려는 회원이 없는 경우 본인이 아니므로 예외를 반환한다.") - void findMemberById_whenNoneExists_thenFail() { - // given when then - assertThatThrownBy(() -> memberQueryService.findById(authMember, Long.MAX_VALUE)) - .isInstanceOf(MemberForbiddenException.class); - } + @DisplayName("로그인한 식별값(ID)에 해당하는 회원 정보를 찾을 수 없는 경우 예외를 반환한다.") + void findMyProfile_whenNoneExists_thenFail() { + // given + AuthMember otherAuthMember = new User(Long.MAX_VALUE, Collections.emptyList(), Collections.emptyList()); - @Test - @DisplayName("조회하려는 회원이 본인이 아닌 경우 예외를 반환한다.") - void findMemberById_whenNotSameMember_thenFail() { - // given when then - assertThatThrownBy(() -> memberQueryService.findById(authMember, Long.MAX_VALUE)) - .isInstanceOf(MemberForbiddenException.class); + // when then + assertThatThrownBy(() -> memberQueryService.findMemberDetail(otherAuthMember)) + .isInstanceOf(MemberNotFoundException.class); } @Test 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 c3a89587..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("회원 목록 조회") @@ -41,15 +45,15 @@ void findAllMember() throws Exception { 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", @@ -58,12 +62,12 @@ void findMemberById() throws Exception { LocalDateTime.now() ); - given(memberQueryService.findById(any(), 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 @@ -95,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 @@ -130,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 @@ -165,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 @@ -196,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 @@ -207,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 d06d2b33..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,32 +21,36 @@ 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 @@ -59,10 +64,10 @@ void findTopicAccessDetailByTopicId() throws Exception { 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 567e352f..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,37 +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; @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 @@ -57,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)); @@ -150,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() { @@ -178,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() { @@ -291,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 c3b20b38..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; @@ -21,8 +23,8 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; -@DataJpaTest -class PinImageRepositoryTest { +@RepositoryTest +class PinImageRepositoryTest extends TestDatabaseContainer { @Autowired private PinImageRepository pinImageRepository; 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 fd26deda..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,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.common.config.JpaConfig; import com.mapbefine.mapbefine.location.LocationFixture; import com.mapbefine.mapbefine.location.domain.Location; @@ -21,9 +23,8 @@ import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; import org.springframework.context.annotation.Import; -@Import(JpaConfig.class) -@DataJpaTest -class PinRepositoryTest { +@RepositoryTest +class PinRepositoryTest extends TestDatabaseContainer { @Autowired private TopicRepository topicRepository; 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 c66f1ae1..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; @@ -31,13 +36,14 @@ 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; @ServiceTest -class TopicCommandServiceTest { +class TopicCommandServiceTest extends TestDatabaseContainer { @Autowired private MemberRepository memberRepository; @@ -333,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 @@ -365,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 @@ -411,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 cc45bb7a..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; @@ -35,7 +36,7 @@ import org.springframework.beans.factory.annotation.Autowired; @ServiceTest -class TopicQueryServiceTest { +class TopicQueryServiceTest extends TestDatabaseContainer { @Autowired private TopicRepository topicRepository; 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 c419261d..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,8 +15,8 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; -@DataJpaTest -class TopicRepositoryTest { +@RepositoryTest +class TopicRepositoryTest extends TestDatabaseContainer { @Autowired private TopicRepository topicRepository; 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()); } }