From e525f9de64b7fbd2785ef70f4505b4b380201211 Mon Sep 17 00:00:00 2001 From: Klaus Date: Wed, 17 Jun 2026 21:43:06 +0900 Subject: [PATCH] =?UTF-8?q?test(creator):=20=EC=B1=84=EB=84=90=20=EB=9D=BC?= =?UTF-8?q?=EC=9D=B4=EB=B8=8C=20=ED=86=B5=ED=95=A9=20=EC=9D=91=EB=8B=B5=20?= =?UTF-8?q?=EA=B2=80=EC=A6=9D=EC=9D=84=20=EB=B3=B4=EA=B0=95=ED=95=9C?= =?UTF-8?q?=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../web/CreatorChannelLiveControllerTest.kt | 81 ++++++-- .../in/web/CreatorChannelLiveEndToEndTest.kt | 191 ++++++++++++++++++ 2 files changed, 253 insertions(+), 19 deletions(-) create mode 100644 src/test/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/live/adapter/in/web/CreatorChannelLiveEndToEndTest.kt diff --git a/src/test/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/live/adapter/in/web/CreatorChannelLiveControllerTest.kt b/src/test/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/live/adapter/in/web/CreatorChannelLiveControllerTest.kt index a37a38f8..1a7c82b3 100644 --- a/src/test/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/live/adapter/in/web/CreatorChannelLiveControllerTest.kt +++ b/src/test/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/live/adapter/in/web/CreatorChannelLiveControllerTest.kt @@ -116,6 +116,41 @@ class CreatorChannelLiveControllerTest @Autowired constructor( ) } + @Test + @DisplayName("크리에이터 채널 라이브 탭 조회는 다음 페이지가 있는 대표 응답 표면을 반환한다") + fun shouldReturnLiveTabSurfaceWhenNextPageExists() { + val viewer = createMember(id = 10L) + Mockito.doReturn(createResponse(liveReplayContentCount = 21, contentCount = 20, hasNext = true)) + .`when`(facade).getLiveTab( + eqValue(1L), + eqValue(viewer), + eqValue(ContentSort.LATEST), + eqValue(0), + eqValue(20), + anyValue(LocalDateTime.now()) + ) + + mockMvc.perform( + get("/api/v2/creator-channels/1/live") + .with(user(MemberAdapter(viewer))) + ) + .andExpect(status().isOk) + .andExpect(jsonPath("$.success").value(true)) + .andExpect(jsonPath("$.data.liveReplayContentCount").value(21)) + .andExpect(jsonPath("$.data.currentLive.liveId").value(101L)) + .andExpect(jsonPath("$.data.liveReplayContents.length()").value(20)) + .andExpect(jsonPath("$.data.sort").value("LATEST")) + .andExpect(jsonPath("$.data.page").value(0)) + .andExpect(jsonPath("$.data.size").value(20)) + .andExpect(jsonPath("$.data.hasNext").value(true)) + .andExpect(jsonPath("$.data.liveReplayContents[0].isOwned").value(true)) + .andExpect(jsonPath("$.data.liveReplayContents[0].isRented").value(false)) + .andExpect(jsonPath("$.data.liveReplayContents[1].isOwned").value(false)) + .andExpect(jsonPath("$.data.liveReplayContents[1].isRented").value(true)) + .andExpect(jsonPath("$.data.liveReplayContents[2].isOwned").value(false)) + .andExpect(jsonPath("$.data.liveReplayContents[2].isRented").value(false)) + } + @Test @DisplayName("크리에이터 채널 라이브 탭 조회는 잘못된 page 요청을 기존 오류 응답으로 반환한다") fun shouldReturnErrorResponseWhenPageIsInvalid() { @@ -181,9 +216,13 @@ class CreatorChannelLiveControllerTest @Autowired constructor( } } - private fun createResponse(): CreatorChannelLiveTabResponse { + private fun createResponse( + liveReplayContentCount: Int = 1, + contentCount: Int = 1, + hasNext: Boolean = false + ): CreatorChannelLiveTabResponse { return CreatorChannelLiveTabResponse( - liveReplayContentCount = 1, + liveReplayContentCount = liveReplayContentCount, currentLive = CreatorChannelLiveResponse( liveId = 101L, title = "live", @@ -192,26 +231,30 @@ class CreatorChannelLiveControllerTest @Autowired constructor( price = 20, isAdult = true ), - liveReplayContents = listOf( - CreatorChannelAudioContentResponse( - audioContentId = 201L, - title = "audio", - duration = "00:10:00", - imageUrl = "audio.png", - price = 30, - isAdult = false, - isPointAvailable = true, - isFirstContent = true, - seriesName = "series", - isOriginalSeries = true, - isOwned = true, - isRented = false - ) - ), + liveReplayContents = createAudioContents(contentCount), sort = ContentSort.LATEST, page = 0, size = 20, - hasNext = false + hasNext = hasNext ) } + + private fun createAudioContents(count: Int): List { + return (1..count).map { index -> + CreatorChannelAudioContentResponse( + audioContentId = 200L + index, + title = "audio-$index", + duration = "00:10:00", + imageUrl = "audio-$index.png", + price = 30, + isAdult = false, + isPointAvailable = true, + isFirstContent = index == 1, + seriesName = "series", + isOriginalSeries = true, + isOwned = index == 1, + isRented = index == 2 + ) + } + } } diff --git a/src/test/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/live/adapter/in/web/CreatorChannelLiveEndToEndTest.kt b/src/test/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/live/adapter/in/web/CreatorChannelLiveEndToEndTest.kt new file mode 100644 index 00000000..6604ef93 --- /dev/null +++ b/src/test/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/live/adapter/in/web/CreatorChannelLiveEndToEndTest.kt @@ -0,0 +1,191 @@ +package kr.co.vividnext.sodalive.v2.api.creator.channel.live.adapter.`in`.web + +import kr.co.vividnext.sodalive.content.AudioContent +import kr.co.vividnext.sodalive.content.order.Order +import kr.co.vividnext.sodalive.content.order.OrderType +import kr.co.vividnext.sodalive.content.theme.AudioContentTheme +import kr.co.vividnext.sodalive.live.room.GenderRestriction +import kr.co.vividnext.sodalive.live.room.LiveRoom +import kr.co.vividnext.sodalive.member.Member +import kr.co.vividnext.sodalive.member.MemberAdapter +import kr.co.vividnext.sodalive.member.MemberRole +import kr.co.vividnext.sodalive.support.EmbeddedRedisInitializer +import org.junit.jupiter.api.DisplayName +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.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user +import org.springframework.test.context.ContextConfiguration +import org.springframework.test.web.servlet.MockMvc +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get +import org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath +import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status +import org.springframework.transaction.support.TransactionTemplate +import java.time.LocalDateTime +import javax.persistence.EntityManager + +@SpringBootTest( + properties = [ + "cloud.aws.cloud-front.host=https://cdn.test", + "spring.cache.type=none", + "spring.datasource.url=jdbc:h2:mem:creator-channel-live-e2e;MODE=MySQL;NON_KEYWORDS=VALUE;DB_CLOSE_ON_EXIT=FALSE" + ] +) +@AutoConfigureMockMvc +@ContextConfiguration(initializers = [EmbeddedRedisInitializer::class]) +class CreatorChannelLiveEndToEndTest @Autowired constructor( + private val mockMvc: MockMvc, + private val entityManager: EntityManager, + private val transactionTemplate: TransactionTemplate +) { + @Test + @DisplayName("라이브 탭 API는 controller-service-repository를 거쳐 대표 응답을 반환한다") + fun shouldReturnLiveTabThroughControllerServiceAndRepository() { + val fixture = createFixture() + + mockMvc.perform( + get("/api/v2/creator-channels/${fixture.creatorId}/live") + .param("sort", "LATEST") + .param("page", "0") + .param("size", "20") + .with(user(MemberAdapter(fixture.viewer))) + ) + .andExpect(status().isOk) + .andExpect(jsonPath("$.success").value(true)) + .andExpect(jsonPath("$.data.liveReplayContentCount").value(21)) + .andExpect(jsonPath("$.data.currentLive.liveId").value(fixture.currentLiveId)) + .andExpect(jsonPath("$.data.currentLive.title").value("e2e-live")) + .andExpect(jsonPath("$.data.currentLive.coverImageUrl").value("https://cdn.test/live-cover.png")) + .andExpect(jsonPath("$.data.liveReplayContents.length()").value(20)) + .andExpect(jsonPath("$.data.liveReplayContents[0].audioContentId").value(fixture.keepContentId)) + .andExpect(jsonPath("$.data.liveReplayContents[0].imageUrl").value("https://cdn.test/audio-1.png")) + .andExpect(jsonPath("$.data.liveReplayContents[0].isOwned").value(true)) + .andExpect(jsonPath("$.data.liveReplayContents[0].isRented").value(false)) + .andExpect(jsonPath("$.data.liveReplayContents[1].audioContentId").value(fixture.rentalContentId)) + .andExpect(jsonPath("$.data.liveReplayContents[1].isOwned").value(false)) + .andExpect(jsonPath("$.data.liveReplayContents[1].isRented").value(true)) + .andExpect(jsonPath("$.data.liveReplayContents[2].audioContentId").value(fixture.unorderedContentId)) + .andExpect(jsonPath("$.data.liveReplayContents[2].isOwned").value(false)) + .andExpect(jsonPath("$.data.liveReplayContents[2].isRented").value(false)) + .andExpect(jsonPath("$.data.sort").value("LATEST")) + .andExpect(jsonPath("$.data.page").value(0)) + .andExpect(jsonPath("$.data.size").value(20)) + .andExpect(jsonPath("$.data.hasNext").value(true)) + } + + private fun createFixture(): Fixture { + return transactionTemplate.execute { + val now = LocalDateTime.now() + val viewer = saveMember("live-e2e-viewer", MemberRole.USER) + val creator = saveMember("live-e2e-creator", MemberRole.CREATOR) + val currentLive = saveLiveRoom(creator, now.minusHours(1)) + val liveReplayTheme = saveTheme("다시듣기") + val contents = (1..21).map { index -> + saveAudioContent( + creator = creator, + releaseDate = now.minusMinutes(index.toLong()), + theme = liveReplayTheme, + coverImage = "audio-$index.png" + ) + } + saveOrder(viewer, creator, contents[0], OrderType.KEEP) + saveOrder(viewer, creator, contents[1], OrderType.RENTAL, endDate = now.plusDays(1)) + entityManager.flush() + + Fixture( + viewer = viewer, + creatorId = creator.id!!, + currentLiveId = currentLive.id!!, + keepContentId = contents[0].id!!, + rentalContentId = contents[1].id!!, + unorderedContentId = contents[2].id!! + ) + }!! + } + + private fun saveMember(nickname: String, role: MemberRole): Member { + val member = Member( + email = "$nickname@test.com", + password = "password", + nickname = nickname, + role = role + ) + entityManager.persist(member) + return member + } + + private fun saveLiveRoom(creator: Member, beginDateTime: LocalDateTime): LiveRoom { + val liveRoom = LiveRoom( + title = "e2e-live", + notice = "notice", + beginDateTime = beginDateTime, + numberOfPeople = 0, + coverImage = "live-cover.png", + isAdult = false, + price = 50, + isAvailableJoinCreator = true, + genderRestriction = GenderRestriction.ALL + ) + liveRoom.member = creator + liveRoom.channelName = "e2e-live-channel" + liveRoom.isActive = true + entityManager.persist(liveRoom) + return liveRoom + } + + private fun saveAudioContent( + creator: Member, + releaseDate: LocalDateTime, + theme: AudioContentTheme, + coverImage: String + ): AudioContent { + val content = AudioContent( + title = "audio-$coverImage", + detail = "detail", + languageCode = "ko", + releaseDate = releaseDate, + isAdult = false, + price = 100, + isPointAvailable = true + ) + content.member = creator + content.theme = theme + content.isActive = true + content.coverImage = coverImage + content.duration = "00:10:00" + entityManager.persist(content) + return content + } + + private fun saveTheme(name: String): AudioContentTheme { + val theme = AudioContentTheme(theme = name, image = "$name.png", isActive = true) + entityManager.persist(theme) + return theme + } + + private fun saveOrder( + member: Member, + creator: Member, + content: AudioContent, + type: OrderType, + endDate: LocalDateTime? = null + ): Order { + val order = Order(type = type, isActive = true) + order.member = member + order.creator = creator + order.audioContent = content + entityManager.persist(order) + endDate?.let { order.endDate = it } + return order + } + + private data class Fixture( + val viewer: Member, + val creatorId: Long, + val currentLiveId: Long, + val keepContentId: Long, + val rentalContentId: Long, + val unorderedContentId: Long + ) +}