feat(home): 홈 추천 조회 컨트롤러를 추가한다
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
package kr.co.vividnext.sodalive.v2.api.home
|
||||
|
||||
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.MemberRepository
|
||||
@@ -19,10 +20,12 @@ import org.springframework.http.MediaType
|
||||
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.request.MockMvcRequestBuilders.post
|
||||
import org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath
|
||||
import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status
|
||||
import org.springframework.transaction.annotation.Transactional
|
||||
import java.time.LocalDateTime
|
||||
import javax.persistence.EntityManager
|
||||
|
||||
@SpringBootTest
|
||||
@@ -192,6 +195,193 @@ class HomeRecommendationControllerTest @Autowired constructor(
|
||||
assertEquals(0, creatorFollowingRepository.findAll().size)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("메인 홈 통합 조회는 비회원도 호출 가능하고 섹션별 빈 배열을 포함해 성공 응답한다")
|
||||
fun shouldReturnHomeRecommendationsForAnonymous() {
|
||||
mockMvc.perform(get("/api/v2/home/recommendations"))
|
||||
.andExpect(status().isOk)
|
||||
.andExpect(jsonPath("$.success").value(true))
|
||||
.andExpect(jsonPath("$.data.lives").isArray)
|
||||
.andExpect(jsonPath("$.data.banners").isArray)
|
||||
.andExpect(jsonPath("$.data.recentlyActiveCreators").isArray)
|
||||
.andExpect(jsonPath("$.data.recentDebutCreators").isArray)
|
||||
.andExpect(jsonPath("$.data.firstAudioContents").isArray)
|
||||
.andExpect(jsonPath("$.data.aiCharacters").isArray)
|
||||
.andExpect(jsonPath("$.data.genreCreators").isArray)
|
||||
.andExpect(jsonPath("$.data.cheerCreators").isArray)
|
||||
.andExpect(jsonPath("$.data.popularCommunities").isArray)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("메인 홈 통합 조회는 인증 회원도 호출 가능하고 성공 응답한다")
|
||||
fun shouldReturnHomeRecommendationsForMember() {
|
||||
val member = saveMember("home-viewer", MemberRole.USER)
|
||||
entityManager.flush()
|
||||
entityManager.clear()
|
||||
|
||||
mockMvc.perform(
|
||||
get("/api/v2/home/recommendations").with(user(MemberAdapter(member)))
|
||||
)
|
||||
.andExpect(status().isOk)
|
||||
.andExpect(jsonPath("$.success").value(true))
|
||||
.andExpect(jsonPath("$.data.lives").isArray)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("라이브 전체보기는 page/size를 포함한 페이징 응답 형식으로 반환한다")
|
||||
fun shouldReturnPagedLives() {
|
||||
val member = saveMember("paged-live-viewer", MemberRole.USER)
|
||||
entityManager.flush()
|
||||
entityManager.clear()
|
||||
|
||||
mockMvc.perform(get("/api/v2/home/recommendations/lives").with(user(MemberAdapter(member))))
|
||||
.andExpect(status().isOk)
|
||||
.andExpect(jsonPath("$.success").value(true))
|
||||
.andExpect(jsonPath("$.data.items").isArray)
|
||||
.andExpect(jsonPath("$.data.page").value(0))
|
||||
.andExpect(jsonPath("$.data.size").value(20))
|
||||
.andExpect(jsonPath("$.data.hasNext").value(false))
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("섹션별 전체보기는 size 최대값 50으로 제한한다")
|
||||
fun shouldCapPageSizeAtFifty() {
|
||||
val member = saveMember("paged-debut-viewer", MemberRole.USER)
|
||||
entityManager.flush()
|
||||
entityManager.clear()
|
||||
|
||||
mockMvc.perform(
|
||||
get("/api/v2/home/recommendations/debut-creators")
|
||||
.with(user(MemberAdapter(member)))
|
||||
.param("size", "100")
|
||||
)
|
||||
.andExpect(status().isOk)
|
||||
.andExpect(jsonPath("$.data.size").value(50))
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("첫 오디오/AI 캐릭터 전체보기도 같은 페이징 응답 형식을 사용한다")
|
||||
fun shouldReturnPagedSectionsWithSameFormat() {
|
||||
val member = saveMember("paged-section-viewer", MemberRole.USER)
|
||||
entityManager.flush()
|
||||
entityManager.clear()
|
||||
|
||||
for (path in listOf("/first-audio-contents", "/ai-characters")) {
|
||||
mockMvc.perform(
|
||||
get("/api/v2/home/recommendations$path")
|
||||
.with(user(MemberAdapter(member)))
|
||||
.param("page", "1")
|
||||
.param("size", "10")
|
||||
)
|
||||
.andExpect(status().isOk)
|
||||
.andExpect(jsonPath("$.data.items").isArray)
|
||||
.andExpect(jsonPath("$.data.page").value(1))
|
||||
.andExpect(jsonPath("$.data.size").value(10))
|
||||
.andExpect(jsonPath("$.data.hasNext").isBoolean)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("세부 전체보기 API는 비회원 요청을 거부한다")
|
||||
fun shouldRejectAnonymousSectionPages() {
|
||||
for (path in listOf("/lives", "/debut-creators", "/first-audio-contents", "/ai-characters")) {
|
||||
mockMvc.perform(get("/api/v2/home/recommendations$path"))
|
||||
.andExpect(status().isUnauthorized)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("세부 전체보기 API는 음수 page를 0으로 보정한다")
|
||||
fun shouldNormalizeNegativePageToZero() {
|
||||
val member = saveMember("negative-page-viewer", MemberRole.USER)
|
||||
entityManager.flush()
|
||||
entityManager.clear()
|
||||
|
||||
mockMvc.perform(
|
||||
get("/api/v2/home/recommendations/lives")
|
||||
.with(user(MemberAdapter(member)))
|
||||
.param("page", "-1")
|
||||
)
|
||||
.andExpect(status().isOk)
|
||||
.andExpect(jsonPath("$.data.page").value(0))
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("커뮤니티 전체보기 API는 인증 회원에게도 제공하지 않는다")
|
||||
fun shouldNotExposeCommunitiesFullViewEndpoint() {
|
||||
val member = saveMember("removed-community-viewer", MemberRole.USER)
|
||||
entityManager.flush()
|
||||
entityManager.clear()
|
||||
|
||||
mockMvc.perform(
|
||||
get("/api/v2/home/recommendations/communities")
|
||||
.with(user(MemberAdapter(member)))
|
||||
)
|
||||
.andExpect(status().isNotFound)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("라이브 전체보기 page=0에서 성인 라이브를 제외하고 최신순 첫 항목과 hasNext=true를 반환한다")
|
||||
fun shouldReturnFirstPageLivesExcludingAdult() {
|
||||
val member = saveMember("adult-hidden-live-viewer-p0", MemberRole.USER)
|
||||
val creator = saveMember("adult-hidden-live-creator-p0", MemberRole.CREATOR)
|
||||
val baseAt = LocalDateTime.of(2026, 5, 31, 10, 0)
|
||||
val newest = saveLiveRoom(creator, baseAt.plusMinutes(2), "normal-newest-p0", isAdult = false)
|
||||
saveLiveRoom(creator, baseAt.plusMinutes(1), "adult-hidden-p0", isAdult = true)
|
||||
saveLiveRoom(creator, baseAt, "normal-oldest-p0", isAdult = false)
|
||||
entityManager.flush()
|
||||
entityManager.clear()
|
||||
|
||||
mockMvc.perform(
|
||||
get("/api/v2/home/recommendations/lives")
|
||||
.with(user(MemberAdapter(member)))
|
||||
.param("page", "0")
|
||||
.param("size", "1")
|
||||
)
|
||||
.andExpect(status().isOk)
|
||||
.andExpect(jsonPath("$.data.items[0].liveRoomId").value(newest.id))
|
||||
.andExpect(jsonPath("$.data.hasNext").value(true))
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("라이브 전체보기 page=1에서 성인 라이브를 제외하고 두 번째 항목과 hasNext=false를 반환한다")
|
||||
fun shouldReturnSecondPageLivesExcludingAdult() {
|
||||
val member = saveMember("adult-hidden-live-viewer-p1", MemberRole.USER)
|
||||
val creator = saveMember("adult-hidden-live-creator-p1", MemberRole.CREATOR)
|
||||
val baseAt = LocalDateTime.of(2026, 5, 31, 10, 0)
|
||||
saveLiveRoom(creator, baseAt.plusMinutes(2), "normal-newest-p1", isAdult = false)
|
||||
saveLiveRoom(creator, baseAt.plusMinutes(1), "adult-hidden-p1", isAdult = true)
|
||||
val oldest = saveLiveRoom(creator, baseAt, "normal-oldest-p1", isAdult = false)
|
||||
entityManager.flush()
|
||||
entityManager.clear()
|
||||
|
||||
mockMvc.perform(
|
||||
get("/api/v2/home/recommendations/lives")
|
||||
.with(user(MemberAdapter(member)))
|
||||
.param("page", "1")
|
||||
.param("size", "1")
|
||||
)
|
||||
.andExpect(status().isOk)
|
||||
.andExpect(jsonPath("$.data.items[0].liveRoomId").value(oldest.id))
|
||||
.andExpect(jsonPath("$.data.hasNext").value(false))
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("세부 전체보기 API는 size=0을 기본값 20으로 보정한다")
|
||||
fun shouldNormalizeZeroSizeToDefault() {
|
||||
val member = saveMember("zero-size-viewer", MemberRole.USER)
|
||||
entityManager.flush()
|
||||
entityManager.clear()
|
||||
|
||||
mockMvc.perform(
|
||||
get("/api/v2/home/recommendations/lives")
|
||||
.with(user(MemberAdapter(member)))
|
||||
.param("size", "0")
|
||||
)
|
||||
.andExpect(status().isOk)
|
||||
.andExpect(jsonPath("$.data.size").value(20))
|
||||
}
|
||||
|
||||
private fun saveMember(seed: String, role: MemberRole): Member {
|
||||
return memberRepository.saveAndFlush(
|
||||
Member(
|
||||
@@ -211,4 +401,23 @@ class HomeRecommendationControllerTest @Autowired constructor(
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private fun saveLiveRoom(
|
||||
creator: Member,
|
||||
beginDateTime: LocalDateTime,
|
||||
channelName: String,
|
||||
isAdult: Boolean
|
||||
): LiveRoom {
|
||||
val room = LiveRoom(
|
||||
title = "live-${creator.nickname}-$channelName",
|
||||
notice = "notice",
|
||||
beginDateTime = beginDateTime,
|
||||
numberOfPeople = 0,
|
||||
isAdult = isAdult
|
||||
)
|
||||
room.member = creator
|
||||
room.channelName = channelName
|
||||
entityManager.persist(room)
|
||||
return room
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user