diff --git a/.gitignore b/.gitignore index a8660af..39420aa 100644 --- a/.gitignore +++ b/.gitignore @@ -12,7 +12,6 @@ build/ ### images ### **/src/main/resources/static/ -**/src/test/resources/images/ ### STS ### .apt_generated diff --git a/src/main/java/com/inq/wishhair/wesharewishhair/photo/application/PhotoService.java b/src/main/java/com/inq/wishhair/wesharewishhair/photo/application/PhotoService.java index 5a8b419..dbf64bf 100644 --- a/src/main/java/com/inq/wishhair/wesharewishhair/photo/application/PhotoService.java +++ b/src/main/java/com/inq/wishhair/wesharewishhair/photo/application/PhotoService.java @@ -25,15 +25,19 @@ public List uploadPhotos(final List files) { } @Transactional - public void deletePhotosByReviewId(final Review review) { + public boolean deletePhotosByReviewId(final Review review) { deletePhotosInCloud(review); photoRepository.deleteAllByReview(review.getId()); + + return true; } @Transactional - public void deletePhotosByWriter(final List reviews) { + public boolean deletePhotosByWriter(final List reviews) { reviews.forEach(this::deletePhotosInCloud); photoRepository.deleteAllByReviews(reviews.stream().map(Review::getId).toList()); + + return true; } private void deletePhotosInCloud(final Review review) { diff --git a/src/main/java/com/inq/wishhair/wesharewishhair/photo/domain/PhotoRepository.java b/src/main/java/com/inq/wishhair/wesharewishhair/photo/domain/PhotoRepository.java index 1c25028..f2c0844 100644 --- a/src/main/java/com/inq/wishhair/wesharewishhair/photo/domain/PhotoRepository.java +++ b/src/main/java/com/inq/wishhair/wesharewishhair/photo/domain/PhotoRepository.java @@ -1,9 +1,16 @@ package com.inq.wishhair.wesharewishhair.photo.domain; import java.util.List; +import java.util.Optional; public interface PhotoRepository { + Photo save(Photo photo); + + Optional findById(Long id); + + List findAll(); + void deleteAllByReview(Long reviewId); void deleteAllByReviews(List reviewIds); diff --git a/src/main/java/com/inq/wishhair/wesharewishhair/photo/domain/PhotoStore.java b/src/main/java/com/inq/wishhair/wesharewishhair/photo/domain/PhotoStore.java index cd6c2fb..0987ea2 100644 --- a/src/main/java/com/inq/wishhair/wesharewishhair/photo/domain/PhotoStore.java +++ b/src/main/java/com/inq/wishhair/wesharewishhair/photo/domain/PhotoStore.java @@ -8,5 +8,5 @@ public interface PhotoStore { List uploadFiles(List files); - void deleteFiles(List storeUrls); + boolean deleteFiles(List storeUrls); } diff --git a/src/main/java/com/inq/wishhair/wesharewishhair/photo/infrastructure/S3PhotoStore.java b/src/main/java/com/inq/wishhair/wesharewishhair/photo/infrastructure/S3PhotoStore.java index 456e15f..90166ae 100644 --- a/src/main/java/com/inq/wishhair/wesharewishhair/photo/infrastructure/S3PhotoStore.java +++ b/src/main/java/com/inq/wishhair/wesharewishhair/photo/infrastructure/S3PhotoStore.java @@ -61,8 +61,9 @@ private String uploadFile(final MultipartFile file) { } } - public void deleteFiles(final List storeUrls) { + public boolean deleteFiles(final List storeUrls) { storeUrls.forEach(this::deleteFile); + return true; } private void deleteFile(final String storeUrl) { diff --git a/src/main/java/com/inq/wishhair/wesharewishhair/review/domain/entity/Review.java b/src/main/java/com/inq/wishhair/wesharewishhair/review/domain/entity/Review.java index a63ae23..4e3f8c8 100644 --- a/src/main/java/com/inq/wishhair/wesharewishhair/review/domain/entity/Review.java +++ b/src/main/java/com/inq/wishhair/wesharewishhair/review/domain/entity/Review.java @@ -23,13 +23,12 @@ import jakarta.persistence.JoinColumn; import jakarta.persistence.ManyToOne; import jakarta.persistence.OneToMany; -import lombok.AccessLevel; import lombok.Getter; import lombok.NoArgsConstructor; @Entity @Getter -@NoArgsConstructor(access = AccessLevel.PROTECTED) +@NoArgsConstructor public class Review extends BaseEntity { @Id diff --git a/src/test/java/com/inq/wishhair/wesharewishhair/common/utils/FileMockingUtils.java b/src/test/java/com/inq/wishhair/wesharewishhair/common/utils/FileMockingUtils.java new file mode 100644 index 0000000..daf94d7 --- /dev/null +++ b/src/test/java/com/inq/wishhair/wesharewishhair/common/utils/FileMockingUtils.java @@ -0,0 +1,45 @@ +package com.inq.wishhair.wesharewishhair.common.utils; + +import java.io.FileInputStream; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import org.springframework.mock.web.MockMultipartFile; +import org.springframework.web.multipart.MultipartFile; + +import lombok.AccessLevel; +import lombok.NoArgsConstructor; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public abstract class FileMockingUtils { + + private static final String FILE_PATH = "src/test/resources/images/"; + private static final String FILE_META_NAME = "files"; + private static final String CONTENT_TYPE = "image/bmp"; + + public static MultipartFile createMockMultipartFile( + final String fileName + ) throws IOException { + try (final FileInputStream stream = new FileInputStream(FILE_PATH + fileName)) { + return new MockMultipartFile(FILE_META_NAME, fileName, CONTENT_TYPE, stream); + } + } + + public static List createMockMultipartFiles() throws IOException { + List files = new ArrayList<>(); + for (int i = 1; i <= 2; i++) { + files.add(createMockMultipartFile(String.format("hello%s.jpg", i))); + } + return files; + } + + public static MultipartFile createEmptyFile() { + return new MockMultipartFile( + "file", + "hello.png", + "image/png", + new byte[] {} + ); + } +} diff --git a/src/test/java/com/inq/wishhair/wesharewishhair/photo/application/PhotoServiceTest.java b/src/test/java/com/inq/wishhair/wesharewishhair/photo/application/PhotoServiceTest.java new file mode 100644 index 0000000..c4013d0 --- /dev/null +++ b/src/test/java/com/inq/wishhair/wesharewishhair/photo/application/PhotoServiceTest.java @@ -0,0 +1,84 @@ +package com.inq.wishhair.wesharewishhair.photo.application; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.BDDMockito.*; + +import java.io.IOException; +import java.util.List; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; +import org.springframework.test.util.ReflectionTestUtils; +import org.springframework.web.multipart.MultipartFile; + +import com.inq.wishhair.wesharewishhair.common.utils.FileMockingUtils; +import com.inq.wishhair.wesharewishhair.photo.domain.Photo; +import com.inq.wishhair.wesharewishhair.photo.domain.PhotoRepository; +import com.inq.wishhair.wesharewishhair.photo.domain.PhotoStore; +import com.inq.wishhair.wesharewishhair.review.domain.entity.Review; +import com.inq.wishhair.wesharewishhair.review.fixture.ReviewFixture; + +@DisplayName("[PhotoService 테스트] - Application") +class PhotoServiceTest { + + private final PhotoService photoService; + private final PhotoStore photoStore; + + public PhotoServiceTest() { + PhotoRepository photoRepository = Mockito.mock(PhotoRepository.class); + this.photoStore = Mockito.mock(PhotoStore.class); + this.photoService = new PhotoService(photoStore, photoRepository); + } + + @Test + void uploadPhotos() throws IOException { + //given + List files = FileMockingUtils.createMockMultipartFiles(); + + List urls = List.of("test_url1", "test_url2"); + given(photoStore.uploadFiles(anyList())) + .willReturn(urls); + + //when + List actual = photoService.uploadPhotos(files); + + //then + assertThat(actual).isEqualTo(urls); + } + + @Test + void deletePhotosByReviewId() { + //given + Review review = ReviewFixture.getEmptyReview(1L); + Photo photo = Photo.createReviewPhoto("url1", review); + + ReflectionTestUtils.setField(review, "photos", List.of(photo)); + + //when + boolean actual = photoService.deletePhotosByReviewId(review); + + //then + assertThat(actual).isTrue(); + verify(photoStore, times(1)).deleteFiles(anyList()); + } + + @Test + void deletePhotosByWriter() { + //given + Review review1 = ReviewFixture.getEmptyReview(1L); + Review review2 = ReviewFixture.getEmptyReview(2L); + Photo photo1 = Photo.createReviewPhoto("url1", review1); + Photo photo2 = Photo.createReviewPhoto("url1", review2); + + ReflectionTestUtils.setField(review1, "photos", List.of(photo1)); + ReflectionTestUtils.setField(review2, "photos", List.of(photo2)); + + //when + boolean actual = photoService.deletePhotosByWriter(List.of(review1, review2)); + + //then + assertThat(actual).isTrue(); + verify(photoStore, times(2)).deleteFiles(anyList()); + } +} \ No newline at end of file diff --git a/src/test/java/com/inq/wishhair/wesharewishhair/photo/domain/PhotoRepositoryTest.java b/src/test/java/com/inq/wishhair/wesharewishhair/photo/domain/PhotoRepositoryTest.java new file mode 100644 index 0000000..8ec4253 --- /dev/null +++ b/src/test/java/com/inq/wishhair/wesharewishhair/photo/domain/PhotoRepositoryTest.java @@ -0,0 +1,63 @@ +package com.inq.wishhair.wesharewishhair.photo.domain; + +import static org.assertj.core.api.Assertions.*; + +import java.util.List; +import java.util.Optional; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.util.ReflectionTestUtils; + +import com.inq.wishhair.wesharewishhair.common.support.RepositoryTestSupport; +import com.inq.wishhair.wesharewishhair.review.domain.entity.Review; + +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; + +@DisplayName("[PhotoRepository 테스트] - Domain") +class PhotoRepositoryTest extends RepositoryTestSupport { + + @PersistenceContext + private EntityManager entityManager; + @Autowired + private PhotoRepository photoRepository; + + @Test + @DisplayName("[리뷰 아이디를 가진 Photo 를 삭제한다]") + void deleteAllByReview() { + //given + Review review = new Review(); + ReflectionTestUtils.setField(review, "id", 1L); + Photo photo = photoRepository.save(Photo.createReviewPhoto("url", review)); + + //when + photoRepository.deleteAllByReview(1L); + entityManager.clear(); + + //then + Optional actual = photoRepository.findById(photo.getId()); + assertThat(actual).isNotPresent(); + } + + @Test + @DisplayName("[리뷰 아이디 리스트에 포함된 Photo 를 삭제한다]") + void deleteAllByReviews() { + //given + Review review1 = new Review(); + ReflectionTestUtils.setField(review1, "id", 1L); + Review review2 = new Review(); + ReflectionTestUtils.setField(review2, "id", 2L); + photoRepository.save(Photo.createReviewPhoto("url1", review1)); + photoRepository.save(Photo.createReviewPhoto("url2", review2)); + + //when + photoRepository.deleteAllByReviews(List.of(1L, 2L)); + entityManager.clear(); + + //then + List actual = photoRepository.findAll(); + assertThat(actual).isEmpty(); + } +} \ No newline at end of file diff --git a/src/test/java/com/inq/wishhair/wesharewishhair/photo/domain/PhotoStoreTest.java b/src/test/java/com/inq/wishhair/wesharewishhair/photo/domain/PhotoStoreTest.java new file mode 100644 index 0000000..82201bb --- /dev/null +++ b/src/test/java/com/inq/wishhair/wesharewishhair/photo/domain/PhotoStoreTest.java @@ -0,0 +1,93 @@ +package com.inq.wishhair.wesharewishhair.photo.domain; + +import static org.assertj.core.api.Assertions.*; +import static org.assertj.core.api.ThrowableAssert.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.BDDMockito.*; + +import java.io.IOException; +import java.net.URI; +import java.net.URL; +import java.util.List; +import java.util.UUID; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; +import org.springframework.web.multipart.MultipartFile; + +import com.amazonaws.services.s3.AmazonS3Client; +import com.amazonaws.services.s3.model.PutObjectRequest; +import com.inq.wishhair.wesharewishhair.common.utils.FileMockingUtils; +import com.inq.wishhair.wesharewishhair.global.exception.ErrorCode; +import com.inq.wishhair.wesharewishhair.global.exception.WishHairException; +import com.inq.wishhair.wesharewishhair.photo.infrastructure.S3PhotoStore; + +@DisplayName("[PhotoStore 테스트] - Domain") +class PhotoStoreTest { + + private static final String BUCKET_NAME = "bucket"; + + private final PhotoStore photoStore; + private final AmazonS3Client amazonS3Client; + + public PhotoStoreTest() { + this.amazonS3Client = Mockito.mock(AmazonS3Client.class); + this.photoStore = new S3PhotoStore(amazonS3Client, "bucket"); + } + + @Nested + @DisplayName("[이미지를 업로드한다]") + class uploadFiles { + + @Test + @DisplayName("[성공적으로 업로드한다]") + void success() throws IOException { + //given + MultipartFile file = FileMockingUtils.createMockMultipartFile("hello1.jpg"); + + URL url = URI.create("http://localhost:8080/test/url").toURL(); + given(amazonS3Client.getUrl(eq(BUCKET_NAME), anyString())) + .willReturn(url); + + //when + List actual = photoStore.uploadFiles(List.of(file)); + + //then + assertThat(actual).hasSize(1); + assertThat(actual.get(0)).isEqualTo(url.toString()); + } + + @Test + @DisplayName("[이미지 업로드에 실패한다]") + void fail() throws IOException { + //given + MultipartFile file = FileMockingUtils.createMockMultipartFile("hello1.jpg"); + + given(amazonS3Client.putObject(any(PutObjectRequest.class))) + .willThrow(new WishHairException(ErrorCode.FILE_TRANSFER_EX)); + + //when + ThrowingCallable when = () -> photoStore.uploadFiles(List.of(file)); + + //then + assertThatThrownBy(when) + .isInstanceOf(WishHairException.class) + .hasMessageContaining(ErrorCode.FILE_TRANSFER_EX.getMessage()); + } + } + + @Test + @DisplayName("[이미지를 삭제한다]") + void fail() { + //given + String url = "http://localhost:8080/" + UUID.randomUUID(); + + //when + boolean actual = photoStore.deleteFiles(List.of(url)); + + //then + assertThat(actual).isTrue(); + } +} \ No newline at end of file diff --git a/src/test/java/com/inq/wishhair/wesharewishhair/review/fixture/ReviewFixture.java b/src/test/java/com/inq/wishhair/wesharewishhair/review/fixture/ReviewFixture.java new file mode 100644 index 0000000..434ee5f --- /dev/null +++ b/src/test/java/com/inq/wishhair/wesharewishhair/review/fixture/ReviewFixture.java @@ -0,0 +1,18 @@ +package com.inq.wishhair.wesharewishhair.review.fixture; + +import org.springframework.test.util.ReflectionTestUtils; + +import com.inq.wishhair.wesharewishhair.review.domain.entity.Review; + +import lombok.AccessLevel; +import lombok.NoArgsConstructor; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public final class ReviewFixture { + + public static Review getEmptyReview(Long id) { + Review review = new Review(); + ReflectionTestUtils.setField(review, "id", id); + return review; + } +} diff --git a/src/test/resources/images/hello1.jpg b/src/test/resources/images/hello1.jpg new file mode 100644 index 0000000..4a5dc1f Binary files /dev/null and b/src/test/resources/images/hello1.jpg differ diff --git a/src/test/resources/images/hello2.jpg b/src/test/resources/images/hello2.jpg new file mode 100644 index 0000000..8a9c1ab Binary files /dev/null and b/src/test/resources/images/hello2.jpg differ diff --git a/src/test/resources/images/hello3.png b/src/test/resources/images/hello3.png new file mode 100644 index 0000000..3bbcf7f Binary files /dev/null and b/src/test/resources/images/hello3.png differ