Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,10 @@ internal class UserReceiverRepositoryImpl
}
}

override fun refreshReceiverList() {
receiverRefreshRevision.update { it + 1 }
}

/**
* 비로그인 상태에서는 서버를 호출하지 않고 빈 목록을 돌려준다. 따라서 호출처는 빈 목록만으로
* «수신인 없음» 과 «로그인 안 됨» 을 구분할 수 없다 — 구분이 필요하면 [AuthRepository.isLoggedIn] 을 함께 봐야 한다.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,71 @@ class UserRepositoryImplTest {
assertEquals(2, requestCount)
}

@Test
fun `refreshReceiverList - 같은 구독을 서버의 최신 목록으로 갱신한다`() =
runBlocking {
var requestCount = 0
val repository =
repository(
onGetReceivers = {
requestCount++
dataResponse(listOf(receiverDto("조회 $requestCount")))
},
)
val emissions = Channel<List<Receiver>>(capacity = Channel.UNLIMITED)
val collector =
launch(start = CoroutineStart.UNDISPATCHED) {
repository.receiverListFlow.collect { emissions.send(it) }
}

try {
assertEquals("조회 1", withTimeout(TEST_TIMEOUT_MILLIS) { emissions.receive() }.single().name)

val narrowContract: UserReceiverRepository = repository
narrowContract.refreshReceiverList()

assertEquals("조회 2", withTimeout(TEST_TIMEOUT_MILLIS) { emissions.receive() }.single().name)
assertEquals(2, requestCount)
} finally {
collector.cancelAndJoin()
}
}

@Test
fun `refreshReceiverList - 갱신 실패는 마지막 목록을 유지하고 다음 갱신은 복구한다`() =
runBlocking {
var requestCount = 0
val repository =
repository(
onGetReceivers = {
requestCount++
if (requestCount == 2) throw UnknownHostException("일시적인 조회 실패")
dataResponse(listOf(receiverDto("조회 $requestCount")))
},
)
val emissions = Channel<List<Receiver>>(capacity = Channel.UNLIMITED)
val collector =
launch(start = CoroutineStart.UNDISPATCHED) {
repository.receiverListFlow.collect { emissions.send(it) }
}

try {
val first = withTimeout(TEST_TIMEOUT_MILLIS) { emissions.receive() }
repository.refreshReceiverList()

assertEquals(first, withTimeout(TEST_TIMEOUT_MILLIS) { emissions.receive() })
assertEquals(2, requestCount)
assertEquals(1, errorReporter.writtenFailures.size)

repository.refreshReceiverList()

assertEquals("조회 3", withTimeout(TEST_TIMEOUT_MILLIS) { emissions.receive() }.single().name)
assertEquals(3, requestCount)
} finally {
collector.cancelAndJoin()
}
}

@Test
fun `receiverListFlow - 조회가 실패해도 예외로 새지 않고 빈 목록을 낸다`() {
val repository = repository(onGetReceivers = { throw UnknownHostException("Unable to resolve host") })
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,12 @@ interface UserReceiverRepository {
*/
val receiverListFlow: Flow<List<Receiver>>

/**
* 현재 구독 중인 수신자 목록을 다시 조회하도록 요청한다. 조회 결과와 세션별 실패 처리는
* [receiverListFlow] 계약을 따르며, 새 구독은 이 호출 없이도 최신 목록을 조회한다.
*/
fun refreshReceiverList()

// 수신자 목록 조회
suspend fun getReceivers(): List<Receiver>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ class FakeUserReceiverRepository(
var onUpdateReceiverMessage: (suspend (Long, String) -> Unit)? = null,
var onGetReceiverDeliveryConditions: (suspend (Long) -> ReceiverDeliveryConditions)? = null,
var onUpdateReceiverDeliveryConditions: (suspend (Long, List<DeliveryConditionItem>) -> ReceiverDeliveryConditions)? = null,
var onRefreshReceiverList: (() -> Unit)? = null,
) : UserReceiverRepository {
val receiverState = MutableStateFlow(receivers.toList())
val receiverDetails = ConcurrentHashMap(receiverDetails)
Expand All @@ -43,6 +44,7 @@ class FakeUserReceiverRepository(

private val receiverListFlowCounter = AtomicInteger()
private val getReceiversCounter = AtomicInteger()
private val refreshReceiverListCounter = AtomicInteger()

val receiverCreateCalls = CopyOnWriteArrayList<FakeUserReceiverRepository.ReceiverCreateCall>()
val receiverDetailCalls = CopyOnWriteArrayList<Long>()
Expand All @@ -53,6 +55,7 @@ class FakeUserReceiverRepository(

val receiverListFlowCalls: Int get() = receiverListFlowCounter.get()
val getReceiversCalls: Int get() = getReceiversCounter.get()
val refreshReceiverListCalls: Int get() = refreshReceiverListCounter.get()
val receiverCalls: Int get() = getReceiversCounter.get()

override val receiverListFlow: Flow<List<Receiver>>
Expand All @@ -61,6 +64,11 @@ class FakeUserReceiverRepository(
return onReceiverListFlow?.invoke() ?: receiverState
}

override fun refreshReceiverList() {
refreshReceiverListCounter.incrementAndGet()
onRefreshReceiverList?.invoke()
}

override suspend fun getReceivers(): List<Receiver> {
getReceiversCounter.incrementAndGet()
onGetReceivers?.let { return it() }
Expand Down Expand Up @@ -179,6 +187,7 @@ class FakeUserReceiverRepository(
receivers = emptyList(),
onReceiverListFlow = { unexpectedCall("UserReceiverRepository.receiverListFlow") },
onGetReceivers = { unexpectedCall("UserReceiverRepository.getReceivers") },
onRefreshReceiverList = { unexpectedCall("UserReceiverRepository.refreshReceiverList") },
onCreateReceiver = { _, _, _, _, _ -> unexpectedCall("UserReceiverRepository.createReceiver") },
onGetReceiverDetail = { unexpectedCall("UserReceiverRepository.getReceiverDetail") },
onUpdateReceiver = { _, _, _, _, _ -> unexpectedCall("UserReceiverRepository.updateReceiver") },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ class FakeUserRepository private constructor(
onUpdateReceiverDeliveryConditions: (
suspend (Long, List<DeliveryConditionItem>) -> ReceiverDeliveryConditions
)? = null,
onRefreshReceiverList: (() -> Unit)? = null,
) : this(
receiverFake =
FakeUserReceiverRepository(
Expand All @@ -52,6 +53,7 @@ class FakeUserRepository private constructor(
deliveryConditions = deliveryConditions,
onReceiverListFlow = onReceiverListFlow,
onGetReceivers = onGetReceivers,
onRefreshReceiverList = onRefreshReceiverList,
onCreateReceiver = onCreateReceiver,
onGetReceiverDetail = onGetReceiverDetail,
onUpdateReceiver = onUpdateReceiver,
Expand Down Expand Up @@ -79,6 +81,7 @@ class FakeUserRepository private constructor(
val deliveryUpdateCalls: CopyOnWriteArrayList<DeliveryUpdateCall> get() = receiverFake.deliveryUpdateCalls
val receiverListFlowCalls: Int get() = receiverFake.receiverListFlowCalls
val getReceiversCalls: Int get() = receiverFake.getReceiversCalls
val refreshReceiverListCalls: Int get() = receiverFake.refreshReceiverListCalls
val receiverCalls: Int get() = receiverFake.receiverCalls

var onReceiverListFlow: (() -> Flow<List<Receiver>>)?
Expand All @@ -87,6 +90,12 @@ class FakeUserRepository private constructor(
receiverFake.onReceiverListFlow = value
}

var onRefreshReceiverList: (() -> Unit)?
get() = receiverFake.onRefreshReceiverList
set(value) {
receiverFake.onRefreshReceiverList = value
}

var onGetReceivers: (suspend () -> List<Receiver>)?
get() = receiverFake.onGetReceivers
set(value) {
Expand Down Expand Up @@ -158,6 +167,7 @@ class FakeUserRepository private constructor(
receivers = emptyList(),
onReceiverListFlow = { unexpectedCall("UserRepository.receiverListFlow") },
onGetReceivers = { unexpectedCall("UserRepository.getReceivers") },
onRefreshReceiverList = { unexpectedCall("UserRepository.refreshReceiverList") },
onCreateReceiver = { _, _, _, _, _ -> unexpectedCall("UserRepository.createReceiver") },
onGetReceiverDetail = { unexpectedCall("UserRepository.getReceiverDetail") },
onUpdateReceiver = { _, _, _, _, _ -> unexpectedCall("UserRepository.updateReceiver") },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import javax.inject.Inject
internal class ReceiverListViewModel
@Inject
constructor(
userRepository: UserReceiverRepository,
private val userRepository: UserReceiverRepository,
) : MviViewModel<ReceiverListIntent, ReceiverListUiState, ReceiverListReducerEvent>(ReceiverListUiState()) {
private val receivers = userRepository.receiverListFlow
private var observationJob: Job? = null
Expand All @@ -36,8 +36,14 @@ internal class ReceiverListViewModel
}

private fun startObservation() {
val isReturning = stopJob != null
stopJob?.cancel()
if (observationJob?.isActive == true) return
stopJob = null
if (observationJob?.isActive == true) {
// 구독 중단 유예 안에 복귀해도 서버 목록은 갱신한다. 같은 구독의 실패 시 캐시는 보존한다.
if (isReturning) userRepository.refreshReceiverList()
return
}
observationJob =
viewModelScope.launch {
receivers.collect { receivers ->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ class ReceiverListViewModelTest {
private val receivers = MutableStateFlow(listOf(Receiver(7L, "김수신", "가족", "auth-7")))
private var subscriptions = 0
private var cancellations = 0
private var refreshes = 0
private var serverReceivers = receivers.value

@Before
fun setUp() {
Expand All @@ -53,6 +55,7 @@ class ReceiverListViewModelTest {

assertEquals(listOf(ReceiverListItem(7L, "김수신", "가족")), viewModel.uiState.value.receivers)
assertEquals(1, subscriptions)
assertEquals(0, refreshes)
}

@Test
Expand All @@ -67,26 +70,48 @@ class ReceiverListViewModelTest {

assertEquals(1, subscriptions)
assertEquals(0, cancellations)
assertEquals(0, refreshes)
}

@Test
fun `5초 이내 복귀는 기존 구독과 최신 목록을 유지한다`() =
fun `5초 이내 복귀는 기존 구독을 유지하면서 서버 목록을 다시 조회한다`() =
runTest(dispatcher) {
val viewModel = viewModel()
viewModel.onIntent(ReceiverListIntent.ObservationStarted)
runCurrent()
viewModel.onIntent(ReceiverListIntent.ObservationStopped)
advanceTimeBy(4_999)
receivers.value = listOf(Receiver(8L, "박수신", "친구", "auth-8"))
serverReceivers = listOf(Receiver(8L, "박수신", "친구", "auth-8"))
runCurrent()

assertEquals(listOf(ReceiverListItem(8L, "박수신", "친구")), viewModel.uiState.value.receivers)
assertEquals(listOf(ReceiverListItem(7L, "김수신", "가족")), viewModel.uiState.value.receivers)
viewModel.onIntent(ReceiverListIntent.ObservationStarted)
advanceTimeBy(5_000)
runCurrent()

assertEquals(listOf(ReceiverListItem(8L, "박수신", "친구")), viewModel.uiState.value.receivers)
assertEquals(1, subscriptions)
assertEquals(0, cancellations)
assertEquals(1, refreshes)
}

@Test
fun `빠른 재진입마다 한 번 갱신하고 중복 시작 신호는 추가 조회하지 않는다`() =
runTest(dispatcher) {
val viewModel = viewModel()
viewModel.onIntent(ReceiverListIntent.ObservationStarted)
runCurrent()

repeat(2) {
viewModel.onIntent(ReceiverListIntent.ObservationStopped)
viewModel.onIntent(ReceiverListIntent.ObservationStarted)
viewModel.onIntent(ReceiverListIntent.ObservationStarted)
runCurrent()
}

assertEquals(1, subscriptions)
assertEquals(0, cancellations)
assertEquals(2, refreshes)
}

@Test
Expand All @@ -108,6 +133,7 @@ class ReceiverListViewModelTest {
runCurrent()

assertEquals(2, subscriptions)
assertEquals(0, refreshes)
assertEquals(listOf(ReceiverListItem(9L, "이수신", "동료")), viewModel.uiState.value.receivers)
}

Expand Down Expand Up @@ -157,6 +183,10 @@ class ReceiverListViewModelTest {
private fun viewModel(): ReceiverListViewModel {
val repository =
FakeUserReceiverRepository.strict().apply {
onRefreshReceiverList = {
refreshes++
receivers.value = serverReceivers
}
onReceiverListFlow = {
flow {
subscriptions++
Expand Down
Loading