diff --git a/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api2/endpoint/GeneralApi.kt b/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api2/endpoint/GeneralApi.kt index 8b66de30052..63c3852dea3 100644 --- a/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api2/endpoint/GeneralApi.kt +++ b/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api2/endpoint/GeneralApi.kt @@ -21,10 +21,10 @@ import io.getstream.chat.android.client.api.QueryParams import io.getstream.chat.android.client.api2.UrlQueryPayload import io.getstream.chat.android.client.api2.model.requests.QueryMembersRequest import io.getstream.chat.android.client.api2.model.requests.SyncHistoryRequest -import io.getstream.chat.android.client.api2.model.response.QueryMembersResponse import io.getstream.chat.android.client.api2.model.response.SearchMessagesResponse import io.getstream.chat.android.client.api2.model.response.SyncHistoryResponse import io.getstream.chat.android.client.call.RetrofitCall +import io.getstream.chat.android.network.models.MembersResponse import io.getstream.chat.android.network.models.SearchPayload import io.getstream.chat.android.network.models.WrappedUnreadCountsResponse import okhttp3.ResponseBody @@ -53,7 +53,7 @@ internal interface GeneralApi { @GET("/members") fun queryMembers( @UrlQueryPayload @Query("payload") payload: QueryMembersRequest, - ): RetrofitCall + ): RetrofitCall @GET("/unread") fun getUnreadCounts(): RetrofitCall diff --git a/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api2/model/response/QueryMembersResponse.kt b/stream-chat-android-client/src/main/java/io/getstream/chat/android/network/models/MembersResponse.kt similarity index 55% rename from stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api2/model/response/QueryMembersResponse.kt rename to stream-chat-android-client/src/main/java/io/getstream/chat/android/network/models/MembersResponse.kt index d4434720448..43bda4078c3 100644 --- a/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api2/model/response/QueryMembersResponse.kt +++ b/stream-chat-android-client/src/main/java/io/getstream/chat/android/network/models/MembersResponse.kt @@ -14,12 +14,25 @@ * limitations under the License. */ -package io.getstream.chat.android.client.api2.model.response +@file:Suppress( + "ArrayInDataClass", + "EnumEntryName", + "RemoveRedundantQualifierName", + "UnusedImport", +) + +package io.getstream.chat.android.network.models + +import com.squareup.moshi.Json -import com.squareup.moshi.JsonClass -import io.getstream.chat.android.client.api2.model.dto.DownstreamMemberDto +/** + * + */ +@com.squareup.moshi.JsonClass(generateAdapter = true) +internal data class MembersResponse( + @Json(name = "duration") + internal val duration: String, -@JsonClass(generateAdapter = true) -internal data class QueryMembersResponse( - val members: List, + @Json(name = "members") + internal val members: List = emptyList(), ) diff --git a/stream-chat-android-client/src/test/java/io/getstream/chat/android/client/Mother.kt b/stream-chat-android-client/src/test/java/io/getstream/chat/android/client/Mother.kt index 3ad193aa658..c3c25707dd2 100644 --- a/stream-chat-android-client/src/test/java/io/getstream/chat/android/client/Mother.kt +++ b/stream-chat-android-client/src/test/java/io/getstream/chat/android/client/Mother.kt @@ -92,6 +92,7 @@ import io.getstream.chat.android.models.querysort.QuerySorter import io.getstream.chat.android.network.models.AppResponseFields import io.getstream.chat.android.network.models.BlockUsersResponse import io.getstream.chat.android.network.models.BlockedUserResponse +import io.getstream.chat.android.network.models.ChannelMemberResponse import io.getstream.chat.android.network.models.ChannelResponse import io.getstream.chat.android.network.models.CreateGuestResponse import io.getstream.chat.android.network.models.DeviceResponse @@ -728,6 +729,34 @@ internal object Mother { last_reaction_at = lastReactionAt, ) + fun randomUserResponse( + id: String = randomString(), + role: String = randomString(), + language: String = randomString(), + ): UserResponse = UserResponse( + id = id, + role = role, + language = language, + banned = randomBoolean(), + online = randomBoolean(), + createdAt = randomDate(), + updatedAt = randomDate(), + ) + + fun randomChannelMemberResponse( + user: UserResponse = randomUserResponse(), + channelRole: String = randomString(), + ): ChannelMemberResponse = ChannelMemberResponse( + user = user, + userId = user.id, + channelRole = channelRole, + createdAt = randomDate(), + updatedAt = randomDate(), + banned = randomBoolean(), + shadowBanned = randomBoolean(), + notificationsMuted = randomBoolean(), + ) + fun randomDownstreamMemberDto( user: DownstreamUserDto = randomDownstreamUserDto(), createdAt: Date = randomDate(), @@ -1065,20 +1094,6 @@ internal object Mother { lastThreadMessageAt = lastThreadMessageAt, ) - fun randomUserResponse( - id: String = randomString(), - role: String = randomString(), - language: String = randomString(), - ): UserResponse = UserResponse( - id = id, - role = role, - language = language, - banned = randomBoolean(), - online = randomBoolean(), - createdAt = randomDate(), - updatedAt = randomDate(), - ) - fun randomChannelResponse( id: String = randomString(), type: String = randomString(), diff --git a/stream-chat-android-client/src/test/java/io/getstream/chat/android/client/api2/MoshiChatApiTest.kt b/stream-chat-android-client/src/test/java/io/getstream/chat/android/client/api2/MoshiChatApiTest.kt index 34fadec9c0e..225a652b474 100644 --- a/stream-chat-android-client/src/test/java/io/getstream/chat/android/client/api2/MoshiChatApiTest.kt +++ b/stream-chat-android-client/src/test/java/io/getstream/chat/android/client/api2/MoshiChatApiTest.kt @@ -65,7 +65,6 @@ import io.getstream.chat.android.client.api2.model.response.QueryChannelsRespons import io.getstream.chat.android.client.api2.model.response.QueryDraftMessagesResponse import io.getstream.chat.android.client.api2.model.response.QueryGroupedChannelsGroup import io.getstream.chat.android.client.api2.model.response.QueryGroupedChannelsResponse -import io.getstream.chat.android.client.api2.model.response.QueryMembersResponse import io.getstream.chat.android.client.api2.model.response.QueryPollVotesResponse import io.getstream.chat.android.client.api2.model.response.QueryPollsResponse import io.getstream.chat.android.client.api2.model.response.QueryReactionsResponse @@ -147,6 +146,7 @@ import io.getstream.chat.android.network.models.ListUserGroupsResponse import io.getstream.chat.android.network.models.MarkDeliveredRequest import io.getstream.chat.android.network.models.MarkReadRequest import io.getstream.chat.android.network.models.MarkUnreadRequest +import io.getstream.chat.android.network.models.MembersResponse import io.getstream.chat.android.network.models.MessageActionRequest import io.getstream.chat.android.network.models.MessageRequest import io.getstream.chat.android.network.models.MuteChannelRequest @@ -2378,7 +2378,7 @@ internal class MoshiChatApiTest { @ParameterizedTest @MethodSource("io.getstream.chat.android.client.api2.MoshiChatApiTestArguments#queryMembersInput") - fun testQueryMembers(call: RetrofitCall, expected: KClass<*>) = runTest { + fun testQueryMembers(call: RetrofitCall, expected: KClass<*>) = runTest { // given val api = mock() whenever(api.queryMembers(any())).doReturn(call) diff --git a/stream-chat-android-client/src/test/java/io/getstream/chat/android/client/api2/MoshiChatApiTestArguments.kt b/stream-chat-android-client/src/test/java/io/getstream/chat/android/client/api2/MoshiChatApiTestArguments.kt index f2b60fd2092..8d42f8761ae 100644 --- a/stream-chat-android-client/src/test/java/io/getstream/chat/android/client/api2/MoshiChatApiTestArguments.kt +++ b/stream-chat-android-client/src/test/java/io/getstream/chat/android/client/api2/MoshiChatApiTestArguments.kt @@ -45,7 +45,6 @@ import io.getstream.chat.android.client.api2.model.response.QueryChannelsRespons import io.getstream.chat.android.client.api2.model.response.QueryDraftMessagesResponse import io.getstream.chat.android.client.api2.model.response.QueryGroupedChannelsGroup import io.getstream.chat.android.client.api2.model.response.QueryGroupedChannelsResponse -import io.getstream.chat.android.client.api2.model.response.QueryMembersResponse import io.getstream.chat.android.client.api2.model.response.QueryPollVotesResponse import io.getstream.chat.android.client.api2.model.response.QueryPollsResponse import io.getstream.chat.android.client.api2.model.response.QueryReactionsResponse @@ -77,6 +76,7 @@ import io.getstream.chat.android.network.models.GetBlockedUsersResponse import io.getstream.chat.android.network.models.GetUserGroupResponse import io.getstream.chat.android.network.models.ListDevicesResponse import io.getstream.chat.android.network.models.ListUserGroupsResponse +import io.getstream.chat.android.network.models.MembersResponse import io.getstream.chat.android.network.models.PollOptionResponse import io.getstream.chat.android.network.models.RemoveUserGroupMembersResponse import io.getstream.chat.android.network.models.Response @@ -532,10 +532,12 @@ internal object MoshiChatApiTestArguments { @JvmStatic fun queryMembersInput() = listOf( Arguments.of( - RetroSuccess(QueryMembersResponse(listOf(Mother.randomDownstreamMemberDto()))).toRetrofitCall(), + RetroSuccess( + MembersResponse(duration = randomString(), members = listOf(Mother.randomChannelMemberResponse())), + ).toRetrofitCall(), Result.Success::class, ), - Arguments.of(RetroError(statusCode = 500).toRetrofitCall(), Result.Failure::class), + Arguments.of(RetroError(statusCode = 500).toRetrofitCall(), Result.Failure::class), ) @JvmStatic diff --git a/stream-chat-android-client/src/test/java/io/getstream/chat/android/client/parser2/ChannelMemberResponseParsingTest.kt b/stream-chat-android-client/src/test/java/io/getstream/chat/android/client/parser2/ChannelMemberResponseParsingTest.kt new file mode 100644 index 00000000000..1aa629beebd --- /dev/null +++ b/stream-chat-android-client/src/test/java/io/getstream/chat/android/client/parser2/ChannelMemberResponseParsingTest.kt @@ -0,0 +1,85 @@ +/* + * Copyright (c) 2014-2026 Stream.io Inc. All rights reserved. + * + * Licensed under the Stream License; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://github.com/GetStream/stream-chat-android/blob/main/LICENSE + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.getstream.chat.android.client.parser2 + +import io.getstream.chat.android.network.models.MembersResponse +import org.amshove.kluent.shouldBeEqualTo +import org.amshove.kluent.shouldHaveSize +import org.junit.jupiter.api.Test + +internal class ChannelMemberResponseParsingTest { + private val parser = ParserFactory.createMoshiChatParser() + + @Test + fun `Collect the root-level custom fields of a member`() { + val members = parser.fromJson(MEMBERS_JSON, MembersResponse::class.java).members + + members shouldHaveSize 1 + // Every key the adapter keeps is declared on the model, so each one appears here only because apps + // already read it out of `extraData`. Asserted as an exact map so a key silently dropping out fails. + members.first().custom shouldBeEqualTo mapOf( + "user_id" to "leandro", + "role" to "member", + "is_moderator" to true, + "deleted_at" to "2026-08-14T12:00:00.000Z", + "deleted_messages" to listOf("m1"), + "memberProbe" to "sentinel", + ) + } + + @Test + fun `Standard member fields do not leak into custom`() { + val member = parser.fromJson(MEMBERS_JSON, MembersResponse::class.java).members.first() + + member.channelRole shouldBeEqualTo "channel_member" + member.userId shouldBeEqualTo "leandro" + member.user?.custom shouldBeEqualTo mapOf("birthland" to "Polis Massa") + } + + companion object { + private const val MEMBERS_JSON = + """{ + "duration": "7ms", + "members": [ + { + "user_id": "leandro", + "channel_role": "channel_member", + "created_at": "2021-10-22T00:07:24.000Z", + "updated_at": "2026-05-28T07:40:11.000Z", + "banned": false, + "shadow_banned": false, + "notifications_muted": false, + "role": "member", + "is_moderator": true, + "deleted_at": "2026-08-14T12:00:00.000Z", + "deleted_messages": ["m1"], + "memberProbe": "sentinel", + "user": { + "id": "leandro", + "role": "user", + "language": "pt", + "banned": false, + "online": true, + "created_at": "2021-07-20T14:17:07.000Z", + "updated_at": "2026-07-31T11:38:42.000Z", + "birthland": "Polis Massa" + } + } + ] + }""" + } +}