diff --git a/app/src/main/java/kr/co/vividnext/sodalive/di/AppDI.kt b/app/src/main/java/kr/co/vividnext/sodalive/di/AppDI.kt index 19b1d509..829070f8 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/di/AppDI.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/di/AppDI.kt @@ -186,9 +186,8 @@ 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.dm.DmChatRoomViewModel import kr.co.vividnext.sodalive.v2.main.chat.dm.data.DmChatApi -import kr.co.vividnext.sodalive.v2.main.chat.dm.data.DmChatEventClient -import kr.co.vividnext.sodalive.v2.main.chat.dm.data.DmChatRealtimeClient import kr.co.vividnext.sodalive.v2.main.chat.dm.data.DmChatRepository +import kr.co.vividnext.sodalive.v2.main.chat.dm.data.DmChatSocketClient import kr.co.vividnext.sodalive.v2.main.home.HomeCreatorRankingViewModel import kr.co.vividnext.sodalive.v2.main.home.HomeRecommendationViewModel import kr.co.vividnext.sodalive.v2.main.home.data.HomeCreatorRankingApi @@ -313,7 +312,7 @@ class AppDI(private val context: Context, isDebugMode: Boolean) { single { ApiBuilder().build(get(), CharacterCommentApi::class.java) } single { ApiBuilder().build(get(), OriginalWorkApi::class.java) } single { ApiBuilder().build(get(named("agoraRetrofit")), V2vApi::class.java) } - single { DmChatEventClient(okHttpClient = get(), gson = get(), baseUrl = baseUrl) } + single { DmChatSocketClient(okHttpClient = get(), gson = get(), baseUrl = baseUrl) } } private val viewModelModule = module { @@ -462,7 +461,7 @@ class AppDI(private val context: Context, isDebugMode: Boolean) { factory { PointStatusRepository(get()) } factory { HomeRepository(get()) } factory { ChatRoomRepository(get()) } - factory { DmChatRepository(api = get(), realtimeClient = get()) } + factory { DmChatRepository(api = get(), socketClient = get()) } factory { HomeCreatorRankingRepository(get()) } factory { HomeRecommendationRepository(get()) } factory { diff --git a/app/src/main/java/kr/co/vividnext/sodalive/v2/main/chat/dm/data/DmChatApi.kt b/app/src/main/java/kr/co/vividnext/sodalive/v2/main/chat/dm/data/DmChatApi.kt index a5425220..c4657c5a 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/v2/main/chat/dm/data/DmChatApi.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/v2/main/chat/dm/data/DmChatApi.kt @@ -30,17 +30,4 @@ interface DmChatApi { @Query("cursor") cursor: Long?, @Query("limit") limit: Int = 20 ): Single> - - @POST("/api/v2/user-creator-chat/rooms/{roomId}/messages/text") - fun sendDmTextMessage( - @Header("Authorization") authHeader: String, - @Path("roomId") roomId: Long, - @Body request: SendDmTextMessageRequest - ): Single> - - @POST("/api/v2/user-creator-chat/rooms/{roomId}/events/disconnect") - fun disconnectRealtime( - @Header("Authorization") authHeader: String, - @Path("roomId") roomId: Long - ): Single> } diff --git a/app/src/main/java/kr/co/vividnext/sodalive/v2/main/chat/dm/data/DmChatModels.kt b/app/src/main/java/kr/co/vividnext/sodalive/v2/main/chat/dm/data/DmChatModels.kt index c8acc89a..a6f38a30 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/v2/main/chat/dm/data/DmChatModels.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/v2/main/chat/dm/data/DmChatModels.kt @@ -42,15 +42,3 @@ data class DmChatMessageResponse( @SerializedName("senderNickname") val senderNickname: String, @SerializedName("senderProfileImageUrl") val senderProfileImageUrl: String ) - -@Keep -data class SendDmTextMessageRequest( - @SerializedName("textMessage") val textMessage: String -) - -@Keep -data class SendDmChatMessageResponse( - @SerializedName("message") val message: DmChatMessageResponse, - @SerializedName("deliveredRealtime") val deliveredRealtime: Boolean, - @SerializedName("pushSent") val pushSent: Boolean -) diff --git a/app/src/main/java/kr/co/vividnext/sodalive/v2/main/chat/dm/data/DmChatRepository.kt b/app/src/main/java/kr/co/vividnext/sodalive/v2/main/chat/dm/data/DmChatRepository.kt index e4174e85..8d68cfea 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/v2/main/chat/dm/data/DmChatRepository.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/v2/main/chat/dm/data/DmChatRepository.kt @@ -5,7 +5,7 @@ import kr.co.vividnext.sodalive.common.ApiResponse class DmChatRepository( private val api: DmChatApi, - private val realtimeClient: DmChatRealtimeClient? = null + private val socketClient: DmChatSocketClient? = null ) { fun createOrGetRoom( token: String, @@ -37,34 +37,31 @@ class DmChatRepository( limit = limit ) - fun sendTextMessage( + fun connectSocket( token: String, - roomId: Long, - textMessage: String - ): Single> = api.sendDmTextMessage( - authHeader = bearer(token), - roomId = roomId, - request = SendDmTextMessageRequest(textMessage = textMessage) - ) - - fun disconnectRealtime( - token: String, - roomId: Long - ): Single> = api.disconnectRealtime( - authHeader = bearer(token), - roomId = roomId - ) - - fun connectRealtime( - token: String, - roomId: Long, - listener: DmChatEventClient.Listener + listener: DmChatSocketClient.Listener ) { - realtimeClient?.connect(token = token, roomId = roomId, listener = listener) + socketClient?.connect(token = token, listener = listener) } - fun cancelRealtime() { - realtimeClient?.cancel() + fun sendJoinRoom(roomId: Long): Boolean = socketClient?.sendJoinRoom(roomId = roomId) ?: false + + fun sendLeaveRoom(roomId: Long): Boolean = socketClient?.sendLeaveRoom(roomId = roomId) ?: false + + fun sendSocketText( + roomId: Long, + requestId: String, + textMessage: String + ): Boolean = socketClient?.sendText( + roomId = roomId, + requestId = requestId, + textMessage = textMessage + ) ?: false + + fun sendPing(): Boolean = socketClient?.sendPing() ?: false + + fun closeSocket() { + socketClient?.close() } private fun bearer(token: String) = "Bearer $token" diff --git a/app/src/test/java/kr/co/vividnext/sodalive/v2/main/chat/dm/DmChatRepositoryTest.kt b/app/src/test/java/kr/co/vividnext/sodalive/v2/main/chat/dm/DmChatRepositoryTest.kt index a423274b..afd65f47 100644 --- a/app/src/test/java/kr/co/vividnext/sodalive/v2/main/chat/dm/DmChatRepositoryTest.kt +++ b/app/src/test/java/kr/co/vividnext/sodalive/v2/main/chat/dm/DmChatRepositoryTest.kt @@ -5,20 +5,32 @@ import kr.co.vividnext.sodalive.common.ApiResponse import kr.co.vividnext.sodalive.v2.main.chat.dm.data.CreateDmChatRoomRequest import kr.co.vividnext.sodalive.v2.main.chat.dm.data.CreateDmChatRoomResponse import kr.co.vividnext.sodalive.v2.main.chat.dm.data.DmChatApi -import kr.co.vividnext.sodalive.v2.main.chat.dm.data.DmChatMessageResponse import kr.co.vividnext.sodalive.v2.main.chat.dm.data.DmChatMessagesPageResponse import kr.co.vividnext.sodalive.v2.main.chat.dm.data.DmChatRepository import kr.co.vividnext.sodalive.v2.main.chat.dm.data.DmChatRoomOpenResponse -import kr.co.vividnext.sodalive.v2.main.chat.dm.data.SendDmChatMessageResponse -import kr.co.vividnext.sodalive.v2.main.chat.dm.data.SendDmTextMessageRequest +import kr.co.vividnext.sodalive.v2.main.chat.dm.data.DmChatSocketClient +import kr.co.vividnext.sodalive.v2.main.chat.dm.data.DmChatSocketEvent +import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.WebSocket +import okhttp3.WebSocketListener +import okio.ByteString import org.junit.Assert.assertEquals -import org.junit.Assert.assertNull +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue import org.junit.Test class DmChatRepositoryTest { private val api = FakeDmChatApi() - private val repository = DmChatRepository(api) + private val socketFactory = FakeWebSocketFactory() + private val socketClient = DmChatSocketClient( + okHttpClient = OkHttpClient(), + gson = com.google.gson.Gson(), + baseUrl = "https://api.example.com", + webSocketFactory = socketFactory::newWebSocket + ) + private val repository = DmChatRepository(api = api, socketClient = socketClient) @Test fun `createOrGetRoom은 bearer header와 creatorId를 API에 위임한다`() { @@ -48,27 +60,38 @@ class DmChatRepositoryTest { } @Test - fun `sendTextMessage는 textMessage request를 API에 위임한다`() { - repository.sendTextMessage(token = "test-token", roomId = 12L, textMessage = "안녕").blockingGet() + fun `connectSocket은 bearer header와 listener를 socket client에 위임한다`() { + repository.connectSocket(token = "test-token", listener = TestListener()) - assertEquals("Bearer test-token", api.lastAuthHeader) - assertEquals(12L, api.lastRoomId) - assertEquals(SendDmTextMessageRequest(textMessage = "안녕"), api.lastSendRequest) + assertEquals("Bearer test-token", socketFactory.request?.header("Authorization")) + assertEquals("wss://api.example.com/ws/v2/user-creator-chat", socketFactory.request?.tag(String::class.java)) } @Test - fun `disconnectRealtime은 bearer header와 roomId를 API에 위임한다`() { - repository.disconnectRealtime(token = "test-token", roomId = 12L).blockingGet() + fun `socket send 메서드는 socket client envelope 전송에 위임한다`() { + repository.connectSocket(token = "test-token", listener = TestListener()) - assertEquals("Bearer test-token", api.lastAuthHeader) - assertEquals(12L, api.lastRoomId) - assertNull(api.lastSendRequest) + assertTrue(repository.sendJoinRoom(roomId = 12L)) + assertTrue(repository.sendLeaveRoom(roomId = 12L)) + assertTrue(repository.sendSocketText(roomId = 12L, requestId = "request-1", textMessage = "안녕")) + assertTrue(repository.sendPing()) + + assertEquals(listOf("JOIN_ROOM", "LEAVE_ROOM", "SEND_TEXT", "PING"), socketFactory.webSocket.sentTypes()) + } + + @Test + fun `closeSocket은 socket close에 위임하고 미연결 전송은 false를 반환한다`() { + repository.connectSocket(token = "test-token", listener = TestListener()) + + repository.closeSocket() + + assertEquals(1, socketFactory.webSocket.closeCount) + assertFalse(repository.sendPing()) } private class FakeDmChatApi : DmChatApi { var lastAuthHeader: String? = null var lastCreateRequest: CreateDmChatRoomRequest? = null - var lastSendRequest: SendDmTextMessageRequest? = null var lastRoomId: Long? = null var lastCursor: Long? = null var lastLimit: Int? = null @@ -126,46 +149,47 @@ class DmChatRepositoryTest { ) ) } + } - override fun sendDmTextMessage( - authHeader: String, - roomId: Long, - request: SendDmTextMessageRequest - ): Single> { - lastAuthHeader = authHeader - lastRoomId = roomId - lastSendRequest = request - return Single.just( - ApiResponse( - success = true, - data = SendDmChatMessageResponse( - message = message(), - deliveredRealtime = true, - pushSent = false - ) - ) - ) + private open class TestListener : DmChatSocketClient.Listener { + override fun onEvent(event: DmChatSocketEvent) = Unit + override fun onFailure(throwable: Throwable) = Unit + } + + private class FakeWebSocketFactory { + val webSocket = FakeWebSocket() + var request: Request? = null + + fun newWebSocket(request: Request, listener: WebSocketListener): WebSocket { + this.request = request + return webSocket + } + } + + private class FakeWebSocket : WebSocket { + val sentTexts = mutableListOf() + var closeCount = 0 + + override fun request(): Request = Request.Builder().url("wss://example.com").build() + + override fun queueSize(): Long = 0L + + override fun send(text: String): Boolean { + sentTexts += text + return true } - override fun disconnectRealtime( - authHeader: String, - roomId: Long - ): Single> { - lastAuthHeader = authHeader - lastRoomId = roomId - return Single.just(ApiResponse(success = true, data = true)) + override fun send(bytes: ByteString): Boolean = true + + override fun close(code: Int, reason: String?): Boolean { + closeCount += 1 + return true } - private fun message() = DmChatMessageResponse( - messageId = 1L, - messageType = "TEXT", - mine = true, - createdAt = 1000L, - textMessage = "안녕", - voiceMessageUrl = null, - senderId = 2L, - senderNickname = "나", - senderProfileImageUrl = "https://example.com/profile.png" - ) + override fun cancel() = Unit + + fun sentTypes(): List = sentTexts.map { text -> + com.google.gson.JsonParser.parseString(text).asJsonObject.get("type").asString + } } }