extractor用例:
package site.daydream.colleen
import org.junit.jupiter.api.Assertions.*
import org.junit.jupiter.api.BeforeAll
import org.junit.jupiter.api.Disabled
import org.junit.jupiter.api.TestInstance
import org.junit.jupiter.api.assertThrows
import java.io.ByteArrayInputStream
import java.io.InputStream
import java.lang.reflect.Proxy
import kotlin.test.Test
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class ExtractorTest {
private lateinit var app: Colleen
data class UserDto(val name: String, val age: Int)
data class QueryDto(val page: Int, val size: Int)
@BeforeAll
fun setup() {
app = mockColleen()
}
// ============================================
// Context 参数提取测试
// ============================================
fun handler1(ctx: Context): String = "path: ${ctx.path}"
@Test
fun `should extract Context parameter directly`() {
val handler = cx(::handler1)
val ctx = createContext(path = "/test")
val result = handler(ctx)
assertEquals("path: /test", result)
}
fun handler2(ctx: Context, ctx2: Context): String =
"${ctx.path} - ${ctx2.path}"
@Test
fun `should handle multiple Context parameters`() {
val handler = cx(::handler2)
val ctx = createContext(path = "/multi")
val result = handler(ctx) as String
assertEquals("/multi - /multi", result)
}
// ============================================
// Path 参数提取测试
// ============================================
fun handler3(@Param("id") id: Path<String>) = "id: ${id.value}"
@Test
fun `should extract Path String parameter`() {
val ctx = createContext().apply { params["id"] = "123" }
val result = cx(::handler3)(ctx) as String
assertEquals("id: 123", result)
}
fun handler4(@Param("id") id: Path<Int>) = id.value * 2
@Test
fun `should extract Path Int parameter`() {
val ctx = createContext().apply { params["id"] = "42" }
val result = cx(::handler4)(ctx) as Int
assertEquals(84, result)
}
fun handler5(@Param("id") id: Path<Long>) = id.value
@Test
fun `should extract Path Long parameter`() {
val ctx = createContext().apply { params["id"] = "9999999999" }
val result = cx(::handler5)(ctx) as Long
assertEquals(9999999999L, result)
}
fun handler6(@Param("id") id: Path<Int>) = id.value
@Test
fun `should throw when Path parameter is missing`() {
val ctx = createContext()
assertThrows<ExtractionError.MissingParameter> {
cx(::handler6)(ctx)
}
}
fun handler7(@Param("id") id: Path<Int>) = id.value
@Test
fun `should throw when Path parameter conversion fails`() {
val ctx = createContext().apply { params["id"] = "not-a-number" }
assertThrows<ExtractionError.ConversionFailed> {
cx(::handler7)(ctx)
}
}
fun handler8(id: Path<Int>) = id.value
@Test
@Disabled("在IDEA中运行时似乎会保留参数名")
fun `should throw when Path parameter name is missing`() {
assertThrows<IllegalArgumentException> {
cx(::handler8)
}
}
// ============================================
// Query 参数提取测试
// ============================================
fun handler9(@Param("name") name: Query<String>) = "Hello ${name.value}"
@Test
fun `should extract Query String parameter`() {
val ctx = createContext(queryString = "name=Alice")
val result = cx(::handler9)(ctx) as String
assertEquals("Hello Alice", result)
}
fun handler10(@Param("page") page: Query<Int>) = page.value
@Test
fun `should extract Query Int parameter`() {
val ctx = createContext(queryString = "page=5")
val result = cx(::handler10)(ctx) as Int
assertEquals(5, result)
}
fun handler11(@Param("page") page: Query<Int?>) = page.value ?: -1
@Test
fun `should extract nullable Query Int parameter when present`() {
val ctx = createContext(queryString = "page=10")
val result = cx(::handler11)(ctx) as Int
assertEquals(10, result)
}
fun handler12(@Param("page") page: Query<Int?>) = page.value ?: -1
@Test
fun `should extract nullable Query Int parameter when absent`() {
val ctx = createContext(queryString = "")
val result = cx(::handler12)(ctx) as Int
assertEquals(-1, result)
}
fun handler13(@Param("page") page: Query<Int>) = page.value
@Test
fun `should throw when required Query parameter is missing`() {
val ctx = createContext(queryString = "")
assertThrows<ExtractionError.MissingParameter> {
cx(::handler13)(ctx)
}
}
fun handler14(@Param("active") active: Query<Boolean>) = active.value
@Test
fun `should extract Query Boolean parameter - true variants`() {
listOf("true", "True", "TRUE", "1", "yes", "Yes", "y", "Y").forEach { value ->
val ctx = createContext(queryString = "active=$value")
val result = cx(::handler14)(ctx) as Boolean
assertTrue(result, "Failed for value: $value")
}
}
fun handler15(@Param("active") active: Query<Boolean>) = active.value
@Test
fun `should extract Query Boolean parameter - false variants`() {
listOf("false", "False", "0", "no", "n", "anything").forEach { value ->
val ctx = createContext(queryString = "active=$value")
val result = cx(::handler15)(ctx) as Boolean
assertFalse(result, "Failed for value: $value")
}
}
fun handler16(@Param("tags") tags: Query<List<String>>) = tags.value.size
@Test
fun `should extract Query List of Strings`() {
val ctx = createContext(queryString = "tags=java&tags=kotlin&tags=spring")
val result = cx(::handler16)(ctx) as Int
assertEquals(3, result)
}
fun handler17(@Param("ids") ids: Query<List<Int>>) = ids.value.sum()
@Test
fun `should extract Query List of Ints`() {
val ctx = createContext(queryString = "ids=1&ids=2&ids=3")
val result = cx(::handler17)(ctx) as Int
assertEquals(6, result)
}
fun handler18(params: Query<Map<String, List<String>>>) =
params.value.keys.size
@Test
fun `should extract Query Map of all parameters`() {
val ctx = createContext(queryString = "a=1&b=2&c=3")
val result = cx(::handler18)(ctx) as Int
assertEquals(3, result)
}
fun handler19(dto: Query<QueryDto>) = dto.value.page + dto.value.size
@Test
fun `should extract Query as DTO`() {
val ctx = createContext(queryString = "page=1&size=20")
val result = cx(::handler19)(ctx) as Int
assertEquals(21, result)
}
fun handler20(@Param("message") msg: Query<String>) = msg.value
@Test
fun `should handle URL encoded Query parameters`() {
val ctx = createContext(queryString = "message=hello%20world")
val result = cx(::handler20)(ctx) as String
assertEquals("hello world", result)
}
// ============================================
// Form 参数提取测试
// ============================================
fun handler21(@Param("username") name: Form<String>) = name.value
@Test
fun `should extract Form String parameter`() {
val ctx = createContext(
body = "username=john",
contentType = "application/x-www-form-urlencoded"
)
val result = cx(::handler21)(ctx) as String
assertEquals("john", result)
}
fun handler22(@Param("age") age: Form<Int>) = age.value
@Test
fun `should extract Form Int parameter`() {
val ctx = createContext(
body = "age=25",
contentType = "application/x-www-form-urlencoded"
)
val result = cx(::handler22)(ctx) as Int
assertEquals(25, result)
}
fun handler23(@Param("age") age: Form<Int?>) = age.value ?: 0
@Test
fun `should extract nullable Form parameter when absent`() {
val ctx = createContext(
body = "name=test",
contentType = "application/x-www-form-urlencoded"
)
val result = cx(::handler23)(ctx) as Int
assertEquals(0, result)
}
fun handler24(@Param("colors") colors: Form<List<String>>) = colors.value
@Test
fun `should extract Form List of Strings`() {
val ctx = createContext(
body = "colors=red&colors=blue&colors=green",
contentType = "application/x-www-form-urlencoded"
)
val result = cx(::handler24)(ctx) as List<*>
assertEquals(listOf("red", "blue", "green"), result)
}
fun handler25(data: Form<Map<String, List<String>>>) = data.value.size
@Test
fun `should extract Form as Map`() {
val ctx = createContext(
body = "field1=val1&field2=val2",
contentType = "application/x-www-form-urlencoded"
)
val result = cx(::handler25)(ctx) as Int
assertEquals(2, result)
}
fun handler26(@Param("name") name: Form<String?>) = name.value ?: "empty"
@Test
fun `should return empty when Content-Type is not form`() {
val ctx = createContext(
body = "name=test",
contentType = "application/json"
)
val result = cx(::handler26)(ctx) as String
assertEquals("empty", result)
}
// ============================================
// JSON 参数提取测试
// ============================================
fun handler27(user: Json<UserDto>) = user.value.name
@Test
fun `should extract JSON as DTO`() {
val json = """{"name":"Alice","age":30}"""
val ctx = createContext(body = json)
val result = cx(::handler27)(ctx) as String
assertEquals("Alice", result)
}
fun handler28(user: Json<UserDto>) = user.value
@Test
fun `should throw when JSON parsing fails`() {
val ctx = createContext(body = "invalid json")
assertThrows<ExtractionError.ConversionFailed> {
cx(::handler28)(ctx)
}
}
data class ComplexDto1(val users: List<UserDto>, val count: Int)
fun handler29(data: Json<ComplexDto1>) = data.value.users.size
@Test
fun `should handle complex JSON structures`() {
val json = """{"users":[{"name":"A","age":20},{"name":"B","age":25}],"count":2}"""
val ctx = createContext(body = json)
val result = cx(::handler29)(ctx) as Int
assertEquals(2, result)
}
// ============================================
// Header 参数提取测试
// ============================================
fun handler30(@Param("Authorization") auth: Header) = auth.value
@Test
fun `should extract Header parameter`() {
val ctx = createContext(headers = mapOf("Authorization" to "Bearer token123"))
val result = cx(::handler30)(ctx) as String
assertEquals("Bearer token123", result)
}
fun handler31(@Param("X-Custom") header: Header) = header.value
@Test
fun `should return empty string when Header is missing`() {
val ctx = createContext()
val result = cx(::handler31)(ctx) as String
assertEquals("", result)
}
fun handler32(@Param("content-type") ct: Header) = ct.value
@Test
fun `should extract case-insensitive Header`() {
val ctx = createContext(headers = mapOf("Content-Type" to "application/json"))
val result = cx(::handler32)(ctx) as String
assertEquals("application/json", result)
}
// ============================================
// Cookie 参数提取测试
// ============================================
fun handler33(@Param("session") session: Cookie) = session.value
@Test
fun `should extract Cookie parameter`() {
val ctx = createContext(headers = mapOf("cookie" to "session=abc123"))
val result = cx(::handler33)(ctx) as String
assertEquals("abc123", result)
}
fun handler34(@Param("token") token: Cookie) = token.value
@Test
fun `should return empty string when Cookie is missing`() {
val ctx = createContext()
val result = cx(::handler34)(ctx) as String
assertEquals("", result)
}
fun handler35(@Param("user") user: Cookie) = user.value
@Test
fun `should extract Cookie from multiple cookies`() {
val ctx = createContext(
headers = mapOf("cookie" to "session=xyz; user=john; theme=dark")
)
val result = cx(::handler35)(ctx) as String
assertEquals("john", result)
}
// ============================================
// Text 参数提取测试
// ============================================
fun handler36(text: Text) = text.value
@Test
fun `should extract Text body`() {
val ctx = createContext(body = "Plain text content")
val result = cx(::handler36)(ctx) as String
assertEquals("Plain text content", result)
}
fun handler37(text: Text) = text.value
@Test
fun `should return empty string when body is null`() {
val ctx = createContext(body = null)
val result = cx(::handler37)(ctx) as String
assertEquals("", result)
}
// ============================================
// Stream 参数提取测试
// ============================================
fun handler38(stream: Stream): Int {
return stream.value.use { it.available() }
}
@Test
fun `should extract Stream parameter`() {
val data = "test data".toByteArray()
val ctx = createContext(stream = ByteArrayInputStream(data))
val result = cx(::handler38)(ctx) as Int
assertEquals(data.size, result)
}
fun handler39(stream: Stream) = stream.value
@Test
fun `should throw when Stream is missing`() {
val ctx = createContext(stream = null)
assertThrows<ExtractionError.MissingParameter> {
cx(::handler39)(ctx)
}
}
// ============================================
// UploadedFile 参数提取测试
// ============================================
fun handler40(@Param("avatar") file: UploadedFile) = file.value.filename
@Test
@Disabled("newProxyInstance expects an interface")
fun `should extract UploadedFile with explicit name`() {
val uploadedFile =
Request.UploadedFile("avatar", "avatar.jpg", "image/jpeg", 1024, ByteArrayInputStream(byteArrayOf()))
val ctx = createContext(contentType = "multipart/form-data")
// Mock file method
val mockRequest = ctx.request.copy()
val proxy = Proxy.newProxyInstance(
Request::class.java.classLoader,
arrayOf(Request::class.java)
) { _, method, args ->
if (method.name == "file") {
val name = args?.get(0) as String
return@newProxyInstance if (name == "avatar") uploadedFile else null
}
method.invoke(mockRequest, *(args ?: emptyArray()))
} as Request
val mockCtx = ctx.copy(request = proxy)
val result = cx(::handler40)(mockCtx) as String
assertEquals("avatar.jpg", result)
}
fun handler41(file: UploadedFile) = file.value.contentType
@Test
@Disabled("newProxyInstance expects an interface")
fun `should extract UploadedFile with default name 'file'`() {
val uploadedFile =
Request.UploadedFile("file", "doc.pdf", "application/pdf", 2048, ByteArrayInputStream(byteArrayOf()))
val ctx = createContext(contentType = "multipart/form-data")
val mockRequest = ctx.request.copy()
val proxy = Proxy.newProxyInstance(
Request::class.java.classLoader,
arrayOf(Request::class.java)
) { _, method, args ->
if (method.name == "file") {
val name = args?.get(0) as String
return@newProxyInstance if (name == "avatar") uploadedFile else null
}
method.invoke(mockRequest, *(args ?: emptyArray()))
} as Request
val mockCtx = ctx.copy(request = proxy)
val result = cx(::handler41)(mockCtx) as String
assertEquals("application/pdf", result)
}
fun handler42(@Param("document") file: UploadedFile) = file.value
@Test
fun `should throw when UploadedFile is missing`() {
val ctx = createContext()
assertThrows<ExtractionError.MissingParameter> {
cx(::handler42)(ctx)
}
}
// ============================================
// Service 注入测试
// ============================================
class TestService(val value: String = "injected")
fun handler43(service: TestService) = service.value
@Test
fun `should inject service from container`() {
app.serviceContainer.registerInstance(TestService())
val ctx = createContext()
val result = cx(::handler43)(ctx) as String
assertEquals("injected", result)
}
class MissingService
fun handler44(service: MissingService?) = service ?: "not found"
@Test
fun `should return null when service not found`() {
val ctx = createContext()
val result = cx(::handler44)(ctx)
// Note: This will throw NullPointerException as service is not nullable in signature
// This test demonstrates the limitation
}
// ============================================
// 混合参数测试
// ============================================
fun handler45(
ctx: Context,
@Param("id") id: Path<Int>,
@Param("format") format: Query<String?>,
user: Json<UserDto>
): String {
return "${ctx.path}:${id.value}:${format.value}:${user.value.name}"
}
@Test
fun `should handle mixed parameter types`() {
val ctx = createContext(
path = "/users/123",
queryString = "format=json",
body = """{"name":"Bob","age":28}"""
).apply {
params["id"] = "123"
}
val result = cx(::handler45)(ctx) as String
assertEquals("/users/123:123:json:Bob", result)
}
fun handler46(
@Param("page") page: Query<Int>,
@Param("size") size: Query<Int>,
@Param("sort") sort: Query<String?>
): String {
return "page=${page.value},size=${size.value},sort=${sort.value ?: "default"}"
}
@Test
fun `should handle multiple Query parameters`() {
val ctx = createContext(queryString = "page=2&size=50")
val result = cx(::handler46)(ctx) as String
assertEquals("page=2,size=50,sort=default", result)
}
// ============================================
// 边界情况和错误处理测试
// ============================================
fun handler47(@Param("test") param: Query<String?>) = param.value ?: "empty"
@Test
fun `should handle empty query string`() {
val ctx = createContext(queryString = "")
val result = cx(::handler47)(ctx) as String
assertEquals("empty", result)
}
fun handler48(@Param("name") name: Path<String>) = name.value
@Test
fun `should handle special characters in Path parameter`() {
val ctx = createContext().apply {
params["name"] = "hello-world_123"
}
val result = cx(::handler48)(ctx) as String
assertEquals("hello-world_123", result)
}
fun handler49(@Param("id") id: Path<Int>) = id.value
@Test
fun `should throw descriptive error for invalid Int conversion`() {
val ctx = createContext().apply { params["id"] = "12.34" }
val exception = assertThrows<ExtractionError.ConversionFailed> {
cx(::handler49)(ctx)
}
assertTrue(exception.message!!.contains("12.34"))
}
fun handler50(
@Param("price") price: Query<Double>,
@Param("rate") rate: Query<Float>
) = Pair(price.value, rate.value)
@Test
fun `should handle Float and Double conversions`() {
val ctx = createContext(queryString = "price=99.99&rate=0.15")
val result = cx(::handler50)(ctx) as Pair<*, *>
assertEquals(99.99, result.first)
assertEquals(0.15f, result.second)
}
fun handler51(@Param("timestamp") ts: Query<Long>) = ts.value
@Test
fun `should handle Long conversions for large numbers`() {
val ctx = createContext(queryString = "timestamp=1234567890123")
val result = cx(::handler51)(ctx) as Long
assertEquals(1234567890123L, result)
}
// ============================================
// Helper Methods
// ============================================
private fun createContext(
path: String = "/test",
method: String = "GET",
queryString: String = "",
headers: Map<String, String> = emptyMap(),
body: String? = null,
stream: InputStream? = null,
contentType: String? = null
): Context {
val headerMap = Headers()
headers.forEach { (k, v) -> headerMap[k] = v }
contentType?.let { headerMap["Content-Type"] = it }
val inputStream = stream ?: body?.byteInputStream()
val request = Request(
method = method,
path = path,
queryString = queryString,
headers = headerMap,
stream = inputStream
)
return Context(
request = request,
response = Response(),
app = app
)
}
private fun mockColleen(): Colleen {
return Colleen()
}
}
// ============================================
// Additional Edge Case Tests
// ============================================
class ExtractorEdgeCaseTest {
fun handler1(@Param("data") data: Query<String>) = data.value.length
@Test
fun `should handle very long strings`() {
val longString = "x".repeat(10000)
val ctx = createMinimalContext(queryString = "data=$longString")
val result = cx(::handler1)(ctx) as Int
assertEquals(10000, result)
}
fun handler2(
@Param("temp") temp: Query<Int>,
@Param("balance") balance: Query<Double>
) = Pair(temp.value, balance.value)
@Test
fun `should handle negative numbers`() {
val ctx = createMinimalContext(queryString = "temp=-15&balance=-99.99")
val result = cx(::handler2)(ctx) as Pair<*, *>
assertEquals(-15, result.first)
assertEquals(-99.99, result.second)
}
fun handler3(@Param("count") count: Query<Int>) = count.value
@Test
fun `should handle zero values`() {
val ctx = createMinimalContext(queryString = "count=0")
val result = cx(::handler3)(ctx) as Int
assertEquals(0, result)
}
fun handler4(@Param("tags") tags: Query<List<String>>) = tags.value.isEmpty()
@Test
fun `should handle empty List parameters`() {
val ctx = createMinimalContext(queryString = "other=value")
val result = cx(::handler4)(ctx) as Boolean
assertTrue(result)
}
private fun createMinimalContext(queryString: String = ""): Context {
val request = Request(
method = "GET",
path = "/",
queryString = queryString,
headers = Headers()
)
val mockApp = Colleen()
return Context(request, Response(), mockApp)
}
}