From 5f09f59f53b433e9622a8009ddfe9211eaa78720 Mon Sep 17 00:00:00 2001 From: Klaus Date: Sat, 27 Jun 2026 00:07:15 +0900 Subject: [PATCH] =?UTF-8?q?feat(home-live):=20=ED=98=84=EC=9E=AC=20?= =?UTF-8?q?=EC=A7=84=ED=96=89=20=EC=A4=91=20=EB=9D=BC=EC=9D=B4=EB=B8=8C=20?= =?UTF-8?q?endpoint=EB=A5=BC=20=EC=B6=94=EA=B0=80=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../adapter/in/web/HomeOnAirLiveController.kt | 29 ++++ .../in/web/HomeOnAirLiveControllerTest.kt | 124 ++++++++++++++++++ 2 files changed, 153 insertions(+) create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/v2/api/home/live/adapter/in/web/HomeOnAirLiveController.kt create mode 100644 src/test/kotlin/kr/co/vividnext/sodalive/v2/api/home/live/adapter/in/web/HomeOnAirLiveControllerTest.kt diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/v2/api/home/live/adapter/in/web/HomeOnAirLiveController.kt b/src/main/kotlin/kr/co/vividnext/sodalive/v2/api/home/live/adapter/in/web/HomeOnAirLiveController.kt new file mode 100644 index 00000000..1eb7bf57 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/v2/api/home/live/adapter/in/web/HomeOnAirLiveController.kt @@ -0,0 +1,29 @@ +package kr.co.vividnext.sodalive.v2.api.home.live.adapter.`in`.web + +import kr.co.vividnext.sodalive.common.ApiResponse +import kr.co.vividnext.sodalive.common.SodaException +import kr.co.vividnext.sodalive.member.Member +import kr.co.vividnext.sodalive.v2.api.home.live.application.HomeOnAirLiveFacade +import org.springframework.security.core.annotation.AuthenticationPrincipal +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 + +@RestController +@RequestMapping("/api/v2/home/on-air-lives") +class HomeOnAirLiveController( + private val homeOnAirLiveFacade: HomeOnAirLiveFacade +) { + @GetMapping + fun getOnAirLives( + @RequestParam(defaultValue = "0") page: Int, + @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? + ) = run { + ApiResponse.ok(homeOnAirLiveFacade.getOnAirLives(requireMember(member), page)) + } + + private fun requireMember(member: Member?): Member { + return member ?: throw SodaException(messageKey = "common.error.bad_credentials") + } +} diff --git a/src/test/kotlin/kr/co/vividnext/sodalive/v2/api/home/live/adapter/in/web/HomeOnAirLiveControllerTest.kt b/src/test/kotlin/kr/co/vividnext/sodalive/v2/api/home/live/adapter/in/web/HomeOnAirLiveControllerTest.kt new file mode 100644 index 00000000..44bfcec9 --- /dev/null +++ b/src/test/kotlin/kr/co/vividnext/sodalive/v2/api/home/live/adapter/in/web/HomeOnAirLiveControllerTest.kt @@ -0,0 +1,124 @@ +package kr.co.vividnext.sodalive.v2.api.home.live.adapter.`in`.web + +import kr.co.vividnext.sodalive.common.CountryContext +import kr.co.vividnext.sodalive.i18n.LangContext +import kr.co.vividnext.sodalive.i18n.SodaMessageSource +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.v2.api.home.live.application.HomeOnAirLiveFacade +import kr.co.vividnext.sodalive.v2.api.home.live.dto.HomeOnAirLivePageResponse +import kr.co.vividnext.sodalive.v2.api.home.live.dto.HomeOnAirLiveResponse +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test +import org.mockito.Mockito +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest +import org.springframework.boot.test.context.TestConfiguration +import org.springframework.boot.test.mock.mockito.MockBean +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Import +import org.springframework.http.HttpStatus +import org.springframework.security.config.annotation.web.builders.HttpSecurity +import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.anonymous +import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user +import org.springframework.security.web.SecurityFilterChain +import org.springframework.security.web.authentication.HttpStatusEntryPoint +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 javax.servlet.http.HttpServletResponse + +@WebMvcTest(HomeOnAirLiveController::class) +@Import(HomeOnAirLiveControllerTest.TestSecurityConfig::class) +class HomeOnAirLiveControllerTest @Autowired constructor( + private val mockMvc: MockMvc +) { + @MockBean + private lateinit var facade: HomeOnAirLiveFacade + + @MockBean + private lateinit var countryContext: CountryContext + + @MockBean + private lateinit var langContext: LangContext + + @MockBean + private lateinit var sodaMessageSource: SodaMessageSource + + @TestConfiguration + class TestSecurityConfig { + @Bean + fun securityFilterChain(http: HttpSecurity): SecurityFilterChain { + return http + .csrf().disable() + .authorizeRequests() + .anyRequest().authenticated() + .and() + .exceptionHandling() + .authenticationEntryPoint(HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED)) + .accessDeniedHandler { _, response, _ -> response.sendError(HttpServletResponse.SC_FORBIDDEN) } + .and() + .build() + } + } + + @Test + @DisplayName("현재 진행 중인 라이브 조회는 비회원 요청을 거부한다") + fun shouldRejectAnonymousRequest() { + mockMvc.perform( + get("/api/v2/home/on-air-lives") + .with(anonymous()) + ) + .andExpect(status().isUnauthorized) + } + + @Test + @DisplayName("현재 진행 중인 라이브 조회는 인증 회원과 page를 facade에 전달하고 성공 응답을 반환한다") + fun shouldPassAuthenticatedMemberAndPageToFacade() { + val member = createMember(100L) + Mockito.doReturn(createResponse()).`when`(facade).getOnAirLives(eqValue(member), eqValue(2)) + + mockMvc.perform( + get("/api/v2/home/on-air-lives") + .param("page", "2") + .with(user(MemberAdapter(member))) + ) + .andExpect(status().isOk) + .andExpect(jsonPath("$.success").value(true)) + .andExpect(jsonPath("$.data.size").value(20)) + .andExpect(jsonPath("$.data.items[0].title").value("paid live")) + + Mockito.verify(facade).getOnAirLives(eqValue(member), eqValue(2)) + } + + private fun createResponse() = HomeOnAirLivePageResponse( + items = listOf( + HomeOnAirLiveResponse( + roomId = 1L, + creatorNickname = "creator", + creatorProfileImage = "https://cdn.test/profile.png", + title = "paid live", + price = 30, + beginDateTimeUtc = "2026-06-26T12:30:00Z" + ) + ), + page = 2, + size = 20, + hasNext = false + ) + + private fun createMember(id: Long): Member { + return Member( + email = "viewer$id@test.com", + password = "password", + nickname = "viewer$id", + role = MemberRole.USER + ).apply { this.id = id } + } + + private fun eqValue(value: T): T { + return Mockito.eq(value) ?: value + } +}