diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/configs/SecurityConfig.kt b/src/main/kotlin/kr/co/vividnext/sodalive/configs/SecurityConfig.kt index aca49161..d2481237 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/configs/SecurityConfig.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/configs/SecurityConfig.kt @@ -103,6 +103,7 @@ class SecurityConfig( .antMatchers(HttpMethod.POST, "/charge/payverse/webhook").permitAll() .antMatchers(HttpMethod.GET, "/api/v2/home/recommendations").permitAll() .antMatchers(HttpMethod.GET, "/api/v2/audio/recommendations").permitAll() + .antMatchers(HttpMethod.GET, "/api/v2/audio/rankings").permitAll() .antMatchers(HttpMethod.GET, "/api/v2/home/rankings/creators").permitAll() // 페이지네이션 하위 경로(/lives, /debut-creators 등)는 인증 필수 .antMatchers(HttpMethod.GET, "/api/v2/home/recommendations/**").authenticated() diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/v2/api/content/ranking/adapter/in/web/AudioRankingController.kt b/src/main/kotlin/kr/co/vividnext/sodalive/v2/api/content/ranking/adapter/in/web/AudioRankingController.kt new file mode 100644 index 00000000..36d781be --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/v2/api/content/ranking/adapter/in/web/AudioRankingController.kt @@ -0,0 +1,25 @@ +package kr.co.vividnext.sodalive.v2.api.content.ranking.adapter.`in`.web + +import kr.co.vividnext.sodalive.common.ApiResponse +import kr.co.vividnext.sodalive.member.Member +import kr.co.vividnext.sodalive.v2.api.content.ranking.application.AudioRankingFacade +import kr.co.vividnext.sodalive.v2.content.ranking.domain.AudioRankingType +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/audio/rankings") +class AudioRankingController( + private val facade: AudioRankingFacade +) { + @GetMapping + fun getRankings( + @RequestParam(defaultValue = "WEEKLY_POPULAR") type: AudioRankingType, + @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? + ) = run { + ApiResponse.ok(facade.getRankings(type, member)) + } +} diff --git a/src/test/kotlin/kr/co/vividnext/sodalive/v2/api/content/ranking/adapter/in/web/AudioRankingControllerTest.kt b/src/test/kotlin/kr/co/vividnext/sodalive/v2/api/content/ranking/adapter/in/web/AudioRankingControllerTest.kt new file mode 100644 index 00000000..c22def8e --- /dev/null +++ b/src/test/kotlin/kr/co/vividnext/sodalive/v2/api/content/ranking/adapter/in/web/AudioRankingControllerTest.kt @@ -0,0 +1,114 @@ +package kr.co.vividnext.sodalive.v2.api.content.ranking.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.ranking.application.AudioRankingFacade +import kr.co.vividnext.sodalive.v2.api.content.ranking.dto.AudioRankingItemResponse +import kr.co.vividnext.sodalive.v2.api.content.ranking.dto.AudioRankingResponse +import kr.co.vividnext.sodalive.v2.content.ranking.domain.AudioRankingType +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.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(AudioRankingController::class) +@Import(SecurityConfig::class) +class AudioRankingControllerTest @Autowired constructor( + private val mockMvc: MockMvc +) { + @MockBean + private lateinit var facade: AudioRankingFacade + + @MockBean + private lateinit var countryContext: CountryContext + + @MockBean + private lateinit var langContext: LangContext + + @MockBean + private lateinit var sodaMessageSource: SodaMessageSource + + @MockBean + private lateinit var tokenProvider: TokenProvider + + @MockBean + private lateinit var accessDeniedHandler: JwtAccessDeniedHandler + + @MockBean + private lateinit var authenticationEntryPoint: JwtAuthenticationEntryPoint + + @Test + @DisplayName("오디오 랭킹 조회는 비회원에게 200 OK와 기본 WEEKLY_POPULAR 랭킹을 반환한다") + fun shouldReturnWeeklyPopularRankingsForAnonymousByDefault() { + Mockito.doReturn(rankingResponse(AudioRankingType.WEEKLY_POPULAR)) + .`when`(facade).getRankings(AudioRankingType.WEEKLY_POPULAR, null) + + mockMvc.perform(get("/api/v2/audio/rankings")) + .andExpect(status().isOk) + .andExpect(jsonPath("$.success").value(true)) + .andExpect(jsonPath("$.data.type").value("WEEKLY_POPULAR")) + .andExpect(jsonPath("$.data.items").isArray) + .andExpect(jsonPath("$.data.items[0].contentId").value(1L)) + + Mockito.verify(facade).getRankings(AudioRankingType.WEEKLY_POPULAR, null) + } + + @Test + @DisplayName("오디오 랭킹 조회는 인증 회원과 요청 type을 facade에 전달한다") + fun shouldPassAuthenticatedMemberAndRequestedTypeToFacade() { + val member = Member( + email = "viewer@test.com", + password = "password", + nickname = "viewer", + role = MemberRole.USER + ).apply { id = 10L } + Mockito.doReturn(rankingResponse(AudioRankingType.RISING)) + .`when`(facade).getRankings(eqValue(AudioRankingType.RISING), eqValue(member)) + + mockMvc.perform(get("/api/v2/audio/rankings").param("type", "RISING").with(user(MemberAdapter(member)))) + .andExpect(status().isOk) + .andExpect(jsonPath("$.success").value(true)) + .andExpect(jsonPath("$.data.type").value("RISING")) + .andExpect(jsonPath("$.data.items").isArray) + + Mockito.verify(facade).getRankings(eqValue(AudioRankingType.RISING), eqValue(member)) + } + + private fun rankingResponse(type: AudioRankingType): AudioRankingResponse { + return AudioRankingResponse( + showRankChange = true, + type = type, + items = listOf( + AudioRankingItemResponse( + contentId = 1L, + title = "ranking audio", + creatorNickname = "creator", + rank = 1, + rankChange = 2, + isNew = false, + coverImageUrl = "https://example.com/cover.jpg" + ) + ) + ) + } + + private fun eqValue(value: T): T { + return Mockito.eq(value) ?: value + } +}