diff --git a/backend/build.gradle b/backend/build.gradle index 357ba7075..cbbbbef43 100644 --- a/backend/build.gradle +++ b/backend/build.gradle @@ -34,6 +34,12 @@ dependencies { //swagger implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.0.2' + // WebClient Dependency + implementation 'org.springframework.boot:spring-boot-starter-webflux' + + // XML parsing Dependency + implementation 'org.glassfish.jaxb:jaxb-runtime' + compileOnly 'org.projectlombok:lombok' runtimeOnly 'com.h2database:h2' @@ -43,6 +49,9 @@ dependencies { testImplementation 'org.springframework.boot:spring-boot-starter-test' testImplementation 'io.rest-assured:rest-assured:5.3.1' + + // WebClient Test Dependencies + testImplementation 'com.squareup.okhttp3:mockwebserver' } tasks.named('test') { diff --git a/backend/src/main/java/shook/shook/song/application/ManiaDBSearchService.java b/backend/src/main/java/shook/shook/song/application/ManiaDBSearchService.java new file mode 100644 index 000000000..02108ff14 --- /dev/null +++ b/backend/src/main/java/shook/shook/song/application/ManiaDBSearchService.java @@ -0,0 +1,80 @@ +package shook.shook.song.application; + +import java.nio.charset.StandardCharsets; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatusCode; +import org.springframework.http.MediaType; +import org.springframework.stereotype.Service; +import org.springframework.web.reactive.function.client.WebClient; +import shook.shook.song.application.dto.UnregisteredSongResponse; +import shook.shook.song.application.dto.maniadb.ManiaDBAPISearchResponse; +import shook.shook.song.application.dto.maniadb.SearchedSongFromManiaDBApiResponses; +import shook.shook.song.exception.ExternalApiException; +import shook.shook.song.exception.UnregisteredSongException; +import shook.shook.util.StringChecker; + +@RequiredArgsConstructor +@Service +public class ManiaDBSearchService { + + private static final String MANIA_DB_API_URI = "/%s/?sr=song&display=%d&key=example&v=0.5"; + private static final int SEARCH_SIZE = 100; + private static final String SPECIAL_MARK_REGEX = "[^ㄱ-ㅎㅏ-ㅣ가-힣a-zA-Z0-9,. ]"; + + private final WebClient webClient; + + public List searchSongs(final String searchWord) { + validateSearchWord(searchWord); + + final String parsedSearchWord = replaceSpecialMark(searchWord); + final SearchedSongFromManiaDBApiResponses searchResult = getSearchResult(parsedSearchWord); + + if (Objects.isNull(searchResult.getSongs())) { + return Collections.emptyList(); + } + + return searchResult.getSongs().stream() + .map(UnregisteredSongResponse::from) + .toList(); + } + + private void validateSearchWord(final String searchWord) { + if (StringChecker.isNullOrBlank(searchWord)) { + throw new UnregisteredSongException.NullOrBlankSearchWordException(); + } + } + + private String replaceSpecialMark(final String rawSearchWord) { + return rawSearchWord.replaceAll(SPECIAL_MARK_REGEX, ""); + } + + private SearchedSongFromManiaDBApiResponses getSearchResult(final String searchWord) { + final String searchUrl = String.format(MANIA_DB_API_URI, searchWord, SEARCH_SIZE); + final ManiaDBAPISearchResponse result = getResultFromManiaDB(searchUrl); + + if (Objects.isNull(result)) { + throw new ExternalApiException.EmptyResultException(); + } + + return result.getSongs(); + } + + private ManiaDBAPISearchResponse getResultFromManiaDB(final String searchUrl) { + return webClient.get() + .uri(searchUrl) + .accept(MediaType.TEXT_XML) + .acceptCharset(StandardCharsets.UTF_8) + .retrieve() + .onStatus(HttpStatusCode::is4xxClientError, (clientResponse) -> { + throw new ExternalApiException.ManiaDBClientException(); + }) + .onStatus(HttpStatusCode::is5xxServerError, (clientResponse) -> { + throw new ExternalApiException.ManiaDBServerException(); + }) + .bodyToMono(ManiaDBAPISearchResponse.class) + .block(); + } +} diff --git a/backend/src/main/java/shook/shook/song/application/dto/UnregisteredSongResponse.java b/backend/src/main/java/shook/shook/song/application/dto/UnregisteredSongResponse.java new file mode 100644 index 000000000..20da84abb --- /dev/null +++ b/backend/src/main/java/shook/shook/song/application/dto/UnregisteredSongResponse.java @@ -0,0 +1,52 @@ +package shook.shook.song.application.dto; + +import java.util.stream.Collectors; +import lombok.AllArgsConstructor; +import lombok.Getter; +import shook.shook.song.application.dto.maniadb.SearchedSongFromManiaDBApiResponse; +import shook.shook.song.application.dto.maniadb.SongArtistResponse; + +@AllArgsConstructor +@Getter +public class UnregisteredSongResponse { + + private static final String EMPTY_SINGER = ""; + private static final String SINGER_DELIMITER = ", "; + + private String title; + private String singer; + private String albumImageUrl; + + public static UnregisteredSongResponse from( + final SearchedSongFromManiaDBApiResponse searchedSongFromManiaDBApiResponse) { + if (isEmptyArtists(searchedSongFromManiaDBApiResponse)) { + return new UnregisteredSongResponse( + searchedSongFromManiaDBApiResponse.getTitle().trim(), + EMPTY_SINGER, + searchedSongFromManiaDBApiResponse.getAlbum().getImage().trim() + ); + } + + final String singers = collectToString(searchedSongFromManiaDBApiResponse); + + return new UnregisteredSongResponse( + searchedSongFromManiaDBApiResponse.getTitle().trim(), + singers, + searchedSongFromManiaDBApiResponse.getAlbum().getImage().trim() + ); + } + + private static boolean isEmptyArtists( + final SearchedSongFromManiaDBApiResponse searchedSongFromManiaDBApiResponse) { + return searchedSongFromManiaDBApiResponse.getTrackArtists() == null + || searchedSongFromManiaDBApiResponse.getTrackArtists().getArtists() == null; + } + + private static String collectToString( + final SearchedSongFromManiaDBApiResponse searchedSongFromManiaDBApiResponse) { + return searchedSongFromManiaDBApiResponse.getTrackArtists().getArtists().stream() + .map(SongArtistResponse::getName) + .map(String::trim) + .collect(Collectors.joining(SINGER_DELIMITER)); + } +} diff --git a/backend/src/main/java/shook/shook/song/application/dto/maniadb/ManiaDBAPISearchResponse.java b/backend/src/main/java/shook/shook/song/application/dto/maniadb/ManiaDBAPISearchResponse.java new file mode 100644 index 000000000..12b4acab4 --- /dev/null +++ b/backend/src/main/java/shook/shook/song/application/dto/maniadb/ManiaDBAPISearchResponse.java @@ -0,0 +1,13 @@ +package shook.shook.song.application.dto.maniadb; + +import jakarta.xml.bind.annotation.XmlElement; +import jakarta.xml.bind.annotation.XmlRootElement; +import lombok.Getter; + +@Getter +@XmlRootElement(name = "rss") +public class ManiaDBAPISearchResponse { + + @XmlElement(name = "channel") + private SearchedSongFromManiaDBApiResponses songs; +} diff --git a/backend/src/main/java/shook/shook/song/application/dto/maniadb/SearchedSongFromManiaDBApiResponse.java b/backend/src/main/java/shook/shook/song/application/dto/maniadb/SearchedSongFromManiaDBApiResponse.java new file mode 100644 index 000000000..655ac3041 --- /dev/null +++ b/backend/src/main/java/shook/shook/song/application/dto/maniadb/SearchedSongFromManiaDBApiResponse.java @@ -0,0 +1,19 @@ +package shook.shook.song.application.dto.maniadb; + +import jakarta.xml.bind.annotation.XmlElement; +import jakarta.xml.bind.annotation.XmlRootElement; +import lombok.Getter; + +@Getter +@XmlRootElement(name = "item") +public class SearchedSongFromManiaDBApiResponse { + + @XmlElement(name = "title") + private String title; + + @XmlElement(name = "trackartists", namespace = "http://www.maniadb.com/api") + private SongTrackArtistsResponse trackArtists; + + @XmlElement(name = "album", namespace = "http://www.maniadb.com/api") + private SongAlbumResponse album; +} diff --git a/backend/src/main/java/shook/shook/song/application/dto/maniadb/SearchedSongFromManiaDBApiResponses.java b/backend/src/main/java/shook/shook/song/application/dto/maniadb/SearchedSongFromManiaDBApiResponses.java new file mode 100644 index 000000000..77f5abede --- /dev/null +++ b/backend/src/main/java/shook/shook/song/application/dto/maniadb/SearchedSongFromManiaDBApiResponses.java @@ -0,0 +1,14 @@ +package shook.shook.song.application.dto.maniadb; + +import jakarta.xml.bind.annotation.XmlElement; +import jakarta.xml.bind.annotation.XmlRootElement; +import java.util.List; +import lombok.Getter; + +@Getter +@XmlRootElement(name = "channel") +public class SearchedSongFromManiaDBApiResponses { + + @XmlElement(name = "item") + private List songs; +} diff --git a/backend/src/main/java/shook/shook/song/application/dto/maniadb/SongAlbumResponse.java b/backend/src/main/java/shook/shook/song/application/dto/maniadb/SongAlbumResponse.java new file mode 100644 index 000000000..f80728210 --- /dev/null +++ b/backend/src/main/java/shook/shook/song/application/dto/maniadb/SongAlbumResponse.java @@ -0,0 +1,13 @@ +package shook.shook.song.application.dto.maniadb; + +import jakarta.xml.bind.annotation.XmlElement; +import jakarta.xml.bind.annotation.XmlRootElement; +import lombok.Getter; + +@Getter +@XmlRootElement(name = "album", namespace = "http://www.maniadb.com/api") +public class SongAlbumResponse { + + @XmlElement(name = "image") + private String image; +} diff --git a/backend/src/main/java/shook/shook/song/application/dto/maniadb/SongArtistResponse.java b/backend/src/main/java/shook/shook/song/application/dto/maniadb/SongArtistResponse.java new file mode 100644 index 000000000..969fe44a8 --- /dev/null +++ b/backend/src/main/java/shook/shook/song/application/dto/maniadb/SongArtistResponse.java @@ -0,0 +1,13 @@ +package shook.shook.song.application.dto.maniadb; + +import jakarta.xml.bind.annotation.XmlElement; +import jakarta.xml.bind.annotation.XmlRootElement; +import lombok.Getter; + +@Getter +@XmlRootElement(name = "artist", namespace = "http://www.maniadb.com/api") +public class SongArtistResponse { + + @XmlElement(name = "name") + private String name; +} diff --git a/backend/src/main/java/shook/shook/song/application/dto/maniadb/SongTrackArtistsResponse.java b/backend/src/main/java/shook/shook/song/application/dto/maniadb/SongTrackArtistsResponse.java new file mode 100644 index 000000000..34b420a26 --- /dev/null +++ b/backend/src/main/java/shook/shook/song/application/dto/maniadb/SongTrackArtistsResponse.java @@ -0,0 +1,14 @@ +package shook.shook.song.application.dto.maniadb; + +import jakarta.xml.bind.annotation.XmlElement; +import jakarta.xml.bind.annotation.XmlRootElement; +import java.util.List; +import lombok.Getter; + +@Getter +@XmlRootElement(name = "trackartists", namespace = "http://www.maniadb.com/api") +public class SongTrackArtistsResponse { + + @XmlElement(name = "artist", namespace = "http://www.maniadb.com/api") + private List artists; +} diff --git a/backend/src/main/java/shook/shook/song/config/ManiaDBConfiguration.java b/backend/src/main/java/shook/shook/song/config/ManiaDBConfiguration.java new file mode 100644 index 000000000..300cb9fd2 --- /dev/null +++ b/backend/src/main/java/shook/shook/song/config/ManiaDBConfiguration.java @@ -0,0 +1,30 @@ +package shook.shook.song.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.codec.xml.Jaxb2XmlDecoder; +import org.springframework.web.reactive.function.client.ExchangeStrategies; +import org.springframework.web.reactive.function.client.WebClient; + +@Configuration +public class ManiaDBConfiguration { + + private static final String MANIA_DB_BASE_URL = "http://www.maniadb.com/api/search"; + + @Bean + public WebClient getWebClient() { + return WebClient.builder() + .baseUrl(MANIA_DB_BASE_URL) + .exchangeStrategies( + ExchangeStrategies.builder() + .codecs(configurer -> + configurer.defaultCodecs().jaxb2Decoder(new Jaxb2XmlDecoder()) + ) + .codecs(configurer -> + configurer.defaultCodecs().maxInMemorySize(4 * 1024 * 1024) + ) + .build() + ) + .build(); + } +} diff --git a/backend/src/main/java/shook/shook/song/exception/ExternalApiException.java b/backend/src/main/java/shook/shook/song/exception/ExternalApiException.java new file mode 100644 index 000000000..203059da5 --- /dev/null +++ b/backend/src/main/java/shook/shook/song/exception/ExternalApiException.java @@ -0,0 +1,25 @@ +package shook.shook.song.exception; + +public class ExternalApiException extends RuntimeException { + + public static class EmptyResultException extends ExternalApiException { + + public EmptyResultException() { + super(); + } + } + + public static class ManiaDBServerException extends ExternalApiException { + + public ManiaDBServerException() { + super(); + } + } + + public static class ManiaDBClientException extends ExternalApiException { + + public ManiaDBClientException() { + super(); + } + } +} diff --git a/backend/src/main/java/shook/shook/song/exception/UnregisteredSongException.java b/backend/src/main/java/shook/shook/song/exception/UnregisteredSongException.java new file mode 100644 index 000000000..2e83be56d --- /dev/null +++ b/backend/src/main/java/shook/shook/song/exception/UnregisteredSongException.java @@ -0,0 +1,12 @@ +package shook.shook.song.exception; + +public class UnregisteredSongException extends RuntimeException { + + public static class NullOrBlankSearchWordException extends UnregisteredSongException { + + public NullOrBlankSearchWordException() { + super(); + } + } + +} diff --git a/backend/src/main/java/shook/shook/song/ui/UnregisteredSongSearchController.java b/backend/src/main/java/shook/shook/song/ui/UnregisteredSongSearchController.java new file mode 100644 index 000000000..acf1ca560 --- /dev/null +++ b/backend/src/main/java/shook/shook/song/ui/UnregisteredSongSearchController.java @@ -0,0 +1,29 @@ +package shook.shook.song.ui; + +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import shook.shook.song.application.ManiaDBSearchService; +import shook.shook.song.application.dto.UnregisteredSongResponse; + +@RequiredArgsConstructor +@RequestMapping("/songs/unregistered/search") +@RestController +public class UnregisteredSongSearchController { + + private final ManiaDBSearchService maniaDBSearchService; + + @GetMapping + public ResponseEntity> searchUnregisteredSong( + final @RequestParam("keyword") String searchWord + ) { + final List songs = maniaDBSearchService.searchSongs( + searchWord); + + return ResponseEntity.ok(songs); + } +} diff --git a/backend/src/main/resources/application.yml b/backend/src/main/resources/application.yml index 2aaf383f9..80884d640 100644 --- a/backend/src/main/resources/application.yml +++ b/backend/src/main/resources/application.yml @@ -1,6 +1,7 @@ spring: profiles: active: local + config: import: classpath:shook-security/application.yml diff --git a/backend/src/test/java/shook/shook/song/application/ManiaDBSearchServiceTest.java b/backend/src/test/java/shook/shook/song/application/ManiaDBSearchServiceTest.java new file mode 100644 index 000000000..2f406ea5b --- /dev/null +++ b/backend/src/test/java/shook/shook/song/application/ManiaDBSearchServiceTest.java @@ -0,0 +1,422 @@ +package shook.shook.song.application; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertAll; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.List; +import okhttp3.mockwebserver.MockResponse; +import okhttp3.mockwebserver.MockWebServer; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.NullAndEmptySource; +import org.junit.jupiter.params.provider.ValueSource; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.codec.xml.Jaxb2XmlDecoder; +import org.springframework.web.reactive.function.client.ExchangeStrategies; +import org.springframework.web.reactive.function.client.WebClient; +import shook.shook.song.application.dto.UnregisteredSongResponse; +import shook.shook.song.exception.ExternalApiException; +import shook.shook.song.exception.ExternalApiException.EmptyResultException; +import shook.shook.song.exception.UnregisteredSongException; + +@ExtendWith(MockitoExtension.class) +class ManiaDBSearchServiceTest { + + private static final String SEARCH_WORD = "흔들리는꽃속에서네샴푸향이느껴진거야"; + private static final String SPECIAL_MARK_SEARCH_WORD = "\b흔%들리는꽃*들속@에서네샴/푸향이+느껴진-거야!\t"; + private static final String SEARCH_RESULT = """ + + + + + <![CDATA[Maniadb Open API v0.5 : Search song for "흔들리는꽃속에서네샴푸향이느껴진거야"]]> + + www.maniadb.com + + + + Fri, 28 Jul 2023 12:03:53 +0900 + 1 + 1 + 100 + + + + + + <![CDATA[흔들리는 꽃들 속에서 네 샴푸향이 느껴진거야]]> + + + + + + + + + + + + + + + + + + + + + + + + + <![CDATA[멜로가 체질 by 김태성 [ost] (2019)]]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + """; + + private static final String EMPTY_RESULT_SEARCH_WORD = "빈값검색"; + private static final String EMPTY_RESULT = """ + + + + + <![CDATA[Maniadb Open API v0.5 : Search song for "빈값검색"]]> + + www.maniadb.com + + + + Fri, 28 Jul 2023 17:37:01 +0900 + 0 + 1 + 100 + + + + + + """; + + private static final String EMPTY_SINGER_SEARCH_RESULT = """ + + + + + <![CDATA[Maniadb Open API v0.5 : Search song for "흔들리는꽃속에서네샴푸향이느껴진거야"]]> + + www.maniadb.com + + + + Fri, 28 Jul 2023 12:03:53 +0900 + 1 + 1 + 100 + + + + + + <![CDATA[흔들리는 꽃들 속에서 네 샴푸향이 느껴진거야]]> + + + + + + + + + + + + + + + + + + + + + + + + + <![CDATA[멜로가 체질 by 김태성 [ost] (2019)]]> + + + + + + + + + + + + + + + + + + + + + + + """; + + private static final String EMPTY_STRING = ""; + + private MockWebServer mockServer; + private ManiaDBSearchService maniaDBSearchService; + + @BeforeEach + void startServer() { + mockServer = new MockWebServer(); + maniaDBSearchService = new ManiaDBSearchService( + WebClient.builder() + .baseUrl(mockServer.url("/") + .toString()) + .exchangeStrategies( + ExchangeStrategies.builder() + .codecs(configurer -> + configurer.defaultCodecs().jaxb2Decoder(new Jaxb2XmlDecoder()) + ) + .codecs(configurer -> + configurer.defaultCodecs().maxInMemorySize(4 * 1024 * 1024) + ) + .build() + ) + .build() + ); + } + + @AfterEach + void tearDown() throws IOException { + mockServer.shutdown(); + } + + @DisplayName("검색 요청을 보내면 XML 응답을 정상적으로 받고, 파싱한 데이터를 응답한다.") + @Test + void search() { + // given + mockServer.enqueue(new MockResponse() + .setResponseCode(HttpStatus.OK.value()) + .addHeader(HttpHeaders.CONTENT_TYPE, MediaType.TEXT_XML) + .addHeader(HttpHeaders.ACCEPT_CHARSET, StandardCharsets.UTF_8) + .setBody(SEARCH_RESULT) + ); + + final UnregisteredSongResponse expectedResponse = new UnregisteredSongResponse( + "흔들리는 꽃들 속에서 네 샴푸향이 느껴진거야", + "장범준", + "http://i.maniadb.com/images/album/777/777829_1_f.jpg" + ); + + // when + final List responses = + maniaDBSearchService.searchSongs(SEARCH_WORD); + + // then + assertAll( + () -> assertThat(responses).hasSize(1), + () -> assertThat(responses.get(0)).usingRecursiveComparison() + .isEqualTo(expectedResponse) + ); + } + + @DisplayName("검색 단어가 null 이거나 빈 문자열인 경우, 예외가 발생한다.") + @NullAndEmptySource + @ParameterizedTest(name = "검색 단어가 \"{0}\" 인 경우") + void nullOrBlankSearchWord(final String searchWord) { + // given + // when + // then + assertThatThrownBy(() -> maniaDBSearchService.searchSongs(searchWord)) + .isInstanceOf(UnregisteredSongException.NullOrBlankSearchWordException.class); + } + + @DisplayName("특수문자가 포함된 검색 단어가 입력된 경우, 특수문자를 제거하고 검색한다.") + @Test + void searchBySpecialMarkSearchWord() { + // given + mockServer.enqueue(new MockResponse() + .setResponseCode(HttpStatus.OK.value()) + .addHeader(HttpHeaders.CONTENT_TYPE, MediaType.TEXT_XML) + .addHeader(HttpHeaders.ACCEPT_CHARSET, StandardCharsets.UTF_8) + .setBody(SEARCH_RESULT) + ); + + final UnregisteredSongResponse expectedResponse = new UnregisteredSongResponse( + "흔들리는 꽃들 속에서 네 샴푸향이 느껴진거야", + "장범준", + "http://i.maniadb.com/images/album/777/777829_1_f.jpg" + ); + + // when + final List responses = + maniaDBSearchService.searchSongs(SPECIAL_MARK_SEARCH_WORD); + + // then + assertAll( + () -> assertThat(responses).hasSize(1), + () -> assertThat(responses.get(0)).usingRecursiveComparison() + .isEqualTo(expectedResponse) + ); + } + + @DisplayName("검색할 단어로 빈 값이 입력된 경우, 예외가 발생한다.") + @ValueSource(strings = {" ", "\t", "\n", ""}) + @ParameterizedTest(name = "검색 단어가 \"{0}\" 인 경우") + void searchWithBlankWord(final String blankSearchWord) { + // given + // when + // then + assertThatThrownBy(() -> maniaDBSearchService.searchSongs(blankSearchWord)) + .isInstanceOf(UnregisteredSongException.NullOrBlankSearchWordException.class); + } + + @DisplayName("XML 응답에 노래가 존재하지 않으면 빈 리스트를 리턴한다.") + @Test + void searchResultReturnEmptyList() { + // given + mockServer.enqueue(new MockResponse() + .setResponseCode(HttpStatus.OK.value()) + .addHeader(HttpHeaders.CONTENT_TYPE, MediaType.TEXT_XML) + .addHeader(HttpHeaders.ACCEPT_CHARSET, StandardCharsets.UTF_8) + .setBody(EMPTY_RESULT) + ); + + // when + final List responses = + maniaDBSearchService.searchSongs(EMPTY_RESULT_SEARCH_WORD); + + // then + assertThat(responses).isEmpty(); + } + + @DisplayName("XML 응답에 가수가 없으면 가수의 값으로 빈 문자열을 리턴한다.") + @Test + void searchResultReturnEmptySinger() { + // given + mockServer.enqueue(new MockResponse() + .setResponseCode(HttpStatus.OK.value()) + .addHeader(HttpHeaders.CONTENT_TYPE, MediaType.TEXT_XML) + .addHeader(HttpHeaders.ACCEPT_CHARSET, StandardCharsets.UTF_8) + .setBody(EMPTY_SINGER_SEARCH_RESULT) + ); + + final UnregisteredSongResponse expectedResponse = new UnregisteredSongResponse( + "흔들리는 꽃들 속에서 네 샴푸향이 느껴진거야", + "", + "http://i.maniadb.com/images/album/777/777829_1_f.jpg" + ); + + // when + final List responses = + maniaDBSearchService.searchSongs(SEARCH_WORD); + + // then + assertAll( + () -> assertThat(responses).hasSize(1), + () -> assertThat(responses.get(0)).usingRecursiveComparison() + .isEqualTo(expectedResponse) + ); + } + + @DisplayName("XML 응답을 파싱한 결과값이 null 이면 예외가 발생한다.") + @Test + void nullResultThrowException() { + // given + mockServer.enqueue(new MockResponse() + .setResponseCode(HttpStatus.OK.value()) + .addHeader(HttpHeaders.CONTENT_TYPE, MediaType.TEXT_XML) + .addHeader(HttpHeaders.ACCEPT_CHARSET, StandardCharsets.UTF_8) + .setBody(EMPTY_STRING) + ); + + // when + // then + assertThatThrownBy(() -> maniaDBSearchService.searchSongs(SEARCH_WORD)) + .isInstanceOf(EmptyResultException.class); + } + + @DisplayName("ManiaDB로 보내는 요청이 잘못된 경우, 예외가 발생한다.") + @Test + void wrongRequestThrowException() { + // given + mockServer.enqueue(new MockResponse() + .setResponseCode(HttpStatus.BAD_REQUEST.value()) + .addHeader(HttpHeaders.CONTENT_TYPE, MediaType.TEXT_XML) + .addHeader(HttpHeaders.ACCEPT_CHARSET, StandardCharsets.UTF_8) + ); + + // when + // then + assertThatThrownBy(() -> maniaDBSearchService.searchSongs(SEARCH_WORD)) + .isInstanceOf(ExternalApiException.ManiaDBClientException.class); + } + + @DisplayName("ManiaDB API 서버에서 예외가 발생한 경우, 예외가 발생한다.") + @Test + void maniaDBServerExceptionThrowException() { + // given + mockServer.enqueue(new MockResponse() + .setResponseCode(HttpStatus.INTERNAL_SERVER_ERROR.value()) + .addHeader(HttpHeaders.CONTENT_TYPE, MediaType.TEXT_XML) + .addHeader(HttpHeaders.ACCEPT_CHARSET, StandardCharsets.UTF_8) + ); + + // when + // then + assertThatThrownBy(() -> maniaDBSearchService.searchSongs(SEARCH_WORD)) + .isInstanceOf(ExternalApiException.ManiaDBServerException.class); + } +} diff --git a/backend/src/test/java/shook/shook/song/ui/SongControllerTest.java b/backend/src/test/java/shook/shook/song/ui/SongControllerTest.java index 00b3c975c..674187fb7 100644 --- a/backend/src/test/java/shook/shook/song/ui/SongControllerTest.java +++ b/backend/src/test/java/shook/shook/song/ui/SongControllerTest.java @@ -22,14 +22,14 @@ class SongControllerTest { @LocalServerPort public int port; + @Autowired + private SongRepository songRepository; + @BeforeEach void setUp() { RestAssured.port = port; } - @Autowired - private SongRepository songRepository; - @DisplayName("노래 정보를 조회시 제목, 가수, 길이, URL, 킬링파트를 담은 응답을 반환한다.") @Test void showSongById() {