-
Notifications
You must be signed in to change notification settings - Fork 1
test: 업로드 관련 통합 테스트 작성 #408
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
+244
−0
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
244 changes: 244 additions & 0 deletions
244
src/test/java/gg/agit/konect/integration/domain/upload/UploadApiTest.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,244 @@ | ||
| 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.request.MockMvcRequestBuilders.multipart; | ||
| 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.exception.SdkClientException; | ||
| 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<PutObjectRequest> 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("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 { | ||
| // 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("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 { | ||
| // 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")); | ||
| } | ||
dh2906 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| @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 { | ||
| 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) | ||
| ); | ||
| } | ||
|
|
||
| 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(); | ||
| } | ||
| } | ||
| } | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.