diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/v2/api/content/overview/adapter/in/web/ContentOverviewController.kt b/src/main/kotlin/kr/co/vividnext/sodalive/v2/api/content/overview/adapter/in/web/ContentOverviewController.kt new file mode 100644 index 00000000..81b45124 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/v2/api/content/overview/adapter/in/web/ContentOverviewController.kt @@ -0,0 +1,31 @@ +package kr.co.vividnext.sodalive.v2.api.content.overview.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.content.overview.application.ContentOverviewFacade +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/contents") +class ContentOverviewController( + private val facade: ContentOverviewFacade +) { + @GetMapping + fun getContents( + @RequestParam(required = false) type: String?, + @RequestParam(required = false) page: Int?, + @RequestParam(required = false) size: Int?, + @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? + ) = run { + ApiResponse.ok(facade.getContents(type, page, size, requireMember(member))) + } + + 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/content/overview/adapter/in/web/ContentOverviewControllerTest.kt b/src/test/kotlin/kr/co/vividnext/sodalive/v2/api/content/overview/adapter/in/web/ContentOverviewControllerTest.kt new file mode 100644 index 00000000..8babe2a8 --- /dev/null +++ b/src/test/kotlin/kr/co/vividnext/sodalive/v2/api/content/overview/adapter/in/web/ContentOverviewControllerTest.kt @@ -0,0 +1,105 @@ +package kr.co.vividnext.sodalive.v2.api.content.overview.adapter.`in`.web + +import kr.co.vividnext.sodalive.common.CountryContext +import kr.co.vividnext.sodalive.configs.SecurityConfig +import kr.co.vividnext.sodalive.i18n.LangContext +import kr.co.vividnext.sodalive.i18n.SodaMessageSource +import kr.co.vividnext.sodalive.jwt.JwtAccessDeniedHandler +import kr.co.vividnext.sodalive.jwt.JwtAuthenticationEntryPoint +import kr.co.vividnext.sodalive.jwt.TokenProvider +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.content.overview.application.ContentOverviewFacade +import kr.co.vividnext.sodalive.v2.api.content.overview.dto.ContentOverviewPageResponse +import kr.co.vividnext.sodalive.v2.api.content.overview.dto.ContentOverviewType +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.mock.mockito.MockBean +import org.springframework.context.annotation.Import +import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.anonymous +import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user +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 + +@WebMvcTest(ContentOverviewController::class) +@Import(SecurityConfig::class, JwtAuthenticationEntryPoint::class, JwtAccessDeniedHandler::class) +class ContentOverviewControllerTest @Autowired constructor( + private val mockMvc: MockMvc +) { + @MockBean + private lateinit var facade: ContentOverviewFacade + + @MockBean + private lateinit var countryContext: CountryContext + + @MockBean + private lateinit var langContext: LangContext + + @MockBean + private lateinit var sodaMessageSource: SodaMessageSource + + @MockBean + private lateinit var tokenProvider: TokenProvider + + @Test + @DisplayName("콘텐츠 전체보기는 비회원 요청을 거부한다") + fun shouldRejectAnonymousRequest() { + mockMvc.perform( + get("/api/v2/contents") + .with(anonymous()) + ) + .andExpect(status().isUnauthorized) + } + + @Test + @DisplayName("콘텐츠 전체보기는 인증 회원과 query parameter를 facade에 전달한다") + fun shouldPassAuthenticatedMemberAndQueryParameters() { + val member = member(id = 10L) + Mockito.doReturn(emptyResponse(ContentOverviewType.FIRST_AUDIO_CONTENT)).`when`(facade) + .getContents(eqValue("FIRST_AUDIO_CONTENT"), eqValue(1), eqValue(30), eqValue(member)) + + mockMvc.perform( + get("/api/v2/contents") + .param("type", "FIRST_AUDIO_CONTENT") + .param("page", "1") + .param("size", "30") + .with(user(MemberAdapter(member))) + ) + .andExpect(status().isOk) + .andExpect(jsonPath("$.success").value(true)) + .andExpect(jsonPath("$.data.type").value("FIRST_AUDIO_CONTENT")) + + Mockito.verify(facade).getContents(eqValue("FIRST_AUDIO_CONTENT"), eqValue(1), eqValue(30), eqValue(member)) + } + + private fun eqValue(value: T): T { + return Mockito.eq(value) ?: value + } + + private fun member(id: Long): Member { + return Member( + email = "viewer$id@test.com", + password = "password", + nickname = "viewer$id", + role = MemberRole.USER + ).apply { + this.id = id + } + } + + private fun emptyResponse(type: ContentOverviewType): ContentOverviewPageResponse { + return ContentOverviewPageResponse( + type = type, + items = emptyList(), + page = 0, + size = 20, + hasNext = false + ) + } +}