diff --git a/buildSrc/src/main/kotlin/Dependencies.kt b/buildSrc/src/main/kotlin/Dependencies.kt index 77f25fc9..7fae3e19 100644 --- a/buildSrc/src/main/kotlin/Dependencies.kt +++ b/buildSrc/src/main/kotlin/Dependencies.kt @@ -26,4 +26,5 @@ object Dependencies { // Test dependencies const val springBootStarterTest = "org.springframework.boot:spring-boot-starter-test" const val springSecurityTest = "org.springframework.security:spring-security-test" + const val mockWebServer = "com.squareup.okhttp3:mockwebserver:${Versions.mockWebServer}" } diff --git a/buildSrc/src/main/kotlin/Versions.kt b/buildSrc/src/main/kotlin/Versions.kt index 626c1b93..37b3a440 100644 --- a/buildSrc/src/main/kotlin/Versions.kt +++ b/buildSrc/src/main/kotlin/Versions.kt @@ -14,5 +14,6 @@ object Versions { const val sonarQube = "6.0.1.5171" const val checkStyle = "10.20.2" const val jacoco = "0.8.12" - const val apacheHttpClient = "5.4.1" + const val apacheHttpClient = "5.2.3" + const val mockWebServer = "4.12.0" } diff --git a/stempo-application/build.gradle.kts b/stempo-application/build.gradle.kts index 9db1c9f0..7645ddd1 100644 --- a/stempo-application/build.gradle.kts +++ b/stempo-application/build.gradle.kts @@ -17,4 +17,7 @@ dependencies { implementation(Dependencies.swagger) implementation(Dependencies.googleAuthenticator) implementation(Dependencies.apacheHttpClient) + + // Test + testImplementation(Dependencies.mockWebServer) } diff --git a/stempo-application/src/test/java/com/stempo/config/RestClientConfigTest.java b/stempo-application/src/test/java/com/stempo/config/RestClientConfigTest.java new file mode 100644 index 00000000..b5b24135 --- /dev/null +++ b/stempo-application/src/test/java/com/stempo/config/RestClientConfigTest.java @@ -0,0 +1,90 @@ +package com.stempo.config; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.io.IOException; +import okhttp3.mockwebserver.MockResponse; +import okhttp3.mockwebserver.MockWebServer; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.ApplicationContext; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.test.context.DynamicPropertyRegistry; +import org.springframework.test.context.DynamicPropertySource; +import org.springframework.test.context.junit.jupiter.SpringExtension; +import org.springframework.web.client.RestClient; + +@ExtendWith(SpringExtension.class) +@SpringBootTest(classes = RestClientConfig.class) +class RestClientConfigTest { + + private static MockWebServer mockWebServer; + + @Autowired + private ApplicationContext applicationContext; + + @Autowired + private RestClient rhythmRestClient; + + @BeforeAll + static void setUp() throws IOException { + mockWebServer = new MockWebServer(); + mockWebServer.start(); + } + + @AfterAll + static void tearDown() throws IOException { + mockWebServer.shutdown(); + } + + // @DynamicPropertySource를 사용하여 rhythm-generator.url을 MockWebServer의 URL로 설정 + @DynamicPropertySource + static void registerProperties(DynamicPropertyRegistry registry) { + String baseUrl = mockWebServer.url("/api").toString(); + registry.add("rhythm-generator.url", () -> baseUrl); + } + + @Test + void restClientConfig_빈이_정상적으로_생성된다() { + // given, when + RestClient restClient = applicationContext.getBean(RestClient.class); + + // then + assertThat(restClient).isNotNull(); + } + + @Test + void rhythmRestClient_빈이_올바르게_구성되었는지_확인한다() throws Exception { + // Given + String expectedResponseBody = "{\"message\":\"success\"}"; + mockWebServer.enqueue(new MockResponse() + .setResponseCode(200) + .setBody(expectedResponseBody) + .addHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)); + + // When + ResponseEntity response = rhythmRestClient.get() + .uri("/test-endpoint") + .retrieve() + .toEntity(String.class); + + // Then + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); + assertThat(response.getBody()).isEqualTo(expectedResponseBody); + + // 요청 검증 + okhttp3.mockwebserver.RecordedRequest recordedRequest = mockWebServer.takeRequest(); + assertThat(recordedRequest.getMethod()).isEqualTo("GET"); + assertThat(recordedRequest.getPath()).isEqualTo("/api/test-endpoint"); + + // 기본 헤더 검증 + assertThat(recordedRequest.getHeader(HttpHeaders.CONTENT_TYPE)).isEqualTo(MediaType.APPLICATION_JSON_VALUE); + } +} diff --git a/stempo-application/src/test/java/com/stempo/service/FileServiceTest.java b/stempo-application/src/test/java/com/stempo/service/FileServiceTest.java index d7743219..5179cd5b 100644 --- a/stempo-application/src/test/java/com/stempo/service/FileServiceTest.java +++ b/stempo-application/src/test/java/com/stempo/service/FileServiceTest.java @@ -3,7 +3,9 @@ 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.ArgumentMatchers.anyString; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -111,16 +113,16 @@ void setUp() { // given Pageable pageable = PageRequest.of(0, 10); UploadedFile uploadedFile = UploadedFile.builder() - .id(1L) - .originalFileName("original.txt") - .saveFileName("file.txt") - .url("http://example.com/files/file.txt") - .build(); + .id(1L) + .originalFileName("original.txt") + .saveFileName("file.txt") + .url("http://example.com/files/file.txt") + .build(); UploadedFileResponseDto responseDto = UploadedFileResponseDto.builder() - .originalFileName(uploadedFile.getOriginalFileName()) - .url(uploadedFile.getUrl()) - .createdAt(uploadedFile.getCreatedAt()) - .build(); + .originalFileName(uploadedFile.getOriginalFileName()) + .url(uploadedFile.getUrl()) + .createdAt(uploadedFile.getCreatedAt()) + .build(); Page uploadedFilesPage = new PageImpl<>(Collections.singletonList(uploadedFile), pageable, 1); @@ -188,6 +190,52 @@ void setUp() { verify(uploadedFileService).saveUploadedFile(any(UploadedFile.class)); } + @Test + void saveRhythmFile_정상적으로_저장하고_URL을_반환한다() throws IOException { + // given + byte[] fileData = "Sample rhythm data".getBytes(); + String fileName = "rhythm_120_4_bpm.wav"; + String category = "rhythm"; + String savedFilePath = "/saved/path/rhythm_120_4_bpm.wav"; + String savedFileName = "rhythm_120_4_bpm.wav"; + String url = fileUrl + "/" + category + "/" + savedFileName; + String encryptedFilePath = "encryptedPath"; + + when(fileHandler.saveFile(fileData, category, fileName)).thenReturn(savedFilePath); + when(encryptionUtils.encrypt(savedFilePath)).thenReturn(encryptedFilePath); + + // when + String resultUrl = fileService.saveRhythmFile(fileData, fileName); + + // then + assertThat(resultUrl).isEqualTo(url); + verify(fileHandler).saveFile(fileData, category, fileName); + verify(encryptionUtils).encrypt(savedFilePath); + verify(uploadedFileService).saveUploadedFile(any(UploadedFile.class)); + } + + @Test + void saveRhythmFile_저장_중_예외가_발생하면_BaseException을_던진다() throws IOException { + // given + byte[] fileData = "Sample rhythm data".getBytes(); + String fileName = "rhythm_120_4_bpm.wav"; + String category = "rhythm"; + + when(fileHandler.saveFile(fileData, category, fileName)) + .thenThrow(new IOException("파일 저장 실패")); + + // when, then + assertThatThrownBy(() -> fileService.saveRhythmFile(fileData, fileName)) + .isInstanceOf(BaseException.class) + .hasMessageContaining("리듬 파일 저장에 실패했습니다: " + fileName) + .extracting("errorCode") + .isEqualTo(ErrorCode.RHYTHM_GENERATION_ERROR); + + verify(fileHandler).saveFile(fileData, category, fileName); + verify(encryptionUtils, never()).encrypt(anyString()); + verify(uploadedFileService, never()).saveUploadedFile(any(UploadedFile.class)); + } + @Test void deleteFile_파일을_삭제하고_true를_반환한다() { // given @@ -199,9 +247,9 @@ void setUp() { requestDto.setUrl(url); UploadedFile uploadedFile = UploadedFile.builder() - .savedPath(encryptedFilePath) - .url(url) - .build(); + .savedPath(encryptedFilePath) + .url(url) + .build(); when(uploadedFileService.getUploadedFileByUrl(url)).thenReturn(uploadedFile); when(encryptionUtils.decrypt(encryptedFilePath)).thenReturn(decryptedFilePath); @@ -229,9 +277,9 @@ void setUp() { requestDto.setUrl(url); UploadedFile uploadedFile = UploadedFile.builder() - .savedPath(encryptedFilePath) - .url(url) - .build(); + .savedPath(encryptedFilePath) + .url(url) + .build(); when(uploadedFileService.getUploadedFileByUrl(url)).thenReturn(uploadedFile); when(encryptionUtils.decrypt(encryptedFilePath)).thenReturn(decryptedFilePath); @@ -239,8 +287,8 @@ void setUp() { // when, then assertThatThrownBy(() -> fileService.deleteFile(requestDto)) - .isInstanceOf(BaseException.class) - .hasMessageContaining(ErrorCode.FILE_DELETE_FAILED.getDefaultMessage()); + .isInstanceOf(BaseException.class) + .hasMessageContaining(ErrorCode.FILE_DELETE_FAILED.getDefaultMessage()); verify(uploadedFileService).getUploadedFileByUrl(url); verify(encryptionUtils).decrypt(encryptedFilePath); diff --git a/stempo-infrastructure/src/test/java/com/stempo/util/FileHandlerTest.java b/stempo-infrastructure/src/test/java/com/stempo/util/FileHandlerTest.java index 5bd3a55f..0a8a6f65 100644 --- a/stempo-infrastructure/src/test/java/com/stempo/util/FileHandlerTest.java +++ b/stempo-infrastructure/src/test/java/com/stempo/util/FileHandlerTest.java @@ -27,8 +27,8 @@ class FileHandlerTest { @BeforeEach void setUp() { fileHandler = new FileHandler( - new String[]{"exe", "bat"}, - tempDir.toString() + new String[]{"exe", "bat"}, + tempDir.toString() ); fileHandler.init(); } @@ -37,10 +37,10 @@ void setUp() { void 파일을_성공적으로_저장한다() throws IOException { // given MultipartFile multipartFile = new MockMultipartFile( - "file", - "test.txt", - "text/plain", - "Sample content".getBytes() + "file", + "test.txt", + "text/plain", + "Sample content".getBytes() ); String category = "docs"; @@ -49,10 +49,10 @@ void setUp() { String savedPath = fileHandler.saveFile(multipartFile, category); // then - assertThat(savedPath).isNotNull(); - assertThat(savedPath).contains(tempDir.toString() + File.separator + category); + assertThat(savedPath).isNotNull() + .contains(tempDir.toString() + File.separator + category); File savedFile = new File(savedPath); - assertThat(savedFile.exists()).isTrue(); + assertThat(savedFile).exists(); } @Test @@ -67,7 +67,7 @@ void setUp() { // then assertThat(savedPath).isNotNull(); File savedFile = new File(savedPath); - assertThat(savedFile.exists()).isTrue(); + assertThat(savedFile).exists(); assertThat(Files.readString(savedFile.toPath())).isEqualTo("Sample content"); } @@ -75,46 +75,67 @@ void setUp() { void 파일명에_잘못된_문자열이_포함되어_있을_경우_예외가_발생한다() { // given MultipartFile multipartFile = new MockMultipartFile( - "file", - "test..txt", - "text/plain", - "Sample content".getBytes() + "file", + "test..txt", + "text/plain", + "Sample content".getBytes() ); String category = "docs"; // then assertThatThrownBy(() -> fileHandler.saveFile(multipartFile, category)) - .isInstanceOf(BaseException.class) - .hasMessageContaining(ErrorCode.INVALID_FILE_NAME.getDefaultMessage()); + .isInstanceOf(BaseException.class) + .hasMessageContaining(ErrorCode.INVALID_FILE_NAME.getDefaultMessage()); } @Test void 잘못된_확장자를_가진_파일을_저장하려_할_때_예외가_발생한다() { // given MultipartFile multipartFile = new MockMultipartFile( - "file", - "test.exe", - "application/octet-stream", - "Sample content".getBytes() + "file", + "test.exe", + "application/octet-stream", + "Sample content".getBytes() ); String category = "docs"; // then assertThatThrownBy(() -> fileHandler.saveFile(multipartFile, category)) - .isInstanceOf(BaseException.class) - .hasMessageContaining(ErrorCode.INVALID_FILE_ATTRIBUTE.getDefaultMessage()); + .isInstanceOf(BaseException.class) + .hasMessageContaining(ErrorCode.INVALID_FILE_ATTRIBUTE.getDefaultMessage()); + } + + @Test + void 파일_데이터로_성공적으로_파일을_저장하고_URL을_반환한다() throws IOException { + // given + byte[] fileData = "Sample byte content".getBytes(); + String category = "images"; + String fileName = "picture.png"; + String expectedExtension = "png"; + + // when + String savedPath = fileHandler.saveFile(fileData, category, fileName); + + // then + assertThat(savedPath).isNotNull() + .contains(tempDir.toString() + File.separator + category) + .endsWith("." + expectedExtension); + + File savedFile = new File(savedPath); + assertThat(savedFile).exists(); + assertThat(Files.readAllBytes(savedFile.toPath())).isEqualTo(fileData); } @Test void 파일을_성공적으로_삭제한다() throws IOException { // given MultipartFile multipartFile = new MockMultipartFile( - "file", - "test.txt", - "text/plain", - "Sample content".getBytes() + "file", + "test.txt", + "text/plain", + "Sample content".getBytes() ); String category = "docs"; @@ -126,7 +147,7 @@ void setUp() { // then assertThat(isDeleted).isTrue(); File savedFile = new File(savedPath); - assertThat(savedFile.exists()).isFalse(); + assertThat(savedFile).doesNotExist(); } @Test