From dc54233e3c592e24aae4725f43666dd949b15c37 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EB=8F=99=ED=9B=88?= <2dh2@naver.com> Date: Wed, 18 Mar 2026 18:42:49 +0900 Subject: [PATCH 1/4] =?UTF-8?q?test:=20=EC=97=85=EB=A1=9C=EB=93=9C=20API?= =?UTF-8?q?=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=BD=94=EB=93=9C=20=EC=9E=91?= =?UTF-8?q?=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/upload/UploadApiTest.java | 160 ++++++++++++++++++ 1 file changed, 160 insertions(+) create mode 100644 src/test/java/gg/agit/konect/integration/domain/upload/UploadApiTest.java diff --git a/src/test/java/gg/agit/konect/integration/domain/upload/UploadApiTest.java b/src/test/java/gg/agit/konect/integration/domain/upload/UploadApiTest.java new file mode 100644 index 00000000..4772510d --- /dev/null +++ b/src/test/java/gg/agit/konect/integration/domain/upload/UploadApiTest.java @@ -0,0 +1,160 @@ +package gg.agit.konect.integration.domain.upload; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.willThrow; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import java.awt.image.BufferedImage; +import java.io.ByteArrayOutputStream; + +import javax.imageio.ImageIO; + +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.mockito.ArgumentCaptor; +import org.springframework.mock.web.MockMultipartFile; +import org.springframework.test.context.bean.override.mockito.MockitoBean; +import org.springframework.test.web.servlet.MvcResult; +import org.springframework.test.web.servlet.ResultActions; + +import com.jayway.jsonpath.JsonPath; + +import gg.agit.konect.domain.upload.enums.UploadTarget; +import gg.agit.konect.support.IntegrationTestSupport; +import software.amazon.awssdk.core.sync.RequestBody; +import software.amazon.awssdk.services.s3.S3Client; +import software.amazon.awssdk.services.s3.model.PutObjectRequest; +import software.amazon.awssdk.services.s3.model.S3Exception; + +class UploadApiTest extends IntegrationTestSupport { + + private static final int LOGIN_USER_ID = 2024001001; + private static final int MAX_UPLOAD_BYTES = 20 * 1024 * 1024; + + @MockitoBean + private S3Client s3Client; + + @BeforeEach + void setUp() throws Exception { + mockLoginUser(LOGIN_USER_ID); + } + + @Nested + @DisplayName("POST /upload/image - 이미지 업로드") + class UploadImage { + + @Test + @DisplayName("지원하는 이미지를 업로드하면 webp key와 CDN URL을 반환한다") + void uploadImageSuccess() throws Exception { + // given + MockMultipartFile file = imageFile("club.png", "image/png", createPngBytes(8, 8)); + + // when + MvcResult result = uploadImage(file, UploadTarget.CLUB) + .andExpect(status().isOk()) + .andReturn(); + + // then + String responseBody = result.getResponse().getContentAsString(); + String key = JsonPath.read(responseBody, "$.key"); + String fileUrl = JsonPath.read(responseBody, "$.fileUrl"); + + assertThat(key).startsWith("test/club/"); + assertThat(key).endsWith(".webp"); + assertThat(fileUrl).isEqualTo("https://cdn.test.com/" + key); + + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(PutObjectRequest.class); + verify(s3Client).putObject(requestCaptor.capture(), any(RequestBody.class)); + assertThat(requestCaptor.getValue().bucket()).isEqualTo("test-bucket"); + assertThat(requestCaptor.getValue().key()).isEqualTo(key); + assertThat(requestCaptor.getValue().contentType()).isEqualTo("image/webp"); + } + + @Test + @DisplayName("빈 파일을 업로드하면 400을 반환한다") + void uploadEmptyFileFails() throws Exception { + // given + MockMultipartFile file = imageFile("empty.png", "image/png", new byte[0]); + + // when & then + uploadImage(file, UploadTarget.CLUB) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.code").value("INVALID_REQUEST_BODY")); + + verify(s3Client, never()).putObject(any(PutObjectRequest.class), any(RequestBody.class)); + } + + @Test + @DisplayName("허용하지 않는 content type이면 400을 반환한다") + void uploadImageWithInvalidContentTypeFails() throws Exception { + // given + MockMultipartFile file = imageFile("note.txt", "text/plain", "not-image".getBytes()); + + // when & then + uploadImage(file, UploadTarget.CLUB) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.code").value("INVALID_FILE_CONTENT_TYPE")); + + verify(s3Client, never()).putObject(any(PutObjectRequest.class), any(RequestBody.class)); + } + + @Test + @DisplayName("최대 업로드 크기를 넘기면 400을 반환한다") + void uploadImageWithTooLargeFileFails() throws Exception { + // given + MockMultipartFile file = imageFile( + "large.png", + "image/png", + new byte[MAX_UPLOAD_BYTES + 1] + ); + + // when & then + uploadImage(file, UploadTarget.CLUB) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.code").value("INVALID_FILE_SIZE")); + + verify(s3Client, never()).putObject(any(PutObjectRequest.class), any(RequestBody.class)); + } + + @Test + @DisplayName("S3 업로드에 실패하면 500을 반환한다") + void uploadImageWhenS3FailsReturnsInternalServerError() throws Exception { + // given + MockMultipartFile file = imageFile("club.png", "image/png", createPngBytes(8, 8)); + willThrow(S3Exception.builder().statusCode(500).message("upload failed").build()) + .given(s3Client) + .putObject(any(PutObjectRequest.class), any(RequestBody.class)); + + // when & then + uploadImage(file, UploadTarget.CLUB) + .andExpect(status().isInternalServerError()) + .andExpect(jsonPath("$.code").value("FAILED_UPLOAD_FILE")); + } + } + + private ResultActions uploadImage(MockMultipartFile file, UploadTarget target) throws Exception { + return mockMvc.perform( + org.springframework.test.web.servlet.request.MockMvcRequestBuilders.multipart("/upload/image") + .file(file) + .param("target", target.name()) + ); + } + + private MockMultipartFile imageFile(String fileName, String contentType, byte[] bytes) { + return new MockMultipartFile("file", fileName, contentType, bytes); + } + + private byte[] createPngBytes(int width, int height) throws Exception { + BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB); + try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) { + ImageIO.write(image, "png", outputStream); + return outputStream.toByteArray(); + } + } +} From 94027085ce470fc7a6ba9a98064277c166208277 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EB=8F=99=ED=9B=88?= <2dh2@naver.com> Date: Wed, 18 Mar 2026 18:44:18 +0900 Subject: [PATCH 2/4] =?UTF-8?q?chore:=20static=20import=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../agit/konect/integration/domain/upload/UploadApiTest.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/test/java/gg/agit/konect/integration/domain/upload/UploadApiTest.java b/src/test/java/gg/agit/konect/integration/domain/upload/UploadApiTest.java index 4772510d..bb725fc8 100644 --- a/src/test/java/gg/agit/konect/integration/domain/upload/UploadApiTest.java +++ b/src/test/java/gg/agit/konect/integration/domain/upload/UploadApiTest.java @@ -5,6 +5,7 @@ import static org.mockito.BDDMockito.willThrow; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.multipart; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @@ -140,7 +141,7 @@ void uploadImageWhenS3FailsReturnsInternalServerError() throws Exception { private ResultActions uploadImage(MockMultipartFile file, UploadTarget target) throws Exception { return mockMvc.perform( - org.springframework.test.web.servlet.request.MockMvcRequestBuilders.multipart("/upload/image") + multipart("/upload/image") .file(file) .param("target", target.name()) ); From fb37ce528fd734e29a3fd43ace8a8c9b3440eff3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EB=8F=99=ED=9B=88?= <2dh2@naver.com> Date: Wed, 18 Mar 2026 19:08:23 +0900 Subject: [PATCH 3/4] =?UTF-8?q?test:=20=EC=97=A3=EC=A7=80=20=EC=BC=80?= =?UTF-8?q?=EC=9D=B4=EC=8A=A4=20=EB=B3=B4=EA=B0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/upload/UploadApiTest.java | 69 ++++++++++++++++++- 1 file changed, 68 insertions(+), 1 deletion(-) diff --git a/src/test/java/gg/agit/konect/integration/domain/upload/UploadApiTest.java b/src/test/java/gg/agit/konect/integration/domain/upload/UploadApiTest.java index bb725fc8..dc147c4f 100644 --- a/src/test/java/gg/agit/konect/integration/domain/upload/UploadApiTest.java +++ b/src/test/java/gg/agit/konect/integration/domain/upload/UploadApiTest.java @@ -105,6 +105,34 @@ void uploadImageWithInvalidContentTypeFails() throws Exception { verify(s3Client, never()).putObject(any(PutObjectRequest.class), any(RequestBody.class)); } + @Test + @DisplayName("content type 이 없으면 400을 반환한다") + void uploadImageWithoutContentTypeFails() throws Exception { + // given + MockMultipartFile file = imageFile("club.png", null, createPngBytes(8, 8)); + + // when & then + uploadImage(file, UploadTarget.CLUB) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.code").value("INVALID_FILE_CONTENT_TYPE")); + + verify(s3Client, never()).putObject(any(PutObjectRequest.class), any(RequestBody.class)); + } + + @Test + @DisplayName("content type 이 비어 있으면 400을 반환한다") + void uploadImageWithBlankContentTypeFails() throws Exception { + // given + MockMultipartFile file = imageFile("club.png", " ", createPngBytes(8, 8)); + + // when & then + uploadImage(file, UploadTarget.CLUB) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.code").value("INVALID_FILE_CONTENT_TYPE")); + + verify(s3Client, never()).putObject(any(PutObjectRequest.class), any(RequestBody.class)); + } + @Test @DisplayName("최대 업로드 크기를 넘기면 400을 반환한다") void uploadImageWithTooLargeFileFails() throws Exception { @@ -123,6 +151,34 @@ void uploadImageWithTooLargeFileFails() throws Exception { verify(s3Client, never()).putObject(any(PutObjectRequest.class), any(RequestBody.class)); } + @Test + @DisplayName("target 파라미터가 없으면 400을 반환한다") + void uploadImageWithoutTargetFails() throws Exception { + // given + MockMultipartFile file = imageFile("club.png", "image/png", createPngBytes(8, 8)); + + // when & then + uploadImageWithoutTarget(file) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.code").value("MISSING_REQUIRED_PARAMETER")); + + verify(s3Client, never()).putObject(any(PutObjectRequest.class), any(RequestBody.class)); + } + + @Test + @DisplayName("지원하지 않는 target 이면 400을 반환한다") + void uploadImageWithInvalidTargetFails() throws Exception { + // given + MockMultipartFile file = imageFile("club.png", "image/png", createPngBytes(8, 8)); + + // when & then + uploadImage(file, "INVALID") + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.code").value("INVALID_TYPE_VALUE")); + + verify(s3Client, never()).putObject(any(PutObjectRequest.class), any(RequestBody.class)); + } + @Test @DisplayName("S3 업로드에 실패하면 500을 반환한다") void uploadImageWhenS3FailsReturnsInternalServerError() throws Exception { @@ -140,10 +196,21 @@ void uploadImageWhenS3FailsReturnsInternalServerError() throws Exception { } private ResultActions uploadImage(MockMultipartFile file, UploadTarget target) throws Exception { + return uploadImage(file, target.name()); + } + + private ResultActions uploadImage(MockMultipartFile file, String target) throws Exception { + return mockMvc.perform( + multipart("/upload/image") + .file(file) + .param("target", target) + ); + } + + private ResultActions uploadImageWithoutTarget(MockMultipartFile file) throws Exception { return mockMvc.perform( multipart("/upload/image") .file(file) - .param("target", target.name()) ); } From 6caff1e2c0574b82b7d718ae052bfc147ae10e0e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EB=8F=99=ED=9B=88?= <2dh2@naver.com> Date: Wed, 18 Mar 2026 19:11:21 +0900 Subject: [PATCH 4/4] =?UTF-8?q?test:=20SdkClientException=20=EA=B2=80?= =?UTF-8?q?=EC=A6=9D=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../integration/domain/upload/UploadApiTest.java | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/test/java/gg/agit/konect/integration/domain/upload/UploadApiTest.java b/src/test/java/gg/agit/konect/integration/domain/upload/UploadApiTest.java index dc147c4f..987b5fce 100644 --- a/src/test/java/gg/agit/konect/integration/domain/upload/UploadApiTest.java +++ b/src/test/java/gg/agit/konect/integration/domain/upload/UploadApiTest.java @@ -28,6 +28,7 @@ import gg.agit.konect.domain.upload.enums.UploadTarget; import gg.agit.konect.support.IntegrationTestSupport; +import software.amazon.awssdk.core.exception.SdkClientException; import software.amazon.awssdk.core.sync.RequestBody; import software.amazon.awssdk.services.s3.S3Client; import software.amazon.awssdk.services.s3.model.PutObjectRequest; @@ -193,6 +194,21 @@ void uploadImageWhenS3FailsReturnsInternalServerError() throws Exception { .andExpect(status().isInternalServerError()) .andExpect(jsonPath("$.code").value("FAILED_UPLOAD_FILE")); } + + @Test + @DisplayName("S3 클라이언트 오류가 발생하면 500을 반환한다") + void uploadImageWhenS3ClientFailsReturnsInternalServerError() throws Exception { + // given + MockMultipartFile file = imageFile("club.png", "image/png", createPngBytes(8, 8)); + willThrow(SdkClientException.create("network failure")) + .given(s3Client) + .putObject(any(PutObjectRequest.class), any(RequestBody.class)); + + // when & then + uploadImage(file, UploadTarget.CLUB) + .andExpect(status().isInternalServerError()) + .andExpect(jsonPath("$.code").value("FAILED_UPLOAD_FILE")); + } } private ResultActions uploadImage(MockMultipartFile file, UploadTarget target) throws Exception {