feat(creator): 채널 홈 ViewModel을 추가한다
This commit is contained in:
@@ -0,0 +1,248 @@
|
||||
package kr.co.vividnext.sodalive.v2.creator.channel
|
||||
|
||||
import android.app.Application
|
||||
import android.content.Context
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.Observer
|
||||
import androidx.test.core.app.ApplicationProvider
|
||||
import io.reactivex.rxjava3.android.plugins.RxAndroidPlugins
|
||||
import io.reactivex.rxjava3.core.Scheduler
|
||||
import io.reactivex.rxjava3.core.Single
|
||||
import io.reactivex.rxjava3.plugins.RxJavaPlugins
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import io.reactivex.rxjava3.subjects.SingleSubject
|
||||
import kr.co.vividnext.sodalive.R
|
||||
import kr.co.vividnext.sodalive.chat.talk.room.CreateChatRoomResponse
|
||||
import kr.co.vividnext.sodalive.common.ApiResponse
|
||||
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
|
||||
import kr.co.vividnext.sodalive.v2.common.CreatorActivityType
|
||||
import kr.co.vividnext.sodalive.v2.creator.channel.data.CreatorChannelActivityResponse
|
||||
import kr.co.vividnext.sodalive.v2.creator.channel.data.CreatorChannelAudioContentResponse
|
||||
import kr.co.vividnext.sodalive.v2.creator.channel.data.CreatorChannelCreatorResponse
|
||||
import kr.co.vividnext.sodalive.v2.creator.channel.data.CreatorChannelFanTalkSummaryResponse
|
||||
import kr.co.vividnext.sodalive.v2.creator.channel.data.CreatorChannelHomeRepository
|
||||
import kr.co.vividnext.sodalive.v2.creator.channel.data.CreatorChannelHomeResponse
|
||||
import kr.co.vividnext.sodalive.v2.creator.channel.data.CreatorChannelLiveResponse
|
||||
import kr.co.vividnext.sodalive.v2.creator.channel.data.CreatorChannelScheduleResponse
|
||||
import kr.co.vividnext.sodalive.v2.creator.channel.data.CreatorChannelSnsResponse
|
||||
import kr.co.vividnext.sodalive.v2.creator.channel.model.CreatorChannelHomeUiState
|
||||
import org.junit.After
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.mockito.kotlin.any
|
||||
import org.mockito.kotlin.eq
|
||||
import org.mockito.kotlin.mock
|
||||
import org.mockito.kotlin.never
|
||||
import org.mockito.kotlin.times
|
||||
import org.mockito.kotlin.verify
|
||||
import org.mockito.kotlin.whenever
|
||||
import org.robolectric.RobolectricTestRunner
|
||||
import org.robolectric.annotation.Config
|
||||
|
||||
@RunWith(RobolectricTestRunner::class)
|
||||
@Config(sdk = [28], application = Application::class)
|
||||
class CreatorChannelHomeViewModelTest {
|
||||
|
||||
private val context: Context = ApplicationProvider.getApplicationContext()
|
||||
private lateinit var repository: CreatorChannelHomeRepository
|
||||
private lateinit var viewModel: CreatorChannelHomeViewModel
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
setImmediateRxSchedulers()
|
||||
SharedPreferenceManager.resetForTest()
|
||||
SharedPreferenceManager.init(context)
|
||||
SharedPreferenceManager.token = "test-token"
|
||||
repository = mock()
|
||||
viewModel = CreatorChannelHomeViewModel(repository)
|
||||
}
|
||||
|
||||
@After
|
||||
fun tearDown() {
|
||||
RxJavaPlugins.reset()
|
||||
RxAndroidPlugins.reset()
|
||||
SharedPreferenceManager.resetForTest()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `creatorId가 0 이하이면 홈 API를 호출하지 않는다`() {
|
||||
viewModel.loadHome(0L)
|
||||
|
||||
verify(repository, never()).getHome(any(), any())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `유효한 creatorId 로드는 bearer token으로 홈 API를 호출하고 Content를 emit한다`() {
|
||||
whenever(repository.getHome(100L, "Bearer test-token")).thenReturn(Single.just(ApiResponse(true, response(), null)))
|
||||
|
||||
viewModel.loadHome(100L)
|
||||
|
||||
val state = viewModel.homeStateLiveData.requireValue() as CreatorChannelHomeUiState.Content
|
||||
assertEquals(100L, state.header.creatorId)
|
||||
verify(repository).getHome(100L, "Bearer test-token")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `홈 API failure는 Error와 unknown toast를 emit한다`() {
|
||||
whenever(repository.getHome(100L, "Bearer test-token")).thenReturn(Single.just(ApiResponse(false, null, "failed")))
|
||||
|
||||
viewModel.loadHome(100L)
|
||||
|
||||
assertTrue(viewModel.homeStateLiveData.requireValue() is CreatorChannelHomeUiState.Error)
|
||||
assertEquals(R.string.common_error_unknown, viewModel.toastLiveData.requireValue()?.resId)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `홈 API null data는 Error와 unknown toast를 emit한다`() {
|
||||
whenever(repository.getHome(100L, "Bearer test-token")).thenReturn(Single.just(ApiResponse(true, null, null)))
|
||||
|
||||
viewModel.loadHome(100L)
|
||||
|
||||
assertTrue(viewModel.homeStateLiveData.requireValue() is CreatorChannelHomeUiState.Error)
|
||||
assertEquals(R.string.common_error_unknown, viewModel.toastLiveData.requireValue()?.resId)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `홈 API throwable은 Error와 unknown toast를 emit한다`() {
|
||||
whenever(repository.getHome(100L, "Bearer test-token")).thenReturn(Single.error(IllegalStateException("network")))
|
||||
|
||||
viewModel.loadHome(100L)
|
||||
|
||||
val state = viewModel.homeStateLiveData.requireValue() as CreatorChannelHomeUiState.Error
|
||||
assertEquals("network", state.message)
|
||||
assertEquals(R.string.common_error_unknown, viewModel.toastLiveData.requireValue()?.resId)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `팔로우 성공은 현재 content header의 follow와 notify를 갱신한다`() {
|
||||
whenever(repository.getHome(100L, "Bearer test-token")).thenReturn(
|
||||
Single.just(ApiResponse(true, response(isFollow = false), null))
|
||||
)
|
||||
whenever(repository.followCreator(100L, true, true, "Bearer test-token")).thenReturn(
|
||||
Single.just(ApiResponse(true, Any(), null))
|
||||
)
|
||||
viewModel.loadHome(100L)
|
||||
|
||||
viewModel.follow(follow = true, notify = true)
|
||||
|
||||
val state = viewModel.homeStateLiveData.requireValue() as CreatorChannelHomeUiState.Content
|
||||
assertTrue(state.header.isFollow)
|
||||
assertTrue(state.header.isNotify)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `팔로우 진행 중 중복 요청은 무시한다`() {
|
||||
val pending = SingleSubject.create<ApiResponse<Any>>()
|
||||
whenever(repository.getHome(100L, "Bearer test-token")).thenReturn(Single.just(ApiResponse(true, response(), null)))
|
||||
whenever(repository.followCreator(100L, false, false, "Bearer test-token")).thenReturn(pending)
|
||||
viewModel.loadHome(100L)
|
||||
|
||||
viewModel.follow(follow = false, notify = false)
|
||||
viewModel.follow(follow = false, notify = false)
|
||||
|
||||
verify(repository, times(1)).followCreator(100L, false, false, "Bearer test-token")
|
||||
pending.onSuccess(ApiResponse(true, Any(), null))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `팔로우 실패는 현재 content를 유지하고 unknown toast만 emit한다`() {
|
||||
whenever(repository.getHome(100L, "Bearer test-token")).thenReturn(Single.just(ApiResponse(true, response(), null)))
|
||||
whenever(repository.followCreator(100L, false, false, "Bearer test-token")).thenReturn(
|
||||
Single.just(ApiResponse(false, null, "failed"))
|
||||
)
|
||||
viewModel.loadHome(100L)
|
||||
|
||||
viewModel.follow(follow = false, notify = false)
|
||||
|
||||
val state = viewModel.homeStateLiveData.requireValue() as CreatorChannelHomeUiState.Content
|
||||
assertEquals(100L, state.header.creatorId)
|
||||
assertTrue(state.header.isFollow)
|
||||
assertEquals(R.string.common_error_unknown, viewModel.toastLiveData.requireValue()?.resId)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `채팅방 생성은 characterId가 유효할 때만 호출하고 chatRoomId를 emit한다`() {
|
||||
whenever(repository.createChatRoom(200L, "Bearer test-token"))
|
||||
.thenReturn(Single.just(ApiResponse(true, CreateChatRoomResponse(chatRoomId = 300L), null)))
|
||||
|
||||
viewModel.createChatRoom(0L)
|
||||
viewModel.createChatRoom(200L)
|
||||
|
||||
verify(repository, never()).createChatRoom(eq(0L), any())
|
||||
verify(repository).createChatRoom(200L, "Bearer test-token")
|
||||
val event = viewModel.chatRoomIdLiveData.requireValue()
|
||||
assertEquals(300L, event?.consume())
|
||||
assertEquals(null, event?.consume())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `채팅방 생성 진행 중 중복 요청은 무시한다`() {
|
||||
val pending = SingleSubject.create<ApiResponse<CreateChatRoomResponse>>()
|
||||
whenever(repository.createChatRoom(200L, "Bearer test-token")).thenReturn(pending)
|
||||
|
||||
viewModel.createChatRoom(200L)
|
||||
viewModel.createChatRoom(200L)
|
||||
|
||||
verify(repository, times(1)).createChatRoom(200L, "Bearer test-token")
|
||||
pending.onSuccess(ApiResponse(true, CreateChatRoomResponse(chatRoomId = 300L), null))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `채팅방 생성 실패는 현재 content를 유지하고 unknown toast만 emit한다`() {
|
||||
whenever(repository.getHome(100L, "Bearer test-token")).thenReturn(Single.just(ApiResponse(true, response(), null)))
|
||||
whenever(repository.createChatRoom(200L, "Bearer test-token")).thenReturn(
|
||||
Single.just(ApiResponse(false, null, "failed"))
|
||||
)
|
||||
viewModel.loadHome(100L)
|
||||
|
||||
viewModel.createChatRoom(200L)
|
||||
|
||||
val state = viewModel.homeStateLiveData.requireValue() as CreatorChannelHomeUiState.Content
|
||||
assertEquals(100L, state.header.creatorId)
|
||||
assertEquals(R.string.common_error_unknown, viewModel.toastLiveData.requireValue()?.resId)
|
||||
}
|
||||
|
||||
private fun setImmediateRxSchedulers() {
|
||||
val trampoline = { _: Scheduler -> Schedulers.trampoline() }
|
||||
RxJavaPlugins.setIoSchedulerHandler(trampoline)
|
||||
RxAndroidPlugins.setInitMainThreadSchedulerHandler { Schedulers.trampoline() }
|
||||
RxAndroidPlugins.setMainThreadSchedulerHandler { Schedulers.trampoline() }
|
||||
}
|
||||
|
||||
private fun response(isFollow: Boolean = true) = CreatorChannelHomeResponse(
|
||||
creator = CreatorChannelCreatorResponse(
|
||||
creatorId = 100L,
|
||||
characterId = 200L,
|
||||
nickname = "소다",
|
||||
profileImageUrl = "https://example.com/profile.png",
|
||||
followerCount = 1234,
|
||||
isAiChatAvailable = true,
|
||||
isDmAvailable = true,
|
||||
isFollow = isFollow,
|
||||
isNotify = false
|
||||
),
|
||||
currentLive = CreatorChannelLiveResponse(1L, "라이브", null, "2026-06-11T12:00:00Z", 0, false),
|
||||
latestAudioContent = CreatorChannelAudioContentResponse(1L, "오디오", null, null, 0, true, false, null, null),
|
||||
channelDonations = emptyList(),
|
||||
notices = emptyList(),
|
||||
schedules = listOf(CreatorChannelScheduleResponse("2026-06-11T12:00:00Z", "일정", CreatorActivityType.Live, 1L)),
|
||||
audioContents = emptyList(),
|
||||
series = emptyList(),
|
||||
communities = emptyList(),
|
||||
fanTalk = CreatorChannelFanTalkSummaryResponse(0, null),
|
||||
introduce = "",
|
||||
activity = CreatorChannelActivityResponse(null, "D+1", 1, 2, 3, 4, 5),
|
||||
sns = CreatorChannelSnsResponse("", "", "", "", "")
|
||||
)
|
||||
|
||||
private fun <T> LiveData<T>.requireValue(): T? {
|
||||
var value: T? = null
|
||||
val observer = Observer<T> { value = it }
|
||||
observeForever(observer)
|
||||
removeObserver(observer)
|
||||
return value
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user