Skip to content

Commit 5afc99d

Browse files
authored
Merge pull request #287 from simple-robot/dev/support-channel-kickout-API
改进 ChannelKickoutApi 的部分相关测试
2 parents 9c85658 + 7bc1540 commit 5afc99d

File tree

3 files changed

+230
-73
lines changed

3 files changed

+230
-73
lines changed

simbot-component-kook-api/src/commonMain/kotlin/love/forte/simbot/kook/api/channel/ChannelKickoutApi.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import kotlin.jvm.JvmStatic
3333
* 踢出语音频道中的用户,只能踢出在语音频道中的用户
3434
*
3535
* @author ForteScarlet
36+
* @since 4.2.0
3637
*/
3738
public class ChannelKickoutApi private constructor(
3839
channelId: String,
@@ -69,4 +70,4 @@ public class ChannelKickoutApi private constructor(
6970
@SerialName("user_id")
7071
val userId: String
7172
)
72-
}
73+
}

simbot-component-kook-api/src/commonMain/kotlin/love/forte/simbot/kook/objects/User.kt

Lines changed: 18 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,30 @@
11
/*
2-
* Copyright (c) 2023. ForteScarlet.
2+
* Copyright (c) 2023-2025. ForteScarlet.
33
*
4-
* This file is part of simbot-component-kook.
4+
* This file is part of simbot-component-kook.
55
*
6-
* simbot-component-kook is free software: you can redistribute it and/or modify it under the terms of
7-
* the GNU Lesser General Public License as published by the Free Software Foundation,
8-
* either version 3 of the License, or (at your option) any later version.
6+
* simbot-component-kook is free software: you can redistribute it and/or modify
7+
* it under the terms of the GNU Lesser General Public License as published by
8+
* the Free Software Foundation, either version 3 of the License, or
9+
* (at your option) any later version.
910
*
10-
* simbot-component-kook is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
11-
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
12-
* See the GNU Lesser General Public License for more details.
11+
* simbot-component-kook is distributed in the hope that it will be useful,
12+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
13+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14+
* GNU Lesser General Public License for more details.
1315
*
14-
* You should have received a copy of the GNU Lesser General Public License along with simbot-component-kook,
15-
* If not, see <https://www.gnu.org/licenses/>.
16+
* You should have received a copy of the GNU Lesser General Public License
17+
* along with simbot-component-kook,
18+
* If not, see <https://www.gnu.org/licenses/>.
1619
*/
1720

1821
package love.forte.simbot.kook.objects
1922

2023
import kotlinx.serialization.SerialName
2124
import kotlinx.serialization.Serializable
25+
import love.forte.simbot.kook.objects.SystemUser.SYSTEM_USER_ID
26+
import love.forte.simbot.kook.objects.SystemUser.SYSTEM_USER_IDENTIFY_NUM
27+
import love.forte.simbot.kook.objects.SystemUser.SYSTEM_USER_NAME
2228
import love.forte.simbot.kook.objects.SystemUser.id
2329

2430

@@ -141,8 +147,8 @@ public data class SimpleUser(
141147
* 没有 `identify_num` 的情况下,会**尝试**从 [username] 中切割 `#` 并解析出 identifyNum 的值,
142148
* 无法得到结果时使用空字符串。
143149
*/
144-
@SerialName("identify_num") override val identifyNum: String = username.split("#", limit = 2)
145-
.let { if (it.size < 2) it[0] else "" },
150+
@SerialName("identify_num") override val identifyNum: String =
151+
username.substringAfter('#', ""),
146152
/**
147153
* 当前是否在线,默认为 `false`
148154
*/

simbot-component-kook-api/src/commonTest/kotlin/love/forte/simbot/kook/api/channel/ChannelKickoutApiTest.kt

Lines changed: 210 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -20,117 +20,267 @@
2020

2121
package love.forte.simbot.kook.api.channel
2222

23+
import io.ktor.client.*
24+
import io.ktor.client.engine.mock.*
2325
import io.ktor.http.*
26+
import kotlinx.coroutines.test.runTest
2427
import kotlinx.serialization.json.Json
28+
import kotlinx.serialization.json.jsonObject
29+
import kotlinx.serialization.json.jsonPrimitive
2530
import love.forte.simbot.kook.Kook
31+
import love.forte.simbot.kook.api.ApiResult
32+
import love.forte.simbot.kook.api.requestResult
2633
import kotlin.test.Test
2734
import kotlin.test.assertEquals
2835
import kotlin.test.assertNotNull
2936
import kotlin.test.assertTrue
3037

3138
/**
32-
* Tests for [ChannelKickoutApi] focusing on API structure and serialization.
33-
*
39+
* Comprehensive tests for [ChannelKickoutApi] focusing on API structure, serialization/deserialization,
40+
* and realistic scenarios based on official documentation.
41+
*
42+
* Tests include JSON serialization validation, API structure verification,
43+
* and response deserialization using patterns from official KOOK API documentation.
44+
*
3445
* @author ForteScarlet
46+
* @since 4.2.0
3547
*/
3648
class ChannelKickoutApiTest {
3749
private val json = Json(Kook.DEFAULT_JSON) {
3850
ignoreUnknownKeys = true
51+
prettyPrint = true
3952
}
4053

4154
@Test
4255
fun testApiBasics() {
43-
val channelId = "123456789"
44-
val userId = "987654321"
56+
val channelId = "3321010478582002"
57+
val userId = "1700000"
4558
val api = ChannelKickoutApi.create(channelId, userId)
46-
59+
4760
// Test API properties
4861
assertEquals(HttpMethod.Post, api.method)
49-
assertTrue(api.url.toString().contains("channel/kickout"))
50-
62+
assertEquals("https://www.kookapp.cn/api/v3/channel/kickout", api.url.toString())
63+
5164
// Test body content
5265
val body = api.body
5366
assertNotNull(body, "Body should not be null")
5467
}
55-
68+
5669
@Test
5770
fun testUrlConstruction() {
5871
val api = ChannelKickoutApi.create("test_channel", "test_user")
5972
val url = api.url.toString()
60-
73+
6174
// Verify URL contains correct Kook API base and path
62-
assertTrue(url.contains("kookapp.cn/api/v3"))
63-
assertTrue(url.contains("channel/kickout"))
75+
assertEquals("https://www.kookapp.cn/api/v3/channel/kickout", url)
6476
}
65-
77+
6678
@Test
67-
fun testRequestBodyNotNull() {
68-
val channelId = "voice_channel_123"
69-
val userId = "user_456"
79+
fun testRequestBodyStructure() = runTest {
80+
val channelId = "3321010478582002"
81+
val userId = "1700000"
7082
val api = ChannelKickoutApi.create(channelId, userId)
83+
84+
// Capture actual request body using MockEngine
85+
var capturedRequestBody: String? = null
86+
val mockEngine = MockEngine { request ->
87+
capturedRequestBody = request.body.toByteArray().decodeToString()
88+
respond(
89+
content = """{"code": 0, "message": "操作成功", "data": []}""",
90+
status = HttpStatusCode.OK,
91+
headers = headersOf(HttpHeaders.ContentType, "application/json")
92+
)
93+
}
94+
95+
val client = HttpClient(mockEngine)
7196

72-
val body = api.body
73-
assertNotNull(body, "Body should not be null")
97+
// Make actual request to capture serialized body
98+
api.requestResult(client, "Bot test-token")
7499

75-
// Verify that the body object exists and is not null
76-
assertTrue(body.toString().isNotEmpty())
100+
// Validate the captured JSON body structure
101+
assertNotNull(capturedRequestBody, "Request body should be captured")
102+
val requestJson = json.parseToJsonElement(capturedRequestBody).jsonObject
103+
104+
// Verify JSON structure matches expected serialization
105+
assertEquals(channelId, requestJson["channel_id"]?.jsonPrimitive?.content)
106+
assertEquals(userId, requestJson["user_id"]?.jsonPrimitive?.content)
107+
assertEquals(2, requestJson.size)
77108
}
78-
109+
79110
@Test
80-
fun testMultipleApiInstances() {
81-
val api1 = ChannelKickoutApi.create("channel1", "user1")
82-
val api2 = ChannelKickoutApi.create("channel2", "user2")
83-
84-
// Verify that different instances have different bodies
85-
val body1 = api1.body
86-
val body2 = api2.body
87-
88-
assertNotNull(body1)
89-
assertNotNull(body2)
90-
91-
// Bodies should be different objects (different parameters)
92-
assertTrue(body1 !== body2, "Different API instances should have different body objects")
111+
fun testSuccessfulResponseDeserialization() {
112+
// Based on official documentation example
113+
//language=json
114+
val successResponseJson = """{
115+
"code": 0,
116+
"message": "操作成功",
117+
"data": []
118+
}"""
119+
120+
val apiResult = json.decodeFromString(ApiResult.serializer(), successResponseJson)
121+
122+
assertNotNull(apiResult)
123+
assertEquals(0, apiResult.code)
124+
assertEquals("操作成功", apiResult.message)
125+
assertNotNull(apiResult.data)
126+
}
127+
128+
@Test
129+
fun testErrorResponseDeserialization() {
130+
// Test realistic error scenario - channel doesn't exist or not a voice channel
131+
//language=json
132+
val errorResponseJson = """{
133+
"code": 40001,
134+
"message": "频道不存在或不是语音频道"
135+
}"""
136+
137+
val apiResult = json.decodeFromString(ApiResult.serializer(), errorResponseJson)
138+
139+
assertNotNull(apiResult)
140+
assertEquals(40001, apiResult.code)
141+
assertEquals("频道不存在或不是语音频道", apiResult.message)
93142
}
94-
143+
95144
@Test
96145
fun testApiFactory() {
97-
val channelId = "factory_test_channel"
98-
val userId = "factory_test_user"
99-
146+
val channelId = "7480000000000000"
147+
val userId = "2418000000"
148+
100149
// Test factory method
101150
val api = ChannelKickoutApi.create(channelId, userId)
102151
assertNotNull(api)
103-
152+
104153
// Verify the created API has the correct properties
105154
assertEquals(HttpMethod.Post, api.method)
106155
assertNotNull(api.body)
107156
assertNotNull(api.url)
157+
158+
// Verify API path structure
159+
val url = api.url
160+
assertEquals("https://www.kookapp.cn/api/v3/channel/kickout", url.toString())
161+
assertEquals(listOf("", "api", "v3", "channel", "kickout"), url.pathSegments)
108162
}
109-
163+
110164
@Test
111-
fun testApiPathCorrectness() {
112-
val api = ChannelKickoutApi.create("test", "test")
113-
val url = api.url
114-
115-
// Check that the URL path is correct
116-
assertTrue(url.pathSegments.contains("channel"))
117-
assertTrue(url.pathSegments.contains("kickout"))
118-
119-
// Verify it's a POST API
120-
assertEquals(HttpMethod.Post, api.method)
165+
fun testMultipleApiInstances() {
166+
val api1 = ChannelKickoutApi.create("3321010478582001", "1700001")
167+
val api2 = ChannelKickoutApi.create("3321010478582002", "1700002")
168+
169+
// Verify that different instances have different bodies
170+
val body1 = api1.body
171+
val body2 = api2.body
172+
173+
assertNotNull(body1)
174+
assertNotNull(body2)
175+
176+
// Bodies should be different objects (different parameters)
177+
assertTrue(body1 !== body2, "Different API instances should have different body objects")
178+
179+
// Verify they have different string representations
180+
val string1 = body1.toString()
181+
val string2 = body2.toString()
182+
assertTrue(string1 != string2, "Different instances should have different string representations")
121183
}
122-
184+
123185
@Test
124-
fun testBodyNotNull() {
125-
val api = ChannelKickoutApi.create("channel_test", "user_test")
186+
fun testRealisticScenario() = runTest {
187+
// Test with realistic IDs from documentation
188+
val voiceChannelId = "3321010478582002" // Voice channel from docs
189+
val userId = "1700000" // User ID from docs
190+
191+
val api = ChannelKickoutApi.create(voiceChannelId, userId)
192+
193+
// Verify API structure
194+
assertEquals(HttpMethod.Post, api.method)
195+
assertEquals("https://www.kookapp.cn/api/v3/channel/kickout", api.url.toString())
196+
197+
// Capture actual request body using MockEngine
198+
var capturedRequestBody: String? = null
199+
val mockEngine = MockEngine { request ->
200+
capturedRequestBody = request.body.toByteArray().decodeToString()
201+
respond(
202+
content = """{"code": 0, "message": "操作成功", "data": []}""",
203+
status = HttpStatusCode.OK,
204+
headers = headersOf(HttpHeaders.ContentType, "application/json")
205+
)
206+
}
207+
208+
val client = HttpClient(mockEngine)
126209

127-
// Body should never be null for this API
128-
assertNotNull(api.body)
210+
// Make actual request to capture serialized body
211+
api.requestResult(client, "Bot test-token")
129212

130-
// Multiple calls to body should return the same non-null value
131-
val body1 = api.body
132-
val body2 = api.body
133-
assertNotNull(body1)
134-
assertNotNull(body2)
213+
// Validate the captured JSON body structure
214+
assertNotNull(capturedRequestBody, "Request body should be captured")
215+
val requestJson = json.parseToJsonElement(capturedRequestBody).jsonObject
216+
217+
// Verify JSON structure matches expected serialization for realistic scenario
218+
assertEquals(voiceChannelId, requestJson["channel_id"]?.jsonPrimitive?.content)
219+
assertEquals(userId, requestJson["user_id"]?.jsonPrimitive?.content)
220+
assertEquals(2, requestJson.size)
221+
}
222+
223+
@Test
224+
fun testParameterValidation() = runTest {
225+
// Test with various parameter combinations
226+
val testCases = listOf(
227+
"7480000000000000" to "2418000000", // From documentation
228+
"3321010478582002" to "1700000", // Voice channel example
229+
"test_channel_123" to "test_user_456" // Generic test case
230+
)
231+
232+
testCases.forEach { (channelId, userId) ->
233+
val api = ChannelKickoutApi.create(channelId, userId)
234+
235+
// Verify each API instance is properly constructed
236+
assertEquals(HttpMethod.Post, api.method)
237+
assertNotNull(api.body)
238+
assertNotNull(api.url)
239+
240+
// Capture actual request body using MockEngine for each test case
241+
var capturedRequestBody: String? = null
242+
val mockEngine = MockEngine { request ->
243+
capturedRequestBody = request.body.toByteArray().decodeToString()
244+
respond(
245+
content = """{"code": 0, "message": "操作成功", "data": []}""",
246+
status = HttpStatusCode.OK,
247+
headers = headersOf(HttpHeaders.ContentType, "application/json")
248+
)
249+
}
250+
251+
val client = HttpClient(mockEngine)
252+
253+
// Make actual request to capture serialized body
254+
api.requestResult(client, "Bot test-token")
255+
256+
// Validate the captured JSON body structure for each parameter combination
257+
assertNotNull(capturedRequestBody, "Request body should be captured for channelId=$channelId, userId=$userId")
258+
val requestJson = json.parseToJsonElement(capturedRequestBody).jsonObject
259+
260+
// Verify JSON structure matches expected serialization
261+
assertEquals(channelId, requestJson["channel_id"]?.jsonPrimitive?.content)
262+
assertEquals(userId, requestJson["user_id"]?.jsonPrimitive?.content)
263+
assertEquals(2, requestJson.size)
264+
}
265+
}
266+
267+
@Test
268+
fun testApiResultStructureWithWrapper() {
269+
// Test that API result properly handles KOOK's wrapper structure
270+
271+
//language=json
272+
val wrappedResponseJson = """{
273+
"code": 0,
274+
"message": "操作成功",
275+
"data": []
276+
}"""
277+
278+
val result = json.decodeFromString(ApiResult.serializer(), wrappedResponseJson)
279+
280+
// Verify wrapper structure is properly handled
281+
assertNotNull(result)
282+
assertEquals(0, result.code)
283+
assertEquals("操作成功", result.message)
284+
assertNotNull(result.data)
135285
}
136-
}
286+
}

0 commit comments

Comments
 (0)