feat(chat): 채팅방 ViewModel을 추가한다
This commit is contained in:
@@ -177,6 +177,7 @@ import kr.co.vividnext.sodalive.user.find_password.FindPasswordViewModel
|
|||||||
import kr.co.vividnext.sodalive.user.login.LoginViewModel
|
import kr.co.vividnext.sodalive.user.login.LoginViewModel
|
||||||
import kr.co.vividnext.sodalive.user.signup.SignUpViewModel
|
import kr.co.vividnext.sodalive.user.signup.SignUpViewModel
|
||||||
import kr.co.vividnext.sodalive.v2.main.MainV2ViewModel
|
import kr.co.vividnext.sodalive.v2.main.MainV2ViewModel
|
||||||
|
import kr.co.vividnext.sodalive.v2.main.chat.ChatMainViewModel
|
||||||
import kr.co.vividnext.sodalive.v2.main.chat.data.ChatRoomApi
|
import kr.co.vividnext.sodalive.v2.main.chat.data.ChatRoomApi
|
||||||
import kr.co.vividnext.sodalive.v2.main.chat.data.ChatRoomRepository
|
import kr.co.vividnext.sodalive.v2.main.chat.data.ChatRoomRepository
|
||||||
import kr.co.vividnext.sodalive.v2.main.home.HomeCreatorRankingViewModel
|
import kr.co.vividnext.sodalive.v2.main.home.HomeCreatorRankingViewModel
|
||||||
@@ -392,6 +393,7 @@ class AppDI(private val context: Context, isDebugMode: Boolean) {
|
|||||||
viewModel { SearchViewModel(get()) }
|
viewModel { SearchViewModel(get()) }
|
||||||
viewModel { PointStatusViewModel(get()) }
|
viewModel { PointStatusViewModel(get()) }
|
||||||
viewModel { HomeViewModel(get(), get()) }
|
viewModel { HomeViewModel(get(), get()) }
|
||||||
|
viewModel { ChatMainViewModel(get()) }
|
||||||
viewModel { HomeCreatorRankingViewModel(get()) }
|
viewModel { HomeCreatorRankingViewModel(get()) }
|
||||||
viewModel { HomeRecommendationViewModel(get()) }
|
viewModel { HomeRecommendationViewModel(get()) }
|
||||||
viewModel { PushNotificationListViewModel(get()) }
|
viewModel { PushNotificationListViewModel(get()) }
|
||||||
|
|||||||
@@ -0,0 +1,154 @@
|
|||||||
|
package kr.co.vividnext.sodalive.v2.main.chat
|
||||||
|
|
||||||
|
import androidx.lifecycle.LiveData
|
||||||
|
import androidx.lifecycle.MutableLiveData
|
||||||
|
import com.orhanobut.logger.Logger
|
||||||
|
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||||
|
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||||
|
import kr.co.vividnext.sodalive.R
|
||||||
|
import kr.co.vividnext.sodalive.base.BaseViewModel
|
||||||
|
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
|
||||||
|
import kr.co.vividnext.sodalive.common.ToastMessage
|
||||||
|
import kr.co.vividnext.sodalive.v2.main.chat.data.ChatRoomListPageResponse
|
||||||
|
import kr.co.vividnext.sodalive.v2.main.chat.data.ChatRoomRepository
|
||||||
|
import kr.co.vividnext.sodalive.v2.main.chat.model.ChatRoomFilter
|
||||||
|
import kr.co.vividnext.sodalive.v2.main.chat.model.ChatRoomListUiItem
|
||||||
|
import kr.co.vividnext.sodalive.v2.main.chat.model.ChatRoomListUiState
|
||||||
|
import kr.co.vividnext.sodalive.v2.main.chat.model.toUiItems
|
||||||
|
|
||||||
|
class ChatMainViewModel(
|
||||||
|
private val repository: ChatRoomRepository
|
||||||
|
) : BaseViewModel() {
|
||||||
|
|
||||||
|
private var currentFilter: ChatRoomFilter = ChatRoomFilter.ALL
|
||||||
|
private var currentItems: List<ChatRoomListUiItem> = emptyList()
|
||||||
|
private var nextCursor: String? = null
|
||||||
|
private var hasMore: Boolean = false
|
||||||
|
private var requestGeneration: Long = 0L
|
||||||
|
|
||||||
|
private val _chatRoomStateLiveData = MutableLiveData<ChatRoomListUiState>()
|
||||||
|
val chatRoomStateLiveData: LiveData<ChatRoomListUiState>
|
||||||
|
get() = _chatRoomStateLiveData
|
||||||
|
|
||||||
|
private val _toastLiveData = MutableLiveData<ToastMessage?>()
|
||||||
|
val toastLiveData: LiveData<ToastMessage?>
|
||||||
|
get() = _toastLiveData
|
||||||
|
|
||||||
|
private val _isLoading = MutableLiveData(false)
|
||||||
|
val isLoading: LiveData<Boolean>
|
||||||
|
get() = _isLoading
|
||||||
|
|
||||||
|
private val _isAppending = MutableLiveData(false)
|
||||||
|
val isAppending: LiveData<Boolean>
|
||||||
|
get() = _isAppending
|
||||||
|
|
||||||
|
fun loadFirstPage(filter: ChatRoomFilter = currentFilter) {
|
||||||
|
currentFilter = filter
|
||||||
|
requestGeneration += 1L
|
||||||
|
val generation = requestGeneration
|
||||||
|
currentItems = emptyList()
|
||||||
|
nextCursor = null
|
||||||
|
hasMore = false
|
||||||
|
_isLoading.value = true
|
||||||
|
_isAppending.value = false
|
||||||
|
_chatRoomStateLiveData.value = ChatRoomListUiState.Loading
|
||||||
|
|
||||||
|
compositeDisposable.add(
|
||||||
|
repository.getChatRooms(
|
||||||
|
token = authToken(),
|
||||||
|
filter = currentFilter.apiValue,
|
||||||
|
cursor = null
|
||||||
|
)
|
||||||
|
.subscribeOn(Schedulers.io())
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.subscribe(
|
||||||
|
{
|
||||||
|
if (generation != requestGeneration) return@subscribe
|
||||||
|
_isLoading.value = false
|
||||||
|
val data = it.data
|
||||||
|
if (it.success && data != null) {
|
||||||
|
handleFirstPageSuccess(data)
|
||||||
|
} else {
|
||||||
|
showUnknownError(it.message)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
if (generation != requestGeneration) return@subscribe
|
||||||
|
_isLoading.value = false
|
||||||
|
it.message?.let { message -> Logger.e(message) }
|
||||||
|
showUnknownError(it.message)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun selectFilter(filter: ChatRoomFilter) {
|
||||||
|
if (filter == currentFilter) return
|
||||||
|
loadFirstPage(filter)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun loadNextPage() {
|
||||||
|
val cursor = nextCursor
|
||||||
|
if (_isLoading.value == true || _isAppending.value == true || !hasMore || cursor == null) return
|
||||||
|
|
||||||
|
val generation = requestGeneration
|
||||||
|
_isAppending.value = true
|
||||||
|
_chatRoomStateLiveData.value = ChatRoomListUiState.Content(
|
||||||
|
items = currentItems,
|
||||||
|
isAppending = true
|
||||||
|
)
|
||||||
|
|
||||||
|
compositeDisposable.add(
|
||||||
|
repository.getChatRooms(
|
||||||
|
token = authToken(),
|
||||||
|
filter = currentFilter.apiValue,
|
||||||
|
cursor = cursor
|
||||||
|
)
|
||||||
|
.subscribeOn(Schedulers.io())
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.subscribe(
|
||||||
|
{
|
||||||
|
if (generation != requestGeneration) return@subscribe
|
||||||
|
_isAppending.value = false
|
||||||
|
val data = it.data
|
||||||
|
if (it.success && data != null) {
|
||||||
|
handleNextPageSuccess(data)
|
||||||
|
} else {
|
||||||
|
showUnknownError(it.message)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
if (generation != requestGeneration) return@subscribe
|
||||||
|
_isAppending.value = false
|
||||||
|
it.message?.let { message -> Logger.e(message) }
|
||||||
|
showUnknownError(it.message)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleFirstPageSuccess(data: ChatRoomListPageResponse) {
|
||||||
|
currentItems = data.rooms.toUiItems()
|
||||||
|
hasMore = data.hasMore
|
||||||
|
nextCursor = data.nextCursor
|
||||||
|
_chatRoomStateLiveData.value = if (currentItems.isEmpty()) {
|
||||||
|
ChatRoomListUiState.Empty
|
||||||
|
} else {
|
||||||
|
ChatRoomListUiState.Content(items = currentItems)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleNextPageSuccess(data: ChatRoomListPageResponse) {
|
||||||
|
currentItems = currentItems + data.rooms.toUiItems()
|
||||||
|
hasMore = data.hasMore
|
||||||
|
nextCursor = data.nextCursor
|
||||||
|
_chatRoomStateLiveData.value = ChatRoomListUiState.Content(items = currentItems)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showUnknownError(message: String?) {
|
||||||
|
_chatRoomStateLiveData.value = ChatRoomListUiState.Error(message = message)
|
||||||
|
_toastLiveData.value = ToastMessage(resId = R.string.common_error_unknown)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun authToken(): String = "Bearer ${SharedPreferenceManager.token}"
|
||||||
|
}
|
||||||
@@ -0,0 +1,297 @@
|
|||||||
|
package kr.co.vividnext.sodalive.v2.main.chat
|
||||||
|
|
||||||
|
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.common.ApiResponse
|
||||||
|
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
|
||||||
|
import kr.co.vividnext.sodalive.v2.main.chat.data.ChatRoomApi
|
||||||
|
import kr.co.vividnext.sodalive.v2.main.chat.data.ChatRoomListItemResponse
|
||||||
|
import kr.co.vividnext.sodalive.v2.main.chat.data.ChatRoomListPageResponse
|
||||||
|
import kr.co.vividnext.sodalive.v2.main.chat.data.ChatRoomRepository
|
||||||
|
import kr.co.vividnext.sodalive.v2.main.chat.model.ChatRoomFilter
|
||||||
|
import kr.co.vividnext.sodalive.v2.main.chat.model.ChatRoomListUiState
|
||||||
|
import org.junit.After
|
||||||
|
import org.junit.Assert.assertEquals
|
||||||
|
import org.junit.Assert.assertFalse
|
||||||
|
import org.junit.Assert.assertTrue
|
||||||
|
import org.junit.Before
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.runner.RunWith
|
||||||
|
import org.robolectric.RobolectricTestRunner
|
||||||
|
import org.robolectric.annotation.Config
|
||||||
|
|
||||||
|
@RunWith(RobolectricTestRunner::class)
|
||||||
|
@Config(sdk = [28], application = Application::class)
|
||||||
|
class ChatMainViewModelTest {
|
||||||
|
|
||||||
|
private val context: Context = ApplicationProvider.getApplicationContext()
|
||||||
|
private lateinit var api: FakeChatRoomApi
|
||||||
|
private lateinit var viewModel: ChatMainViewModel
|
||||||
|
|
||||||
|
@Before
|
||||||
|
fun setUp() {
|
||||||
|
setImmediateRxSchedulers()
|
||||||
|
SharedPreferenceManager.resetForTest()
|
||||||
|
SharedPreferenceManager.init(context)
|
||||||
|
SharedPreferenceManager.token = "test-token"
|
||||||
|
api = FakeChatRoomApi()
|
||||||
|
viewModel = ChatMainViewModel(
|
||||||
|
repository = ChatRoomRepository(api)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@After
|
||||||
|
fun tearDown() {
|
||||||
|
RxJavaPlugins.reset()
|
||||||
|
RxAndroidPlugins.reset()
|
||||||
|
SharedPreferenceManager.resetForTest()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `초기 첫 페이지 로드는 ALL filter와 null cursor로 요청한다`() {
|
||||||
|
api.enqueueSuccess(page(rooms = listOf(room(roomId = 1L))))
|
||||||
|
|
||||||
|
viewModel.loadFirstPage()
|
||||||
|
|
||||||
|
assertEquals(listOf(ApiCall("Bearer test-token", "ALL", null)), api.calls)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `다른 filter를 선택하면 기존 목록을 비우고 첫 페이지를 요청한다`() {
|
||||||
|
api.enqueueSuccess(page(rooms = listOf(room(roomId = 1L))))
|
||||||
|
viewModel.loadFirstPage()
|
||||||
|
api.enqueueSuccess(page(rooms = listOf(room(roomId = 2L, chatType = "DM"))))
|
||||||
|
|
||||||
|
viewModel.selectFilter(ChatRoomFilter.DM)
|
||||||
|
|
||||||
|
assertEquals(ApiCall("Bearer test-token", "DM", null), api.calls.last())
|
||||||
|
val state = viewModel.chatRoomStateLiveData.requireValue() as ChatRoomListUiState.Content
|
||||||
|
assertEquals(listOf(2L), state.items.map { it.roomId })
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `현재 선택된 filter를 다시 선택하면 API를 재호출하지 않는다`() {
|
||||||
|
api.enqueueSuccess(page(rooms = listOf(room(roomId = 1L))))
|
||||||
|
viewModel.loadFirstPage()
|
||||||
|
|
||||||
|
viewModel.selectFilter(ChatRoomFilter.ALL)
|
||||||
|
|
||||||
|
assertEquals(1, api.calls.size)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `첫 페이지 success와 rooms가 있으면 Content를 emit한다`() {
|
||||||
|
api.enqueueSuccess(page(rooms = listOf(room(roomId = 10L, targetName = "크리에이터"))))
|
||||||
|
|
||||||
|
viewModel.loadFirstPage()
|
||||||
|
|
||||||
|
val state = viewModel.chatRoomStateLiveData.requireValue() as ChatRoomListUiState.Content
|
||||||
|
|
||||||
|
assertFalse(state.isAppending)
|
||||||
|
assertEquals(1, state.items.size)
|
||||||
|
assertEquals(10L, state.items[0].roomId)
|
||||||
|
assertEquals("크리에이터", state.items[0].targetName)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `첫 페이지 success와 rooms가 비어 있으면 Empty를 emit한다`() {
|
||||||
|
api.enqueueSuccess(page(rooms = emptyList()))
|
||||||
|
|
||||||
|
viewModel.loadFirstPage()
|
||||||
|
|
||||||
|
val state = viewModel.chatRoomStateLiveData.requireValue()
|
||||||
|
|
||||||
|
assertTrue(state is ChatRoomListUiState.Empty)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `다음 cursor가 있으면 다음 페이지를 요청하고 기존 목록에 append한다`() {
|
||||||
|
api.enqueueSuccess(page(rooms = listOf(room(roomId = 1L)), hasMore = true, nextCursor = "next-1"))
|
||||||
|
viewModel.loadFirstPage()
|
||||||
|
api.enqueueSuccess(page(rooms = listOf(room(roomId = 2L)), hasMore = false, nextCursor = null))
|
||||||
|
|
||||||
|
viewModel.loadNextPage()
|
||||||
|
val state = viewModel.chatRoomStateLiveData.requireValue() as ChatRoomListUiState.Content
|
||||||
|
|
||||||
|
assertEquals(ApiCall("Bearer test-token", "ALL", "next-1"), api.calls.last())
|
||||||
|
assertEquals(listOf(1L, 2L), state.items.map { it.roomId })
|
||||||
|
assertFalse(state.isAppending)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `hasMore가 false이면 다음 페이지 API를 호출하지 않는다`() {
|
||||||
|
api.enqueueSuccess(page(rooms = listOf(room(roomId = 1L)), hasMore = false, nextCursor = null))
|
||||||
|
viewModel.loadFirstPage()
|
||||||
|
|
||||||
|
viewModel.loadNextPage()
|
||||||
|
|
||||||
|
assertEquals(1, api.calls.size)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `append loading 중 중복 다음 페이지 요청은 무시한다`() {
|
||||||
|
api.enqueueSuccess(page(rooms = listOf(room(roomId = 1L)), hasMore = true, nextCursor = "next-1"))
|
||||||
|
viewModel.loadFirstPage()
|
||||||
|
val pendingNextPage = SingleSubject.create<ApiResponse<ChatRoomListPageResponse>>()
|
||||||
|
api.enqueue(pendingNextPage)
|
||||||
|
|
||||||
|
viewModel.loadNextPage()
|
||||||
|
viewModel.loadNextPage()
|
||||||
|
|
||||||
|
assertEquals(2, api.calls.size)
|
||||||
|
val appendingState = viewModel.chatRoomStateLiveData.requireValue() as ChatRoomListUiState.Content
|
||||||
|
assertTrue(appendingState.isAppending)
|
||||||
|
|
||||||
|
pendingNextPage.onSuccess(ApiResponse(true, page(rooms = listOf(room(roomId = 2L))), null))
|
||||||
|
val completedState = viewModel.chatRoomStateLiveData.requireValue() as ChatRoomListUiState.Content
|
||||||
|
assertEquals(listOf(1L, 2L), completedState.items.map { it.roomId })
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `filter 변경 전 첫 페이지 응답이 늦게 도착하면 현재 filter 목록을 덮어쓰지 않는다`() {
|
||||||
|
val pendingAllPage = SingleSubject.create<ApiResponse<ChatRoomListPageResponse>>()
|
||||||
|
api.enqueue(pendingAllPage)
|
||||||
|
viewModel.loadFirstPage(ChatRoomFilter.ALL)
|
||||||
|
api.enqueueSuccess(page(rooms = listOf(room(roomId = 2L, chatType = "DM"))))
|
||||||
|
|
||||||
|
viewModel.selectFilter(ChatRoomFilter.DM)
|
||||||
|
pendingAllPage.onSuccess(ApiResponse(true, page(rooms = listOf(room(roomId = 1L))), null))
|
||||||
|
val state = viewModel.chatRoomStateLiveData.requireValue() as ChatRoomListUiState.Content
|
||||||
|
|
||||||
|
assertEquals(listOf(2L), state.items.map { it.roomId })
|
||||||
|
assertEquals(ApiCall("Bearer test-token", "DM", null), api.calls.last())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `filter 변경 전 다음 페이지 응답이 늦게 도착하면 현재 filter 목록에 append하지 않는다`() {
|
||||||
|
api.enqueueSuccess(page(rooms = listOf(room(roomId = 1L)), hasMore = true, nextCursor = "next-1"))
|
||||||
|
viewModel.loadFirstPage(ChatRoomFilter.ALL)
|
||||||
|
val pendingNextPage = SingleSubject.create<ApiResponse<ChatRoomListPageResponse>>()
|
||||||
|
api.enqueue(pendingNextPage)
|
||||||
|
viewModel.loadNextPage()
|
||||||
|
api.enqueueSuccess(page(rooms = listOf(room(roomId = 3L, chatType = "DM"))))
|
||||||
|
|
||||||
|
viewModel.selectFilter(ChatRoomFilter.DM)
|
||||||
|
pendingNextPage.onSuccess(ApiResponse(true, page(rooms = listOf(room(roomId = 2L))), null))
|
||||||
|
val state = viewModel.chatRoomStateLiveData.requireValue() as ChatRoomListUiState.Content
|
||||||
|
|
||||||
|
assertEquals(listOf(3L), state.items.map { it.roomId })
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `API failure는 Error와 unknown error toast를 emit한다`() {
|
||||||
|
api.enqueueSuccess(ApiResponse(false, null, "failed"))
|
||||||
|
|
||||||
|
viewModel.loadFirstPage()
|
||||||
|
|
||||||
|
val state = viewModel.chatRoomStateLiveData.requireValue() as ChatRoomListUiState.Error
|
||||||
|
|
||||||
|
assertEquals("failed", state.message)
|
||||||
|
assertEquals(R.string.common_error_unknown, viewModel.toastLiveData.requireValue()?.resId)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `data null 응답은 Error와 unknown error toast를 emit한다`() {
|
||||||
|
api.enqueueSuccess(ApiResponse(true, null, null))
|
||||||
|
|
||||||
|
viewModel.loadFirstPage()
|
||||||
|
val state = viewModel.chatRoomStateLiveData.requireValue()
|
||||||
|
|
||||||
|
assertTrue(state is ChatRoomListUiState.Error)
|
||||||
|
assertEquals(R.string.common_error_unknown, viewModel.toastLiveData.requireValue()?.resId)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `Throwable은 Error와 unknown error toast를 emit한다`() {
|
||||||
|
api.enqueue(Single.error(IllegalStateException("network")))
|
||||||
|
|
||||||
|
viewModel.loadFirstPage()
|
||||||
|
val state = viewModel.chatRoomStateLiveData.requireValue() as ChatRoomListUiState.Error
|
||||||
|
|
||||||
|
assertEquals("network", state.message)
|
||||||
|
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 page(
|
||||||
|
rooms: List<ChatRoomListItemResponse>,
|
||||||
|
hasMore: Boolean = false,
|
||||||
|
nextCursor: String? = null
|
||||||
|
) = ChatRoomListPageResponse(
|
||||||
|
rooms = rooms,
|
||||||
|
hasMore = hasMore,
|
||||||
|
nextCursor = nextCursor
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun room(
|
||||||
|
roomId: Long,
|
||||||
|
chatType: String = "AI",
|
||||||
|
targetName: String = "상대방",
|
||||||
|
targetImageUrl: String = "https://example.com/profile.png",
|
||||||
|
lastMessage: String = "마지막 메시지",
|
||||||
|
lastMessageAt: String = "2026-06-09T12:00:00Z"
|
||||||
|
) = ChatRoomListItemResponse(
|
||||||
|
roomId = roomId,
|
||||||
|
chatType = chatType,
|
||||||
|
targetName = targetName,
|
||||||
|
targetImageUrl = targetImageUrl,
|
||||||
|
lastMessage = lastMessage,
|
||||||
|
lastMessageAt = lastMessageAt
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun <T> LiveData<T>.requireValue(): T? {
|
||||||
|
var value: T? = null
|
||||||
|
val observer = Observer<T> { value = it }
|
||||||
|
observeForever(observer)
|
||||||
|
removeObserver(observer)
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
private data class ApiCall(
|
||||||
|
val token: String,
|
||||||
|
val filter: String,
|
||||||
|
val cursor: String?
|
||||||
|
)
|
||||||
|
|
||||||
|
private class FakeChatRoomApi : ChatRoomApi {
|
||||||
|
val calls = mutableListOf<ApiCall>()
|
||||||
|
private val responses = ArrayDeque<Single<ApiResponse<ChatRoomListPageResponse>>>()
|
||||||
|
|
||||||
|
fun enqueueSuccess(response: ApiResponse<ChatRoomListPageResponse>) {
|
||||||
|
enqueue(Single.just(response))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun enqueueSuccess(page: ChatRoomListPageResponse) {
|
||||||
|
enqueueSuccess(ApiResponse(true, page, null))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun enqueue(response: Single<ApiResponse<ChatRoomListPageResponse>>) {
|
||||||
|
responses.addLast(response)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getChatRooms(
|
||||||
|
authHeader: String,
|
||||||
|
filter: String,
|
||||||
|
cursor: String?
|
||||||
|
): Single<ApiResponse<ChatRoomListPageResponse>> {
|
||||||
|
calls.add(ApiCall(authHeader, filter, cursor))
|
||||||
|
return responses.removeFirst()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user