Skip to content

Commit 77b2ba0

Browse files
VelikovPetarclaude
andcommitted
fix: omit non-allowed message.type values on send/update wire payload
Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 00be390 commit 77b2ba0

5 files changed

Lines changed: 81 additions & 4 deletions

File tree

stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api2/MoshiChatApi.kt

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,7 @@ import io.getstream.chat.android.models.Member
132132
import io.getstream.chat.android.models.MemberData
133133
import io.getstream.chat.android.models.Message
134134
import io.getstream.chat.android.models.MessageReminder
135+
import io.getstream.chat.android.models.MessageType
135136
import io.getstream.chat.android.models.Mute
136137
import io.getstream.chat.android.models.PendingMessage
137138
import io.getstream.chat.android.models.Poll
@@ -252,7 +253,7 @@ constructor(
252253
channelType = channelType,
253254
channelId = channelId,
254255
message = SendMessageRequest(
255-
message = with(dtoMapping) { message.toDto() },
256+
message = with(dtoMapping) { message.toDto(messageType = message.uploadMessageType) },
256257
skip_push = message.skipPushNotification,
257258
skip_enrich_url = message.skipEnrichUrl,
258259
),
@@ -323,7 +324,7 @@ constructor(
323324
return messageApi.updateMessage(
324325
messageId = message.id,
325326
message = UpdateMessageRequest(
326-
message = with(dtoMapping) { message.toDto() },
327+
message = with(dtoMapping) { message.toDto(messageType = message.uploadMessageType) },
327328
skip_enrich_url = message.skipEnrichUrl,
328329
skip_push = message.skipPushNotification,
329330
),
@@ -1815,4 +1816,19 @@ constructor(
18151816

18161817
private fun <T : Any, R : Any> RetrofitCall<T>.flatMapDomain(transform: DomainMapping.(T) -> Call<R>): Call<R> =
18171818
flatMap { domainMapping.transform(it) }
1819+
1820+
/**
1821+
* Value to send for `message.type` on outbound send/update requests.
1822+
*
1823+
* The backend's UpdateMessage endpoint validates `type` against `oneof='' regular system`,
1824+
* so server-assigned types like `reply`, `error`, or `ephemeral` would be rejected with a 400
1825+
* if echoed back. Coerce anything outside the accepted set to the empty string — the field is
1826+
* ignored by the backend on update anyway, and on send only `regular` and `system` are
1827+
* meaningful for clients to declare.
1828+
*/
1829+
private val Message.uploadMessageType: String
1830+
get() = when (type) {
1831+
MessageType.REGULAR, MessageType.SYSTEM -> type
1832+
else -> ""
1833+
}
18181834
}

stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api2/mapping/DtoMapping.kt

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ import io.getstream.chat.android.models.Member
4343
import io.getstream.chat.android.models.MemberData
4444
import io.getstream.chat.android.models.Message
4545
import io.getstream.chat.android.models.MessageTransformer
46+
import io.getstream.chat.android.models.MessageType
4647
import io.getstream.chat.android.models.Mute
4748
import io.getstream.chat.android.models.Reaction
4849
import io.getstream.chat.android.models.User
@@ -117,8 +118,13 @@ internal class DtoMapping(
117118

118119
/**
119120
* Transforms [Message] to [UpstreamMessageDto].
121+
*
122+
* @param messageType Value to use for [UpstreamMessageDto.type]. When `null` (the default),
123+
* the message's own type is used. Callers in the network layer may pass an explicit value to
124+
* satisfy server-side validation — for example, the UpdateMessage endpoint only accepts the
125+
* empty string, [MessageType.REGULAR], or [MessageType.SYSTEM].
120126
*/
121-
internal fun Message.toDto(): UpstreamMessageDto =
127+
internal fun Message.toDto(messageType: String? = null): UpstreamMessageDto =
122128
messageTransformer.transform(this)
123129
.run {
124130
UpstreamMessageDto(
@@ -127,7 +133,7 @@ internal class DtoMapping(
127133
command = command,
128134
html = html,
129135
id = id,
130-
type = type,
136+
type = messageType ?: type,
131137
mentioned_users = mentionedUsersIds,
132138
parent_id = parentId,
133139
pin_expires = pinExpires,

stream-chat-android-client/src/test/java/io/getstream/chat/android/client/api2/MoshiChatApiTest.kt

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,12 +72,14 @@ import io.getstream.chat.android.client.api2.model.requests.RejectInviteRequest
7272
import io.getstream.chat.android.client.api2.model.requests.ReminderRequest
7373
import io.getstream.chat.android.client.api2.model.requests.SendActionRequest
7474
import io.getstream.chat.android.client.api2.model.requests.SendEventRequest
75+
import io.getstream.chat.android.client.api2.model.requests.SendMessageRequest
7576
import io.getstream.chat.android.client.api2.model.requests.UnblockUserRequest
7677
import io.getstream.chat.android.client.api2.model.requests.UpdateChannelPartialRequest
7778
import io.getstream.chat.android.client.api2.model.requests.UpdateCooldownRequest
7879
import io.getstream.chat.android.client.api2.model.requests.UpdateLiveLocationRequest
7980
import io.getstream.chat.android.client.api2.model.requests.UpdateMemberPartialRequest
8081
import io.getstream.chat.android.client.api2.model.requests.UpdateMemberPartialResponse
82+
import io.getstream.chat.android.client.api2.model.requests.UpdateMessageRequest
8183
import io.getstream.chat.android.client.api2.model.requests.UpsertPushPreferencesRequest
8284
import io.getstream.chat.android.client.api2.model.requests.UpstreamOptionDto
8385
import io.getstream.chat.android.client.api2.model.requests.UpstreamVoteDto
@@ -180,6 +182,7 @@ import org.junit.jupiter.params.ParameterizedTest
180182
import org.junit.jupiter.params.provider.MethodSource
181183
import org.mockito.kotlin.any
182184
import org.mockito.kotlin.anyOrNull
185+
import org.mockito.kotlin.argumentCaptor
183186
import org.mockito.kotlin.doReturn
184187
import org.mockito.kotlin.eq
185188
import org.mockito.kotlin.mock
@@ -314,6 +317,38 @@ internal class MoshiChatApiTest {
314317
verify(api, times(1)).updateMessage(eq(message.id), any())
315318
}
316319

320+
@ParameterizedTest
321+
@MethodSource("io.getstream.chat.android.client.api2.MoshiChatApiTestArguments#uploadMessageTypeInput")
322+
fun testSendMessageCoercesType(inputType: String, expectedWireType: String) = runTest {
323+
val api = mock<MessageApi>()
324+
val call = RetroSuccess(MessageResponse(message = Mother.randomDownstreamMessageDto())).toRetrofitCall()
325+
whenever(api.sendMessage(any(), any(), any())).doReturn(call)
326+
val sut = Fixture().withMessageApi(api).get()
327+
val message = randomMessage(type = inputType)
328+
329+
sut.sendMessage(randomString(), randomString(), message).await()
330+
331+
val captor = argumentCaptor<SendMessageRequest>()
332+
verify(api).sendMessage(any(), any(), captor.capture())
333+
assertEquals(expectedWireType, captor.firstValue.message.type)
334+
}
335+
336+
@ParameterizedTest
337+
@MethodSource("io.getstream.chat.android.client.api2.MoshiChatApiTestArguments#uploadMessageTypeInput")
338+
fun testUpdateMessageCoercesType(inputType: String, expectedWireType: String) = runTest {
339+
val api = mock<MessageApi>()
340+
val call = RetroSuccess(MessageResponse(message = Mother.randomDownstreamMessageDto())).toRetrofitCall()
341+
whenever(api.updateMessage(any(), any())).doReturn(call)
342+
val sut = Fixture().withMessageApi(api).get()
343+
val message = randomMessage(type = inputType)
344+
345+
sut.updateMessage(message).await()
346+
347+
val captor = argumentCaptor<UpdateMessageRequest>()
348+
verify(api).updateMessage(eq(message.id), captor.capture())
349+
assertEquals(expectedWireType, captor.firstValue.message.type)
350+
}
351+
317352
@ParameterizedTest
318353
@MethodSource("io.getstream.chat.android.client.api2.MoshiChatApiTestArguments#partialUpdateMessageInput")
319354
fun testPartialUpdateMessage(call: RetrofitCall<MessageResponse>, expected: KClass<*>) = runTest {

stream-chat-android-client/src/test/java/io/getstream/chat/android/client/api2/MoshiChatApiTestArguments.kt

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ import io.getstream.chat.android.client.api2.model.response.UsersResponse
6666
import io.getstream.chat.android.client.utils.RetroError
6767
import io.getstream.chat.android.client.utils.RetroSuccess
6868
import io.getstream.chat.android.models.EventType
69+
import io.getstream.chat.android.models.MessageType
6970
import io.getstream.chat.android.models.QueryRemindersResult
7071
import io.getstream.chat.android.models.UnreadChannel
7172
import io.getstream.chat.android.models.UnreadChannelByType
@@ -111,6 +112,15 @@ internal object MoshiChatApiTestArguments {
111112
@JvmStatic
112113
fun updateMessageInput() = messageResponseArguments()
113114

115+
@JvmStatic
116+
fun uploadMessageTypeInput() = listOf(
117+
Arguments.of(MessageType.REGULAR, MessageType.REGULAR),
118+
Arguments.of(MessageType.SYSTEM, MessageType.SYSTEM),
119+
Arguments.of(MessageType.REPLY, ""),
120+
Arguments.of(MessageType.ERROR, ""),
121+
Arguments.of(MessageType.EPHEMERAL, ""),
122+
)
123+
114124
@JvmStatic
115125
fun partialUpdateMessageInput() = messageResponseArguments()
116126

stream-chat-android-client/src/test/java/io/getstream/chat/android/client/api2/mapping/DtoMappingTest.kt

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,16 @@ internal class DtoMappingTest {
201201
verify(messageTransformer, times(1)).transform(message)
202202
}
203203

204+
@Test
205+
fun `Message toDto uses messageType parameter when provided`() {
206+
val message = randomMessage(type = "reply")
207+
val mapping = Fixture().get()
208+
209+
val dto = with(mapping) { message.toDto(messageType = "") }
210+
211+
dto.type shouldBeEqualTo ""
212+
}
213+
204214
@Test
205215
fun `Mute is correctly mapped to Dto`() {
206216
val mute = randomMute()

0 commit comments

Comments
 (0)