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/atlas/domain/AtlasRepository.java b/backend/src/main/java/com/mapbefine/mapbefine/atlas/domain/AtlasRepository.java index ede90f46..358a481c 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,8 @@ 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.stereotype.Repository; @Repository @@ -10,7 +12,11 @@ public interface AtlasRepository extends JpaRepository { void deleteByMemberIdAndTopicId(Long memberId, Long topicId); + @Modifying(clearAutomatically = true) + @Query("delete from Atlas a where a.member.id = :memberId") void deleteAllByMemberId(Long memberId); + @Modifying(clearAutomatically = true) + @Query("delete from Atlas a where a.topic.id = :topicId") void deleteAllByTopicId(Long 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..602eb13b 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,8 @@ 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; public interface BookmarkRepository extends JpaRepository { @@ -9,8 +11,12 @@ public interface BookmarkRepository extends JpaRepository { boolean existsByMemberIdAndTopicId(Long memberId, Long topicId); + @Modifying(clearAutomatically = true) + @Query("delete from Bookmark b where b.member.id = :memberId") void deleteAllByMemberId(Long memberId); + @Modifying(clearAutomatically = true) + @Query("delete from Bookmark b where b.topic.id = :topicId") void deleteAllByTopicId(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/permission/domain/PermissionRepository.java b/backend/src/main/java/com/mapbefine/mapbefine/permission/domain/PermissionRepository.java index c8300ce4..6cbbae0b 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,8 @@ 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; public interface PermissionRepository extends JpaRepository { @@ -9,8 +11,12 @@ public interface PermissionRepository extends JpaRepository { boolean existsByTopicIdAndMemberId(Long topicId, Long memberId); + @Modifying(clearAutomatically = true) + @Query("delete from Permission p where p.member.id = :memberId") void deleteAllByMemberId(Long memberId); + @Modifying(clearAutomatically = true) + @Query("delete from Permission p where p.topic.id = :topicId") void deleteAllByTopicId(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 4a31ef1f..f9263c11 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 @@ -22,6 +22,7 @@ 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.PinException.PinBadRequestException; import com.mapbefine.mapbefine.pin.exception.PinException.PinForbiddenException; import com.mapbefine.mapbefine.topic.domain.Topic; @@ -30,16 +31,20 @@ 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 PinRepository pinRepository; private final LocationRepository locationRepository; private final TopicRepository topicRepository; @@ -48,6 +53,7 @@ public class PinCommandService { private final ImageService imageService; public PinCommandService( + ApplicationEventPublisher eventPublisher, PinRepository pinRepository, LocationRepository locationRepository, TopicRepository topicRepository, @@ -55,6 +61,7 @@ public PinCommandService( PinImageRepository pinImageRepository, ImageService imageService ) { + this.eventPublisher = eventPublisher; this.pinRepository = pinRepository; this.locationRepository = locationRepository; this.topicRepository = topicRepository; @@ -81,8 +88,8 @@ public long save( ); addPinImagesToPin(images, pin); - pinRepository.save(pin); + eventPublisher.publishEvent(new PinUpdateEvent(pin, member)); return pin.getId(); } @@ -139,10 +146,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) { 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..b205e3a5 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 @@ -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/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/resources/logback-spring.xml b/backend/src/main/resources/logback-spring.xml index 2b5ee962..d720ab2f 100644 --- a/backend/src/main/resources/logback-spring.xml +++ b/backend/src/main/resources/logback-spring.xml @@ -8,23 +8,25 @@ - - + - + + + + + + - + - - @@ -38,14 +40,12 @@ - - 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 43f04c12..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,7 +1,7 @@ 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; @@ -27,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; @@ -43,33 +42,24 @@ class AdminCommandServiceTest extends TestDatabaseContainer { @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; @@ -77,15 +67,14 @@ class AdminCommandServiceTest extends TestDatabaseContainer { @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 @@ -97,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(); @@ -115,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은 토픽을 삭제시킬 수 있다.") @@ -146,8 +132,6 @@ void deleteTopic_Success() { @Test void deleteTopicImage_Success() { //given - TopicInfo topicInfo = topic.getTopicInfo(); - topic.updateTopicImageUrl("https://imageUrl.png"); assertThat(topic.getTopicInfo().getImageUrl()).isEqualTo("https://imageUrl.png"); 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/pin/PinIntegrationTest.java b/backend/src/test/java/com/mapbefine/mapbefine/pin/PinIntegrationTest.java index 031f0da8..42fee1f0 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,10 +14,12 @@ 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.PinRepository; 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.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; @@ -24,8 +29,10 @@ 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 +47,9 @@ class PinIntegrationTest extends IntegrationTest { private PinCreateRequest createRequestNoDuplicateLocation; private PinCreateRequest createRequestNoDuplicateLocation2; + + @MockBean + private PinHistoryCommandService pinHistoryCommandService; @Autowired private MemberRepository memberRepository; @@ -48,6 +58,8 @@ class PinIntegrationTest extends IntegrationTest { @Autowired private LocationRepository locationRepository; + @Autowired + private PinRepository pinRepository; @BeforeEach void saveTopicAndLocation() { @@ -114,6 +126,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 +164,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 +214,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 +313,45 @@ void findAllPinsByMemberId_Success() { assertThat(pinResponses).hasSize(1); } + @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 c925e03c..ce7b4227 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 @@ -2,12 +2,17 @@ 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; @@ -26,16 +31,19 @@ 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.PinException.PinForbiddenException; import com.mapbefine.mapbefine.topic.TopicFixture; import com.mapbefine.mapbefine.topic.domain.Topic; import com.mapbefine.mapbefine.topic.domain.TopicRepository; +import java.util.Collections; import java.util.List; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.web.multipart.MultipartFile; @ServiceTest @@ -44,10 +52,13 @@ 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 @@ -61,13 +72,12 @@ class PinCommandServiceTest extends TestDatabaseContainer { private Location location; private Topic topic; - private Member member; 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)); location = locationRepository.save(LocationFixture.create()); topic = topicRepository.save(TopicFixture.createByName("topic", member)); @@ -151,6 +161,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() { @@ -158,7 +189,6 @@ void save_FailByForbidden() { .isInstanceOf(PinForbiddenException.class); } - @Test @DisplayName("핀을 변경하면 토픽에 핀의 변경 일시를 새로 반영한다. (모든 일시는 영속화 시점 기준이다.)") void update_Success_UpdateLastPinsAddedAt() { @@ -179,6 +209,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() {