diff --git a/.gitignore b/.gitignore index 1b90726..7284b6d 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,5 @@ .idea/ .gradle/ build/ + +.DS_Store diff --git a/src/main/kotlin/common/util/CacheUtils.kt b/src/main/kotlin/common/util/CacheUtils.kt new file mode 100644 index 0000000..88da2b9 --- /dev/null +++ b/src/main/kotlin/common/util/CacheUtils.kt @@ -0,0 +1,34 @@ +package com.wafflestudio.seminar.spring2023.common.util + +import java.util.concurrent.ConcurrentHashMap + +class CacheUtils( + private val TTL: Long + ) +{ + private val cache = ConcurrentHashMap>() + + fun get(key: K): V? { + return if (cache[key] == null) { + null + } else if (cache[key]!!.isExpired()) { + cache.remove(key) + null + } else { + cache[key]!!.value + } + } + + fun put(key: K, value: V) { + cache[key] = CacheEntry(value, System.currentTimeMillis() + TTL) + } + + class CacheEntry( + val value: V, + private val expireAt: Long + ) { + fun isExpired(): Boolean { + return System.currentTimeMillis() > expireAt + } + } +} diff --git a/src/main/kotlin/common/util/SongUtils.kt b/src/main/kotlin/common/util/SongUtils.kt new file mode 100644 index 0000000..3caab58 --- /dev/null +++ b/src/main/kotlin/common/util/SongUtils.kt @@ -0,0 +1,24 @@ +package com.wafflestudio.seminar.spring2023.common.util + +import com.wafflestudio.seminar.spring2023.song.repository.SongEntity +import com.wafflestudio.seminar.spring2023.song.service.Artist +import com.wafflestudio.seminar.spring2023.song.service.Song + +class SongUtils { + companion object { + fun mapSongEntityToSong(entity: SongEntity): Song { + val artistList = entity.artists.map { artistEntity -> + Artist(artistEntity.id, artistEntity.name) + } + + return Song( + id = entity.id, + title = entity.title, + artists = artistList, + album = entity.album.title, + image = entity.album.image, + duration = entity.duration + ) + } + } +} diff --git a/src/main/kotlin/playlist/controller/PlaylistController.kt b/src/main/kotlin/playlist/controller/PlaylistController.kt index 07064a0..204f77c 100644 --- a/src/main/kotlin/playlist/controller/PlaylistController.kt +++ b/src/main/kotlin/playlist/controller/PlaylistController.kt @@ -1,8 +1,6 @@ package com.wafflestudio.seminar.spring2023.playlist.controller -import com.wafflestudio.seminar.spring2023.playlist.service.Playlist -import com.wafflestudio.seminar.spring2023.playlist.service.PlaylistException -import com.wafflestudio.seminar.spring2023.playlist.service.PlaylistGroup +import com.wafflestudio.seminar.spring2023.playlist.service.* import com.wafflestudio.seminar.spring2023.user.service.Authenticated import com.wafflestudio.seminar.spring2023.user.service.User import org.springframework.http.ResponseEntity @@ -14,11 +12,14 @@ import org.springframework.web.bind.annotation.PostMapping import org.springframework.web.bind.annotation.RestController @RestController -class PlaylistController { +class PlaylistController( + private val playlistService: PlaylistService, + private val playlistLikeService: PlaylistLikeService +) { @GetMapping("/api/v1/playlist-groups") fun getPlaylistGroup(): PlaylistGroupsResponse { - TODO() + return PlaylistGroupsResponse(playlistService.getGroups()) } @GetMapping("/api/v1/playlists/{id}") @@ -26,7 +27,10 @@ class PlaylistController { @PathVariable id: Long, user: User?, ): PlaylistResponse { - TODO() + val playlist = playlistService.get(id) + val liked = if (user == null) false + else playlistLikeService.exists(playlistId = id, userId = user.id) + return PlaylistResponse(playlist, liked) } @PostMapping("/api/v1/playlists/{id}/likes") @@ -34,7 +38,7 @@ class PlaylistController { @PathVariable id: Long, @Authenticated user: User, ) { - TODO() + playlistLikeService.create(playlistId = id, userId = user.id) } @DeleteMapping("/api/v1/playlists/{id}/likes") @@ -42,12 +46,16 @@ class PlaylistController { @PathVariable id: Long, @Authenticated user: User, ) { - TODO() + playlistLikeService.delete(playlistId = id, userId = user.id) } @ExceptionHandler fun handleException(e: PlaylistException): ResponseEntity { - TODO() + val status = when (e) { + is PlaylistAlreadyLikedException, is PlaylistNeverLikedException -> 409 + is PlaylistNotFoundException -> 404 + } + return ResponseEntity.status(status).build() } } diff --git a/src/main/kotlin/playlist/repository/PlaylistEntity.kt b/src/main/kotlin/playlist/repository/PlaylistEntity.kt new file mode 100644 index 0000000..2a609c2 --- /dev/null +++ b/src/main/kotlin/playlist/repository/PlaylistEntity.kt @@ -0,0 +1,26 @@ +package com.wafflestudio.seminar.spring2023.playlist.repository + +import com.wafflestudio.seminar.spring2023.song.repository.SongEntity +import jakarta.persistence.* +import org.hibernate.annotations.BatchSize +import org.hibernate.annotations.Fetch +import org.hibernate.annotations.FetchMode +import org.springframework.web.bind.annotation.Mapping + +@Entity(name = "playlists") +class PlaylistEntity( + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + val id: Long = 0L, + val title: String, + val subtitle: String, + val image: String, + + @ManyToMany + @JoinTable( + name = "playlist_songs", + joinColumns = [JoinColumn(name = "playlist_id")], + inverseJoinColumns = [JoinColumn(name = "song_id")] + ) + val songs: Set, +) diff --git a/src/main/kotlin/playlist/repository/PlaylistGroupEntity.kt b/src/main/kotlin/playlist/repository/PlaylistGroupEntity.kt new file mode 100644 index 0000000..77011db --- /dev/null +++ b/src/main/kotlin/playlist/repository/PlaylistGroupEntity.kt @@ -0,0 +1,16 @@ +package com.wafflestudio.seminar.spring2023.playlist.repository + +import jakarta.persistence.* + +@Entity(name = "playlist_groups") +class PlaylistGroupEntity( + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + val id: Long = 0L, + val title: String, + val open: Boolean, + + @OneToMany + @JoinColumn(name = "group_id") + val playlists: List +) diff --git a/src/main/kotlin/playlist/repository/PlaylistGroupRepository.kt b/src/main/kotlin/playlist/repository/PlaylistGroupRepository.kt new file mode 100644 index 0000000..3151838 --- /dev/null +++ b/src/main/kotlin/playlist/repository/PlaylistGroupRepository.kt @@ -0,0 +1,9 @@ +package com.wafflestudio.seminar.spring2023.playlist.repository + +import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.data.jpa.repository.Query + +interface PlaylistGroupRepository : JpaRepository { + @Query("SELECT pg FROM playlist_groups pg LEFT JOIN FETCH pg.playlists WHERE pg.open = true") + fun findOpenPlaylistGroups(): List +} diff --git a/src/main/kotlin/playlist/repository/PlaylistLikesEntity.kt b/src/main/kotlin/playlist/repository/PlaylistLikesEntity.kt new file mode 100644 index 0000000..af4dca0 --- /dev/null +++ b/src/main/kotlin/playlist/repository/PlaylistLikesEntity.kt @@ -0,0 +1,15 @@ +package com.wafflestudio.seminar.spring2023.playlist.repository + +import jakarta.persistence.Entity +import jakarta.persistence.GeneratedValue +import jakarta.persistence.GenerationType +import jakarta.persistence.Id + +@Entity(name = "playlist_likes") +class PlaylistLikesEntity( + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + val id: Long = 0L, + val playlist_id: Long, + val user_id: Long, +) diff --git a/src/main/kotlin/playlist/repository/PlaylistLikesRepository.kt b/src/main/kotlin/playlist/repository/PlaylistLikesRepository.kt new file mode 100644 index 0000000..9bbb83b --- /dev/null +++ b/src/main/kotlin/playlist/repository/PlaylistLikesRepository.kt @@ -0,0 +1,9 @@ +package com.wafflestudio.seminar.spring2023.playlist.repository + +import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.data.jpa.repository.Query + +interface PlaylistLikesRepository: JpaRepository { + @Query("SELECT p FROM playlist_likes p WHERE p.playlist_id = :playlistId and p.user_id = :userId") + fun findUserPlaylistLike(playlistId: Long, userId: Long): PlaylistLikesEntity? +} diff --git a/src/main/kotlin/playlist/repository/PlaylistRepository.kt b/src/main/kotlin/playlist/repository/PlaylistRepository.kt new file mode 100644 index 0000000..b412174 --- /dev/null +++ b/src/main/kotlin/playlist/repository/PlaylistRepository.kt @@ -0,0 +1,10 @@ +package com.wafflestudio.seminar.spring2023.playlist.repository + +import org.springframework.data.jpa.repository.EntityGraph +import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.data.jpa.repository.Query + +interface PlaylistRepository : JpaRepository { + @Query("SELECT DISTINCT p FROM playlists p LEFT JOIN FETCH p.songs s LEFT JOIN FETCH s.album a LEFT JOIN FETCH s.artists ar WHERE p.id = :id") + fun findPlaylistEntityById(id: Long): PlaylistEntity? +} diff --git a/src/main/kotlin/playlist/service/PlaylistLikeServiceImpl.kt b/src/main/kotlin/playlist/service/PlaylistLikeServiceImpl.kt index d7e6d86..0e444c7 100644 --- a/src/main/kotlin/playlist/service/PlaylistLikeServiceImpl.kt +++ b/src/main/kotlin/playlist/service/PlaylistLikeServiceImpl.kt @@ -1,19 +1,45 @@ package com.wafflestudio.seminar.spring2023.playlist.service +import com.wafflestudio.seminar.spring2023.playlist.repository.PlaylistLikesEntity +import com.wafflestudio.seminar.spring2023.playlist.repository.PlaylistLikesRepository +import com.wafflestudio.seminar.spring2023.playlist.repository.PlaylistRepository import org.springframework.stereotype.Service @Service -class PlaylistLikeServiceImpl : PlaylistLikeService { +class PlaylistLikeServiceImpl( + private val playlistRepository: PlaylistRepository, + private val playlistLikesRepository: PlaylistLikesRepository +) : PlaylistLikeService { override fun exists(playlistId: Long, userId: Long): Boolean { - TODO() + if (!playlistRepository.existsById(playlistId)) { + throw PlaylistNotFoundException() + } + + return playlistLikesRepository.findUserPlaylistLike(playlistId, userId) != null } override fun create(playlistId: Long, userId: Long) { - TODO() + if (!playlistRepository.existsById(playlistId)) { + throw PlaylistNotFoundException() + } + + if (playlistLikesRepository.findUserPlaylistLike(playlistId, userId) != null) { + throw PlaylistAlreadyLikedException() + } + + playlistLikesRepository.save(PlaylistLikesEntity(playlist_id = playlistId, user_id = userId)) } override fun delete(playlistId: Long, userId: Long) { - TODO() + if (!playlistRepository.existsById(playlistId)) { + throw PlaylistNotFoundException() + } + + if (playlistLikesRepository.findUserPlaylistLike(playlistId, userId) == null) { + throw PlaylistNeverLikedException() + } + + playlistLikesRepository.delete(PlaylistLikesEntity(playlist_id = playlistId, user_id = userId)) } } diff --git a/src/main/kotlin/playlist/service/PlaylistServiceCacheImpl.kt b/src/main/kotlin/playlist/service/PlaylistServiceCacheImpl.kt index 42c320c..9155faa 100644 --- a/src/main/kotlin/playlist/service/PlaylistServiceCacheImpl.kt +++ b/src/main/kotlin/playlist/service/PlaylistServiceCacheImpl.kt @@ -1,5 +1,6 @@ package com.wafflestudio.seminar.spring2023.playlist.service +import com.wafflestudio.seminar.spring2023.common.util.CacheUtils import org.springframework.stereotype.Service // TODO: 캐시 TTL이 10초가 되도록 캐시 구현체를 구현 (추가 과제) @@ -7,12 +8,25 @@ import org.springframework.stereotype.Service class PlaylistServiceCacheImpl( private val impl: PlaylistServiceImpl, ) : PlaylistService { - + private val PLAYLIST_GROUP_KEY = "Playlist Group Key" + private val CACHE_TIME: Long = 10000L + private val playlistGroupListCache = CacheUtils>(CACHE_TIME) + private val playlistCache = CacheUtils(CACHE_TIME) override fun getGroups(): List { - TODO("Not yet implemented") + var groups = playlistGroupListCache.get(PLAYLIST_GROUP_KEY) + if (groups == null) { + groups = impl.getGroups() + playlistGroupListCache.put(PLAYLIST_GROUP_KEY, groups) + } + return groups } override fun get(id: Long): Playlist { - TODO("Not yet implemented") + var playlist = playlistCache.get(id) + if (playlist == null) { + playlist = impl.get(id) + playlistCache.put(id, playlist) + } + return playlist } } diff --git a/src/main/kotlin/playlist/service/PlaylistServiceImpl.kt b/src/main/kotlin/playlist/service/PlaylistServiceImpl.kt index f5bc5df..1afd136 100644 --- a/src/main/kotlin/playlist/service/PlaylistServiceImpl.kt +++ b/src/main/kotlin/playlist/service/PlaylistServiceImpl.kt @@ -1,17 +1,43 @@ package com.wafflestudio.seminar.spring2023.playlist.service +import com.wafflestudio.seminar.spring2023.common.util.SongUtils +import com.wafflestudio.seminar.spring2023.playlist.repository.PlaylistEntity +import com.wafflestudio.seminar.spring2023.playlist.repository.PlaylistGroupRepository +import com.wafflestudio.seminar.spring2023.playlist.repository.PlaylistRepository +import jakarta.transaction.Transactional import org.springframework.context.annotation.Primary import org.springframework.stereotype.Service @Primary @Service -class PlaylistServiceImpl : PlaylistService { +class PlaylistServiceImpl( + private val playlistGroupRepository: PlaylistGroupRepository, + private val playlistRepository: PlaylistRepository + ) : PlaylistService { override fun getGroups(): List { - TODO() + val playlistGroups = playlistGroupRepository.findOpenPlaylistGroups() + return playlistGroups.filter { it.playlists.isNotEmpty() }.map { PlaylistGroup( + id = it.id, + title = it.title, + playlists = it.playlists.map { playlistEntity -> PlaylistBrief( + id = playlistEntity.id, + title = playlistEntity.title, + subtitle = playlistEntity.subtitle, + image = playlistEntity.image, + )} + )} } override fun get(id: Long): Playlist { - TODO() + val playlist = playlistRepository.findPlaylistEntityById(id) ?: throw PlaylistNotFoundException() + val songList = playlist.songs.map { SongUtils.mapSongEntityToSong(it) } + return Playlist( + id = playlist.id, + title = playlist.title, + subtitle = playlist.subtitle, + image = playlist.image, + songs = songList.sortedBy { it.id } + ) } } diff --git a/src/main/kotlin/song/controller/SongController.kt b/src/main/kotlin/song/controller/SongController.kt index 6f1a865..928a40b 100644 --- a/src/main/kotlin/song/controller/SongController.kt +++ b/src/main/kotlin/song/controller/SongController.kt @@ -8,20 +8,23 @@ import org.springframework.web.bind.annotation.RequestParam import org.springframework.web.bind.annotation.RestController @RestController -class SongController { - +class SongController( + private val songService: SongService, +) { @GetMapping("/api/v1/songs") fun searchSong( @RequestParam keyword: String, ): SearchSongResponse { - TODO() + val songs = songService.search(keyword) + return SearchSongResponse(songs) } @GetMapping("/api/v1/albums") fun searchAlbum( @RequestParam keyword: String, ): SearchAlbumResponse { - TODO() + val albums = songService.searchAlbum(keyword) + return SearchAlbumResponse(albums) } } diff --git a/src/main/kotlin/song/repository/AlbumRepository.kt b/src/main/kotlin/song/repository/AlbumRepository.kt index e9a579e..146116c 100644 --- a/src/main/kotlin/song/repository/AlbumRepository.kt +++ b/src/main/kotlin/song/repository/AlbumRepository.kt @@ -1,5 +1,9 @@ package com.wafflestudio.seminar.spring2023.song.repository import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.data.jpa.repository.Query -interface AlbumRepository : JpaRepository \ No newline at end of file +interface AlbumRepository : JpaRepository { + @Query("SELECT DISTINCT a FROM albums a LEFT JOIN FETCH a.artist ar WHERE a.title LIKE %:keyword%") + fun findAlbumsByTitleContaining(keyword: String): List +} \ No newline at end of file diff --git a/src/main/kotlin/song/repository/SongEntity.kt b/src/main/kotlin/song/repository/SongEntity.kt new file mode 100644 index 0000000..ff62b07 --- /dev/null +++ b/src/main/kotlin/song/repository/SongEntity.kt @@ -0,0 +1,25 @@ +package com.wafflestudio.seminar.spring2023.song.repository + +import com.wafflestudio.seminar.spring2023.song.service.Artist +import jakarta.persistence.* + +@Entity(name = "songs") +class SongEntity ( + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + val id: Long = 0L, + val title: String, + val duration: String, + + @ManyToOne + @JoinColumn(name = "album_id", referencedColumnName = "id") + val album: AlbumEntity, + + @ManyToMany + @JoinTable( + name = "song_artists", + joinColumns = [JoinColumn(name = "song_id")], + inverseJoinColumns = [JoinColumn(name = "artist_id")] + ) + val artists: Set +) diff --git a/src/main/kotlin/song/repository/SongRepository.kt b/src/main/kotlin/song/repository/SongRepository.kt new file mode 100644 index 0000000..49b374c --- /dev/null +++ b/src/main/kotlin/song/repository/SongRepository.kt @@ -0,0 +1,9 @@ +package com.wafflestudio.seminar.spring2023.song.repository + +import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.data.jpa.repository.Query + +interface SongRepository : JpaRepository { + @Query("SELECT DISTINCT s FROM songs s LEFT JOIN FETCH s.album a LEFT JOIN FETCH s.artists ar WHERE s.title LIKE %:keyword%") + fun findSongsByTitleContaining(keyword: String): List +} diff --git a/src/main/kotlin/song/service/SongArtists.kt b/src/main/kotlin/song/service/SongArtists.kt new file mode 100644 index 0000000..a09aaef --- /dev/null +++ b/src/main/kotlin/song/service/SongArtists.kt @@ -0,0 +1,7 @@ +package com.wafflestudio.seminar.spring2023.song.service + +data class SongArtists( + val id: Long, + val artist: Artist, + val song: Song, +) diff --git a/src/main/kotlin/song/service/SongServiceImpl.kt b/src/main/kotlin/song/service/SongServiceImpl.kt index 93ea395..fd53f33 100644 --- a/src/main/kotlin/song/service/SongServiceImpl.kt +++ b/src/main/kotlin/song/service/SongServiceImpl.kt @@ -1,15 +1,36 @@ package com.wafflestudio.seminar.spring2023.song.service +import com.wafflestudio.seminar.spring2023.common.util.SongUtils +import com.wafflestudio.seminar.spring2023.song.repository.AlbumEntity +import com.wafflestudio.seminar.spring2023.song.repository.AlbumRepository +import com.wafflestudio.seminar.spring2023.song.repository.SongRepository import org.springframework.stereotype.Service @Service -class SongServiceImpl : SongService { - +class SongServiceImpl( + private val songRepository: SongRepository, + private val albumRepository: AlbumRepository + ) : SongService { override fun search(keyword: String): List { - TODO() + val songEntities = songRepository.findSongsByTitleContaining(keyword) + val songList = songEntities.map { SongUtils.mapSongEntityToSong(it) } + return songList.sortedBy { song -> song.title.length } } override fun searchAlbum(keyword: String): List { - TODO() + val albumEntities = albumRepository.findAlbumsByTitleContaining(keyword) + val albumList = albumEntities.map { mapAlbumEntityToAlbum(it) } + return albumList.sortedBy { album -> album.title.length } + } + + private fun mapAlbumEntityToAlbum(entity: AlbumEntity): Album { + val artist = Artist(entity.artist.id, entity.artist.name) + + return Album( + entity.id, + entity.title, + entity.image, + artist, + ) } } diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index ca2f880..6fd2177 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -13,4 +13,4 @@ spring: password: 1234 jpa: defer-datasource-initialization: true - show-sql: true \ No newline at end of file + show-sql: true diff --git a/src/test/kotlin/playlist/PlaylistIntegrationTest.kt b/src/test/kotlin/playlist/PlaylistIntegrationTest.kt index 58de32b..472ac3b 100644 --- a/src/test/kotlin/playlist/PlaylistIntegrationTest.kt +++ b/src/test/kotlin/playlist/PlaylistIntegrationTest.kt @@ -1,13 +1,129 @@ package com.wafflestudio.seminar.spring2023.playlist +import com.fasterxml.jackson.databind.ObjectMapper +import jakarta.transaction.Transactional +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc import org.springframework.boot.test.context.SpringBootTest +import org.springframework.http.MediaType import org.springframework.test.web.servlet.MockMvc +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders +import org.springframework.test.web.servlet.result.MockMvcResultMatchers @AutoConfigureMockMvc +@Transactional @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) class PlaylistIntegrationTest @Autowired constructor( private val mvc: MockMvc, + private val mapper: ObjectMapper, ) { + @Test + fun `플레이리스트 그룹을 조회하면 200 응답과 그룹 정보를 내려준다`() { + val result = mvc.perform( + MockMvcRequestBuilders.get("/api/v1/playlist-groups") + .contentType(MediaType.APPLICATION_JSON) + ) + .andExpect(MockMvcResultMatchers.status().`is`(200)) + .andExpect(MockMvcResultMatchers.content().contentType(MediaType.APPLICATION_JSON)) + .andReturn().response.contentAsString + print(result); + } + + @Test + fun `미로그인 상태로 플레이리스트를 조회하면 200 응답과 정보를 내려준다`() { + val result = mvc.perform( + MockMvcRequestBuilders.get("/api/v1/playlists/1") + .contentType(MediaType.APPLICATION_JSON) + ) + .andExpect(MockMvcResultMatchers.status().`is`(200)) + .andExpect(MockMvcResultMatchers.content().contentType(MediaType.APPLICATION_JSON)) + .andReturn().response.contentAsString + val jsonNode = mapper.readTree(result) + assertThat(jsonNode.get("isLiked")?.asText()).isEqualTo("false") + } + + @Test + fun `로그인 상태로 플레이리스트에 좋아요 요청을 보내면 200 응답을 내려주고, 다시 좋아요 요청을 보내면 409 응답을 내려준다`() { + mvc.perform( + MockMvcRequestBuilders.post("/api/v1/playlists/1/likes") + .contentType(MediaType.APPLICATION_JSON) + .header("Authorization", "Bearer gnirps") + ) + .andExpect(MockMvcResultMatchers.status().`is`(200)) + + val result = mvc.perform( + MockMvcRequestBuilders.get("/api/v1/playlists/1") + .contentType(MediaType.APPLICATION_JSON) + .header("Authorization", "Bearer gnirps") + ) + .andExpect(MockMvcResultMatchers.status().`is`(200)) + .andExpect(MockMvcResultMatchers.content().contentType(MediaType.APPLICATION_JSON)) + .andReturn().response.contentAsString + val jsonNode = mapper.readTree(result) + assertThat(jsonNode.get("isLiked")?.asText()).isEqualTo("true") + + mvc.perform( + MockMvcRequestBuilders.post("/api/v1/playlists/1/likes") + .contentType(MediaType.APPLICATION_JSON) + .header("Authorization", "Bearer gnirps") + ) + .andExpect(MockMvcResultMatchers.status().`is`(409)) + } + + @Test + fun `로그인 상태로 플레이리스트에 좋아요 취소 요청을 보내면 200 응답을 내려준다`() { + mvc.perform( + MockMvcRequestBuilders.post("/api/v1/playlists/1/likes") + .contentType(MediaType.APPLICATION_JSON) + .header("Authorization", "Bearer gnirps") + ) + .andExpect(MockMvcResultMatchers.status().`is`(200)) + + mvc.perform( + MockMvcRequestBuilders.delete("/api/v1/playlists/1/likes") + .contentType(MediaType.APPLICATION_JSON) + .header("Authorization", "Bearer gnirps") + ) + .andExpect(MockMvcResultMatchers.status().`is`(200)) + } + + @Test + fun `로그인 상태로 좋아하지 않는 플레이리스트에 좋아요 취소 요청을 보내면 409 응답을 내려준다`() { + val result = mvc.perform( + MockMvcRequestBuilders.get("/api/v1/playlists/1") + .contentType(MediaType.APPLICATION_JSON) + .header("Authorization", "Bearer gnirps") + ) + .andExpect(MockMvcResultMatchers.status().`is`(200)) + .andExpect(MockMvcResultMatchers.content().contentType(MediaType.APPLICATION_JSON)) + .andReturn().response.contentAsString + val jsonNode = mapper.readTree(result) + assertThat(jsonNode.get("isLiked")?.asText()).isEqualTo("false") + + mvc.perform( + MockMvcRequestBuilders.delete("/api/v1/playlists/1/likes") + .contentType(MediaType.APPLICATION_JSON) + .header("Authorization", "Bearer gnirps") + ) + .andExpect(MockMvcResultMatchers.status().`is`(409)) + } + + @Test + fun `존재하지 않는 플레이리스트에 좋아요 혹은 취소 요청을 보내면 409 응답을 내려준다`() { + mvc.perform( + MockMvcRequestBuilders.post("/api/v1/playlists/12121212121212121/likes") + .contentType(MediaType.APPLICATION_JSON) + .header("Authorization", "Bearer gnirps") + ) + .andExpect(MockMvcResultMatchers.status().`is`(404)) + + mvc.perform( + MockMvcRequestBuilders.delete("/api/v1/playlists/12121212121212121/likes") + .contentType(MediaType.APPLICATION_JSON) + .header("Authorization", "Bearer gnirps") + ) + .andExpect(MockMvcResultMatchers.status().`is`(404)) + } } diff --git a/src/test/kotlin/song/SongIntegrationTest.kt b/src/test/kotlin/song/SongIntegrationTest.kt index 0abe532..32643b3 100644 --- a/src/test/kotlin/song/SongIntegrationTest.kt +++ b/src/test/kotlin/song/SongIntegrationTest.kt @@ -1,13 +1,44 @@ package com.wafflestudio.seminar.spring2023.song +import jakarta.transaction.Transactional +import org.junit.jupiter.api.Test import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc import org.springframework.boot.test.context.SpringBootTest +import org.springframework.http.MediaType import org.springframework.test.web.servlet.MockMvc +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders +import org.springframework.test.web.servlet.result.MockMvcResultMatchers @AutoConfigureMockMvc +@Transactional @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) class SongIntegrationTest @Autowired constructor( private val mvc: MockMvc, ) { + @Test + fun `제목에 키워드를 포함한 곡을 검색하면 200 응답과 곡 정보를 내려준다`() { + // Search with keyword "Don't" + val result = mvc.perform( + MockMvcRequestBuilders.get("/api/v1/songs?keyword=Happier") + .contentType(MediaType.APPLICATION_JSON) + ) + .andExpect(MockMvcResultMatchers.status().`is`(200)) + .andExpect(MockMvcResultMatchers.content().contentType(MediaType.APPLICATION_JSON)) + .andReturn().response.contentAsString + print(result); + } + + @Test + fun `제목에 키워드를 포함한 앨범을 검색하면 200 응답과 앨범 정보를 내려준다`() { + // Search with keyword "Don't" + val result = mvc.perform( + MockMvcRequestBuilders.get("/api/v1/albums?keyword=Deluxe") + .contentType(MediaType.APPLICATION_JSON) + ) + .andExpect(MockMvcResultMatchers.status().`is`(200)) + .andExpect(MockMvcResultMatchers.content().contentType(MediaType.APPLICATION_JSON)) + .andReturn().response.contentAsString + print(result); + } }