diff --git a/src/main/java/org/runnect/server/external/aws/S3Service.java b/src/main/java/org/runnect/server/external/aws/S3Service.java index bf4e48b..bb6d65c 100644 --- a/src/main/java/org/runnect/server/external/aws/S3Service.java +++ b/src/main/java/org/runnect/server/external/aws/S3Service.java @@ -70,9 +70,15 @@ private String createFileName(String fileName) { // 파일 유효성 검사 private String getFileExtension(String fileName) { - if (fileName.length() == 0) { + if (fileName == null || fileName.isEmpty()) { throw new NotFoundException(ErrorStatus.NOT_FOUND_IMAGE_EXCEPTION, ErrorStatus.NOT_FOUND_IMAGE_EXCEPTION.getMessage()); } + int dotIndex = fileName.lastIndexOf("."); + // 파일명에 "."가 아예 없으면(lastIndexOf가 -1) substring(-1)에서 그대로 + // StringIndexOutOfBoundsException(500)이 나던 부분 — 확장자 없는 파일로 명확히 처리한다. + if (dotIndex == -1) { + throw new BadRequestException(ErrorStatus.NOT_FOUND_IMAGE_EXCEPTION, ErrorStatus.NOT_FOUND_IMAGE_EXCEPTION.getMessage()); + } ArrayList fileValidate = new ArrayList<>(); fileValidate.add(".jpg"); fileValidate.add(".jpeg"); @@ -80,11 +86,11 @@ private String getFileExtension(String fileName) { fileValidate.add(".JPG"); fileValidate.add(".JPEG"); fileValidate.add(".PNG"); - String idxFileName = fileName.substring(fileName.lastIndexOf(".")); + String idxFileName = fileName.substring(dotIndex); if (!fileValidate.contains(idxFileName)) { throw new BadRequestException(ErrorStatus.NOT_FOUND_IMAGE_EXCEPTION, ErrorStatus.NOT_FOUND_IMAGE_EXCEPTION.getMessage()); } - return fileName.substring(fileName.lastIndexOf(".")); + return idxFileName; } // 이미지 삭제 diff --git a/src/test/java/org/runnect/server/banner/service/BannerServiceTest.java b/src/test/java/org/runnect/server/banner/service/BannerServiceTest.java new file mode 100644 index 0000000..d9fd731 --- /dev/null +++ b/src/test/java/org/runnect/server/banner/service/BannerServiceTest.java @@ -0,0 +1,58 @@ +package org.runnect.server.banner.service; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.when; + +import java.util.Arrays; +import java.util.Collections; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.runnect.server.banner.dto.response.GetBannerResponseDto; +import org.runnect.server.banner.entity.Banner; +import org.runnect.server.banner.repository.BannerRepository; +import org.springframework.test.util.ReflectionTestUtils; + +@ExtendWith(MockitoExtension.class) +class BannerServiceTest { + + @Mock + private BannerRepository bannerRepository; + + private BannerService bannerService; + + private Banner buildBanner(Long id, String imageUrl, String linkUrl, int sortOrder) { + Banner banner = Banner.builder().imageUrl(imageUrl).linkUrl(linkUrl).sortOrder(sortOrder).build(); + ReflectionTestUtils.setField(banner, "id", id); + return banner; + } + + @Test + void 활성_배너를_정렬된_순서_그대로_0부터_인덱싱해서_반환한다() { + bannerService = new BannerService(bannerRepository); + Banner first = buildBanner(1L, "image1.png", "https://a.com", 0); + Banner second = buildBanner(2L, "image2.png", "https://b.com", 1); + when(bannerRepository.findByIsActiveTrueOrderBySortOrderAscIdAsc()) + .thenReturn(Arrays.asList(first, second)); + + GetBannerResponseDto response = bannerService.getBanners(); + + assertThat(response.getBanners()).hasSize(2); + assertThat(response.getBanners().get(0).getIndex()).isEqualTo(0); + assertThat(response.getBanners().get(0).getImageUrl()).isEqualTo("image1.png"); + assertThat(response.getBanners().get(1).getIndex()).isEqualTo(1); + assertThat(response.getBanners().get(1).getLinkUrl()).isEqualTo("https://b.com"); + } + + @Test + void 활성_배너가_없으면_빈_목록을_반환한다() { + bannerService = new BannerService(bannerRepository); + when(bannerRepository.findByIsActiveTrueOrderBySortOrderAscIdAsc()) + .thenReturn(Collections.emptyList()); + + GetBannerResponseDto response = bannerService.getBanners(); + + assertThat(response.getBanners()).isEmpty(); + } +} diff --git a/src/test/java/org/runnect/server/external/aws/S3ServiceTest.java b/src/test/java/org/runnect/server/external/aws/S3ServiceTest.java new file mode 100644 index 0000000..dd8b9d4 --- /dev/null +++ b/src/test/java/org/runnect/server/external/aws/S3ServiceTest.java @@ -0,0 +1,120 @@ +package org.runnect.server.external.aws; + +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.mock; +import static org.mockito.Mockito.when; + +import com.amazonaws.services.s3.AmazonS3; +import com.amazonaws.services.s3.model.PutObjectRequest; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +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.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.runnect.server.common.exception.BadRequestException; +import org.runnect.server.common.exception.NotFoundException; +import org.springframework.mock.web.MockMultipartFile; +import org.springframework.test.util.ReflectionTestUtils; +import org.springframework.web.multipart.MultipartFile; + +@ExtendWith(MockitoExtension.class) +class S3ServiceTest { + + @Mock + private AmazonS3 amazonS3; + + private S3Service s3Service; + + @BeforeEach + void setUp() { + s3Service = new S3Service(amazonS3); + ReflectionTestUtils.setField(s3Service, "bucket", "runnect-test-bucket"); + } + + private void stubUploadedUrl() throws Exception { + when(amazonS3.getUrl(any(), any())).thenReturn( + new URL("https://runnect-test-bucket.s3.ap-northeast-2.amazonaws.com/course/image/test.jpg")); + } + + @Nested + @DisplayName("uploadImage") + class UploadImage { + + @Test + @DisplayName("정상적인 이미지 파일이면 업로드하고 URL을 반환한다") + void 정상_업로드() throws Exception { + stubUploadedUrl(); + MultipartFile file = new MockMultipartFile("image", "photo.jpg", "image/jpeg", "content".getBytes()); + + String url = s3Service.uploadImage(file, "course"); + + assertThat(url).isEqualTo( + "https://runnect-test-bucket.s3.ap-northeast-2.amazonaws.com/course/image/test.jpg"); + } + + @Test + @DisplayName("대문자 확장자(.PNG)도 정상 업로드된다") + void 대문자_확장자() throws Exception { + stubUploadedUrl(); + MultipartFile file = new MockMultipartFile("image", "photo.PNG", "image/png", "content".getBytes()); + + assertThat(s3Service.uploadImage(file, "course")).isNotNull(); + } + + @Test + @DisplayName("지원하지 않는 확장자면 BadRequestException") + void 지원하지_않는_확장자() { + MultipartFile file = new MockMultipartFile("image", "photo.gif", "image/gif", "content".getBytes()); + + assertThatThrownBy(() -> s3Service.uploadImage(file, "course")) + .isInstanceOf(BadRequestException.class); + } + + @Test + @DisplayName("[버그 수정 검증] 파일명이 null이면 500(NPE) 대신 NotFoundException") + void 파일명이_null() { + MultipartFile file = new MockMultipartFile("image", null, "image/jpeg", "content".getBytes()); + + assertThatThrownBy(() -> s3Service.uploadImage(file, "course")) + .isInstanceOf(NotFoundException.class); + } + + @Test + @DisplayName("파일명이 빈 문자열이면 NotFoundException") + void 파일명이_빈문자열() { + MultipartFile file = new MockMultipartFile("image", "", "image/jpeg", "content".getBytes()); + + assertThatThrownBy(() -> s3Service.uploadImage(file, "course")) + .isInstanceOf(NotFoundException.class); + } + + @Test + @DisplayName("[버그 수정 검증] 확장자(.)가 없는 파일명이면 500(StringIndexOutOfBoundsException) 대신 BadRequestException") + void 확장자가_없는_파일명() { + MultipartFile file = new MockMultipartFile("image", "photo_without_extension", "image/jpeg", + "content".getBytes()); + + assertThatThrownBy(() -> s3Service.uploadImage(file, "course")) + .isInstanceOf(BadRequestException.class); + } + + @Test + @DisplayName("파일 스트림을 읽는 중 오류가 나면 NotFoundException") + void 스트림_읽기_실패() throws IOException { + MultipartFile file = mock(MultipartFile.class); + when(file.getOriginalFilename()).thenReturn("photo.jpg"); + when(file.getInputStream()).thenThrow(new IOException("disk error")); + + assertThatThrownBy(() -> s3Service.uploadImage(file, "course")) + .isInstanceOf(NotFoundException.class); + } + } +}