feat(dm): WebSocket 계약 모델을 추가한다
This commit is contained in:
@@ -0,0 +1,115 @@
|
||||
package kr.co.vividnext.sodalive.v2.main.chat.dm.data
|
||||
|
||||
import androidx.annotation.Keep
|
||||
import com.google.gson.Gson
|
||||
import com.google.gson.JsonObject
|
||||
import com.google.gson.JsonSyntaxException
|
||||
import com.google.gson.annotations.SerializedName
|
||||
|
||||
@Keep
|
||||
data class DmChatSocketEnvelope(
|
||||
@SerializedName("type") val type: String,
|
||||
@SerializedName("payload") val payload: JsonObject?
|
||||
)
|
||||
|
||||
@Keep
|
||||
data class DmChatSocketRoomPayload(
|
||||
@SerializedName("roomId") val roomId: Long
|
||||
)
|
||||
|
||||
@Keep
|
||||
data class DmChatSocketSendTextPayload(
|
||||
@SerializedName("roomId") val roomId: Long,
|
||||
@SerializedName("requestId") val requestId: String,
|
||||
@SerializedName("textMessage") val textMessage: String
|
||||
)
|
||||
|
||||
@Keep
|
||||
data class DmChatSocketMessagePayload(
|
||||
@SerializedName("message") val message: DmChatMessageResponse
|
||||
)
|
||||
|
||||
@Keep
|
||||
data class DmChatSocketSendAckPayload(
|
||||
@SerializedName("requestId") val requestId: String,
|
||||
@SerializedName("message") val message: DmChatMessageResponse
|
||||
)
|
||||
|
||||
@Keep
|
||||
data class DmChatSocketErrorPayload(
|
||||
@SerializedName("requestId") val requestId: String?,
|
||||
@SerializedName("code") val code: String?,
|
||||
@SerializedName("message") val message: String?
|
||||
)
|
||||
|
||||
enum class DmChatSocketClientType(val value: String) {
|
||||
JOIN_ROOM("JOIN_ROOM"),
|
||||
LEAVE_ROOM("LEAVE_ROOM"),
|
||||
SEND_TEXT("SEND_TEXT"),
|
||||
PING("PING")
|
||||
}
|
||||
|
||||
sealed class DmChatSocketEvent {
|
||||
data object Joined : DmChatSocketEvent()
|
||||
data class Message(val message: DmChatMessageResponse) : DmChatSocketEvent()
|
||||
data class SendAck(
|
||||
val requestId: String,
|
||||
val message: DmChatMessageResponse
|
||||
) : DmChatSocketEvent()
|
||||
data class Error(
|
||||
val requestId: String?,
|
||||
val code: String?,
|
||||
val message: String?
|
||||
) : DmChatSocketEvent()
|
||||
data object Pong : DmChatSocketEvent()
|
||||
}
|
||||
|
||||
class DmChatSocketParser(private val gson: Gson) {
|
||||
fun parse(text: String): DmChatSocketEvent? = try {
|
||||
val envelope = gson.fromJson(text, DmChatSocketEnvelope::class.java)
|
||||
when (envelope.type) {
|
||||
TYPE_JOINED -> DmChatSocketEvent.Joined
|
||||
TYPE_MESSAGE -> parseMessage(envelope.payload)
|
||||
TYPE_SEND_ACK -> parseSendAck(envelope.payload)
|
||||
TYPE_ERROR -> parseError(envelope.payload)
|
||||
TYPE_PONG -> DmChatSocketEvent.Pong
|
||||
else -> null
|
||||
}
|
||||
} catch (e: JsonSyntaxException) {
|
||||
null
|
||||
} catch (e: IllegalStateException) {
|
||||
null
|
||||
} catch (e: NullPointerException) {
|
||||
null
|
||||
}
|
||||
|
||||
private fun parseMessage(payload: JsonObject?): DmChatSocketEvent.Message? {
|
||||
val messagePayload = gson.fromJson(payload, DmChatSocketMessagePayload::class.java) ?: return null
|
||||
return DmChatSocketEvent.Message(messagePayload.message)
|
||||
}
|
||||
|
||||
private fun parseSendAck(payload: JsonObject?): DmChatSocketEvent.SendAck? {
|
||||
val ackPayload = gson.fromJson(payload, DmChatSocketSendAckPayload::class.java) ?: return null
|
||||
return DmChatSocketEvent.SendAck(
|
||||
requestId = ackPayload.requestId,
|
||||
message = ackPayload.message
|
||||
)
|
||||
}
|
||||
|
||||
private fun parseError(payload: JsonObject?): DmChatSocketEvent.Error? {
|
||||
val errorPayload = gson.fromJson(payload, DmChatSocketErrorPayload::class.java) ?: return null
|
||||
return DmChatSocketEvent.Error(
|
||||
requestId = errorPayload.requestId,
|
||||
code = errorPayload.code,
|
||||
message = errorPayload.message
|
||||
)
|
||||
}
|
||||
|
||||
private companion object {
|
||||
const val TYPE_JOINED = "JOINED"
|
||||
const val TYPE_MESSAGE = "MESSAGE"
|
||||
const val TYPE_SEND_ACK = "SEND_ACK"
|
||||
const val TYPE_ERROR = "ERROR"
|
||||
const val TYPE_PONG = "PONG"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,168 @@
|
||||
package kr.co.vividnext.sodalive.v2.main.chat.dm
|
||||
|
||||
import com.google.gson.Gson
|
||||
import kr.co.vividnext.sodalive.v2.main.chat.dm.data.DmChatMessageResponse
|
||||
import kr.co.vividnext.sodalive.v2.main.chat.dm.data.DmChatSocketEvent
|
||||
import kr.co.vividnext.sodalive.v2.main.chat.dm.data.DmChatSocketParser
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertNull
|
||||
import org.junit.Test
|
||||
|
||||
class DmChatSocketParserTest {
|
||||
|
||||
private val parser = DmChatSocketParser(Gson())
|
||||
|
||||
@Test
|
||||
fun `JOINED type은 joined event로 파싱된다`() {
|
||||
val event = parser.parse(
|
||||
"""
|
||||
{
|
||||
"type": "JOINED",
|
||||
"payload": { "roomId": 10 }
|
||||
}
|
||||
""".trimIndent()
|
||||
)
|
||||
|
||||
assertEquals(DmChatSocketEvent.Joined, event)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `MESSAGE type은 DM 메시지 payload로 파싱된다`() {
|
||||
val event = parser.parse(
|
||||
"""
|
||||
{
|
||||
"type": "MESSAGE",
|
||||
"payload": { "message": ${messageJson()} }
|
||||
}
|
||||
""".trimIndent()
|
||||
)
|
||||
|
||||
val message = requireMessage(event)
|
||||
assertEquals(10L, message.messageId)
|
||||
assertEquals("안녕하세요", message.textMessage)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `SEND_ACK type은 requestId와 서버 확정 메시지로 파싱된다`() {
|
||||
val event = parser.parse(
|
||||
"""
|
||||
{
|
||||
"type": "SEND_ACK",
|
||||
"payload": {
|
||||
"requestId": "request-1",
|
||||
"message": ${messageJson(messageId = 11L, textMessage = "확정")}
|
||||
}
|
||||
}
|
||||
""".trimIndent()
|
||||
)
|
||||
|
||||
val ack = event as? DmChatSocketEvent.SendAck
|
||||
requireNotNull(ack)
|
||||
assertEquals("request-1", ack.requestId)
|
||||
assertEquals(11L, ack.message.messageId)
|
||||
assertEquals("확정", ack.message.textMessage)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `ERROR type은 nullable requestId와 code message를 보존한다`() {
|
||||
val event = parser.parse(
|
||||
"""
|
||||
{
|
||||
"type": "ERROR",
|
||||
"payload": {
|
||||
"requestId": null,
|
||||
"code": "INVALID_MESSAGE",
|
||||
"message": "메시지를 전송할 수 없습니다"
|
||||
}
|
||||
}
|
||||
""".trimIndent()
|
||||
)
|
||||
|
||||
val error = event as? DmChatSocketEvent.Error
|
||||
requireNotNull(error)
|
||||
assertNull(error.requestId)
|
||||
assertEquals("INVALID_MESSAGE", error.code)
|
||||
assertEquals("메시지를 전송할 수 없습니다", error.message)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `ERROR type은 nullable code와 message를 보존한다`() {
|
||||
val event = parser.parse(
|
||||
"""
|
||||
{
|
||||
"type": "ERROR",
|
||||
"payload": {
|
||||
"requestId": "request-1",
|
||||
"code": null,
|
||||
"message": null
|
||||
}
|
||||
}
|
||||
""".trimIndent()
|
||||
)
|
||||
|
||||
val error = event as? DmChatSocketEvent.Error
|
||||
requireNotNull(error)
|
||||
assertEquals("request-1", error.requestId)
|
||||
assertNull(error.code)
|
||||
assertNull(error.message)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `PONG type은 pong event로 파싱된다`() {
|
||||
val event = parser.parse(
|
||||
"""
|
||||
{
|
||||
"type": "PONG",
|
||||
"payload": {}
|
||||
}
|
||||
""".trimIndent()
|
||||
)
|
||||
|
||||
assertEquals(DmChatSocketEvent.Pong, event)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `알 수 없는 type은 null로 무시된다`() {
|
||||
val event = parser.parse(
|
||||
"""
|
||||
{
|
||||
"type": "UNKNOWN",
|
||||
"payload": {}
|
||||
}
|
||||
""".trimIndent()
|
||||
)
|
||||
|
||||
assertNull(event)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `잘못된 JSON은 null로 무시된다`() {
|
||||
val event = parser.parse("{not-json}")
|
||||
|
||||
assertNull(event)
|
||||
}
|
||||
|
||||
private fun requireMessage(event: DmChatSocketEvent?): DmChatMessageResponse {
|
||||
val messageEvent = event as? DmChatSocketEvent.Message
|
||||
requireNotNull(messageEvent)
|
||||
return messageEvent.message
|
||||
}
|
||||
|
||||
private fun messageJson(
|
||||
messageId: Long = 10L,
|
||||
textMessage: String = "안녕하세요"
|
||||
): String =
|
||||
"""
|
||||
{
|
||||
"messageId": $messageId,
|
||||
"messageType": "TEXT",
|
||||
"mine": false,
|
||||
"createdAt": 1000,
|
||||
"textMessage": "$textMessage",
|
||||
"voiceMessageUrl": null,
|
||||
"senderId": 20,
|
||||
"senderNickname": "크리에이터",
|
||||
"senderProfileImageUrl": "https://example.com/profile.png"
|
||||
}
|
||||
""".trimIndent().replace("\n", "")
|
||||
}
|
||||
Reference in New Issue
Block a user