From c738be290a5773bc1605161d1103e0a54a4f660a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=BC=ED=98=81?= Date: Sun, 13 Sep 2026 17:03:21 +0900 Subject: [PATCH] =?UTF-8?q?test(core):=20=EB=93=B1=EB=A1=9D=EC=9D=B4=20?= =?UTF-8?q?=EB=AA=A9=EB=A1=9D=20=EA=B0=B1=EC=8B=A0=20=EC=8B=A4=ED=8C=A8?= =?UTF-8?q?=EC=97=90=20=EB=A7=A4=EC=9D=B4=EC=A7=80=20=EC=95=8A=EC=9D=8C?= =?UTF-8?q?=EC=9D=84=20=ED=9A=8C=EA=B7=80=EB=A1=9C=20=EA=B3=A0=EC=A0=95=20?= =?UTF-8?q?(#709)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../data/repoimpl/UserRepositoryImplTest.kt | 172 ++++++++++++++++++ 1 file changed, 172 insertions(+) diff --git a/core/data/src/test/java/com/afternote/core/data/repoimpl/UserRepositoryImplTest.kt b/core/data/src/test/java/com/afternote/core/data/repoimpl/UserRepositoryImplTest.kt index c13e6a91a2..8d62b5d472 100644 --- a/core/data/src/test/java/com/afternote/core/data/repoimpl/UserRepositoryImplTest.kt +++ b/core/data/src/test/java/com/afternote/core/data/repoimpl/UserRepositoryImplTest.kt @@ -5,6 +5,7 @@ import com.afternote.core.domain.repository.UserReceiverRepository import com.afternote.core.domain.repository.UserRepository import com.afternote.core.domain.testing.FakeAuthRepository import com.afternote.core.model.user.Receiver +import com.afternote.core.model.user.ReceiverCreated import com.afternote.core.network.dto.DeletePushTokenRequestDto import com.afternote.core.network.dto.PushTokenDto import com.afternote.core.network.dto.ReceiverDetailDto @@ -28,7 +29,9 @@ import com.afternote.core.network.dto.delivery.ReceiverDeliveryConditionUpdateRe import com.afternote.core.network.model.ApiException import com.afternote.core.network.model.BaseResponse import com.afternote.core.network.service.UserApiService +import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.CoroutineStart +import kotlinx.coroutines.async import kotlinx.coroutines.cancelAndJoin import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.first @@ -36,7 +39,9 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking import kotlinx.coroutines.withTimeout import org.junit.Assert.assertEquals +import org.junit.Assert.assertNull import org.junit.Assert.assertThrows +import org.junit.Assert.assertTrue import org.junit.Test import java.net.UnknownHostException @@ -467,6 +472,173 @@ class UserRepositoryImplTest { } } + /** + * #709 회귀 — POST 가 성공한 순간 등록은 확정된다. 뒤따르는 목록 갱신 조회가 아직 매달려 있어도 + * 호출자는 [ReceiverCreated] 를 받아야 하고, 그 조회가 끝내 실패해도 POST 는 한 번뿐이어야 한다. + * + * 옛 구현은 등록 직후의 조회를 `createReceiver` 안에서 직접 기다렸다가 그 실패를 등록 실패로 올렸다. + * 화면은 오류 문구도 없이 사용자를 같은 등록 버튼으로 돌려보냈고, 서버에 중복 방지가 없으면 + * 재시도가 같은 수신자를 한 번 더 만들었다. + */ + @Test + fun `createReceiver - 목록 갱신 조회가 매달려 있어도 등록은 그 자리에서 끝난다`() = + runBlocking { + var getCount = 0 + var postCount = 0 + val refreshStarted = Channel(capacity = Channel.UNLIMITED) + val refreshGate = CompletableDeferred() + val repository = + repository( + onGetReceivers = { + getCount += 1 + if (getCount == 1) { + dataResponse(listOf(receiverDto("계정 A"))) + } else { + refreshStarted.send(Unit) + refreshGate.await() + throw UnknownHostException("등록 직후 갱신 조회 실패") + } + }, + onCreateReceiver = { + postCount += 1 + dataResponse(UserCreateReceiverDto(receiverId = 2L, authCode = "AUTH-2")) + }, + ) + val emissions = Channel>(capacity = Channel.UNLIMITED) + val collector = + launch(start = CoroutineStart.UNDISPATCHED) { + repository.receiverListFlow.collect { emissions.send(it) } + } + + try { + assertEquals( + listOf("계정 A"), + withTimeout(TEST_TIMEOUT_MILLIS) { emissions.receive() }.map { it.name }, + ) + + val registration = + async(start = CoroutineStart.UNDISPATCHED) { + repository.createReceiver( + name = "새 수신자", + relation = "친구", + phone = null, + email = "receiver@example.com", + message = null, + ) + } + withTimeout(TEST_TIMEOUT_MILLIS) { refreshStarted.receive() } + + assertTrue("등록은 매달린 갱신 조회를 기다리지 않는다", registration.isCompleted) + assertEquals(ReceiverCreated(receiverId = 2L, authCode = "AUTH-2"), registration.await()) + assertEquals(1, postCount) + assertNull("갱신 조회가 끝나기 전에는 새 목록도 나오지 않는다", emissions.tryReceive().getOrNull()) + + refreshGate.complete(Unit) + assertEquals( + listOf("계정 A"), + withTimeout(TEST_TIMEOUT_MILLIS) { emissions.receive() }.map { it.name }, + ) + assertEquals(1, postCount) + assertEquals(2, getCount) + } finally { + refreshGate.complete(Unit) + collector.cancelAndJoin() + } + } + + /** + * #709 회귀 — 실패한 갱신 조회는 등록과 무관하게 혼자 복구된다. 목록이 다시 떠도 등록 POST 가 + * 다시 나가면 안 된다(같은 수신자가 두 번 생긴다). + */ + @Test + fun `createReceiver - 갱신 조회가 실패한 뒤 목록이 복구돼도 POST 를 다시 보내지 않는다`() = + runBlocking { + var getCount = 0 + var postCount = 0 + val repository = + repository( + onGetReceivers = { + getCount += 1 + when (getCount) { + 1 -> dataResponse(listOf(receiverDto("계정 A"))) + 2 -> throw UnknownHostException("등록 직후 갱신 조회 실패") + else -> dataResponse(listOf(receiverDto("복구된 목록"))) + } + }, + onCreateReceiver = { + postCount += 1 + dataResponse(UserCreateReceiverDto(receiverId = 2L, authCode = "AUTH-2")) + }, + ) + val emissions = Channel>(capacity = Channel.UNLIMITED) + val collector = + launch(start = CoroutineStart.UNDISPATCHED) { + repository.receiverListFlow.collect { emissions.send(it) } + } + + try { + withTimeout(TEST_TIMEOUT_MILLIS) { emissions.receive() } + + val created = + repository.createReceiver( + name = "새 수신자", + relation = "친구", + phone = null, + email = "receiver@example.com", + message = null, + ) + assertEquals(ReceiverCreated(receiverId = 2L, authCode = "AUTH-2"), created) + assertEquals( + listOf("계정 A"), + withTimeout(TEST_TIMEOUT_MILLIS) { emissions.receive() }.map { it.name }, + ) + + val recovered = withTimeout(TEST_TIMEOUT_MILLIS) { repository.receiverListFlow.first() } + + assertEquals("복구된 목록", recovered.single().name) + assertEquals(1, postCount) + assertEquals(3, getCount) + } finally { + collector.cancelAndJoin() + } + } + + /** + * #709 회귀 — 목록을 아무도 구독하지 않는 상태에서도 등록은 조회 결과에 매이지 않는다. + * 옛 구현은 구독자와 무관하게 등록 직후 조회를 직접 불러, 그 실패가 그대로 등록 실패가 됐다. + */ + @Test + fun `createReceiver - 목록 구독자가 없으면 조회 실패와 무관하게 등록 결과를 돌려준다`() { + var getCount = 0 + var postCount = 0 + val repository = + repository( + onGetReceivers = { + getCount += 1 + throw UnknownHostException("등록 직후 갱신 조회 실패") + }, + onCreateReceiver = { + postCount += 1 + dataResponse(UserCreateReceiverDto(receiverId = 2L, authCode = "AUTH-2")) + }, + ) + + val created = + runBlocking { + repository.createReceiver( + name = "새 수신자", + relation = "친구", + phone = null, + email = "receiver@example.com", + message = null, + ) + } + + assertEquals(ReceiverCreated(receiverId = 2L, authCode = "AUTH-2"), created) + assertEquals(1, postCount) + assertEquals(0, getCount) + } + private fun receiverAuthRepository(loggedIn: Boolean): FakeAuthRepository = FakeAuthRepository.strict(loggedIn = loggedIn).apply { onIsLoggedIn = { loggedInState }