TestClient Code Analysis
Critical Bugs Found
1. Missing Logger Instance
Location: Line ~320 in TestResponse.text()
is Response.Body.Stream -> {
logger.warn("Cannot read Stream body in test") // ❌ logger is not declared
null
}Fix: Add logger declaration at class level:
private val logger = org.slf4j.LoggerFactory.getLogger(TestClient::class.java)2. Content-Length Timing Issue
Location: RequestBuilder.send() method
The Content-Length header is set before the multipart body is built, which means it might be set to null or wrong value.
// ❌ Current code
if (body != null && !headers.has("Content-Length")) {
headers["Content-Length"] = body!!.size.toString()
}
val multipartSupplierData: List<Part>? = if (multipart.isNotEmpty()) {
val (multipartBody, unconsumedParts) = buildMultipartBody()
body = multipartBody // Body is set AFTER Content-Length
// ...
}Fix: Move Content-Length setting after multipart body building:
val multipartSupplierData: List<Part>? = if (multipart.isNotEmpty()) {
val (multipartBody, unconsumedParts) = buildMultipartBody()
body = multipartBody
header("Content-Type", "multipart/form-data; boundary=$multipartBoundary")
unconsumedParts
} else {
null
}
// ✅ Set Content-Length after body is finalized
if (body != null && !headers.has("Content-Length")) {
headers["Content-Length"] = body!!.size.toString()
}3. Potential NullPointerException in InputStream Reading
Location: buildMultipartBody() method
is FileItem -> {
// ...
val fileBytes = part.inputStream.readAllBytes() // ❌ Could be null
}Fix: Add null safety:
val fileBytes = part.inputStream?.readAllBytes() ?: byteArrayOf()Improvement Suggestions
1. Add HTTP Method Validation
class RequestBuilder(
private val method: String,
private val path: String,
private val app: Colleen
) {
init {
require(method in validMethods) { "Invalid HTTP method: $method" }
}
companion object {
private val validMethods = setOf("GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS")
}
}2. Improve Cookie Handling
Current implementation doesn't handle cookie attributes (domain, path, secure, httpOnly). Consider:
data class Cookie(
val name: String,
val value: String,
val domain: String? = null,
val path: String? = null,
val secure: Boolean = false,
val httpOnly: Boolean = false
)
fun cookie(cookie: Cookie): RequestBuilder {
// Build proper cookie string with attributes
}3. Add Timeout Support for Testing
class RequestBuilder {
private var timeout: Long = 30_000 // 30 seconds default
fun timeout(millis: Long): RequestBuilder {
this.timeout = millis
return this
}
}4. Improve Multipart Boundary Generation
Current UUID-based approach is fine, but could be more predictable for testing:
private fun generateBoundary(seed: String? = null): String {
return if (seed != null) {
"----TestBoundary$seed"
} else {
"----TestBoundary${UUID.randomUUID()}"
}
}5. Add Support for Streaming Response Testing
fun stream(): InputStream? {
return when (val body = response.body) {
is Response.Body.Stream -> body.stream
// Convert other types to stream
else -> bytes()?.inputStream()
}
}6. Better Error Messages in Assertions
fun assertStatus(expected: Int): TestResponse {
if (status != expected) {
val body = text() ?: "(empty body)"
fail("""
Expected status $expected but got $status
Response body: $body
Headers: ${headers.entries.joinToString()}
""".trimIndent())
}
return this
}7. Add Redirect Following Support
fun followRedirects(follow: Boolean = true): RequestBuilder {
this.followRedirects = follow
return this
}
fun send(): TestResponse {
var response = sendInternal()
var redirectCount = 0
while (followRedirects &&
response.status in 300..399 &&
redirectCount < maxRedirects) {
val location = response.header("Location") ?: break
response = app.testClient().get(location).send()
redirectCount++
}
return response
}8. Add Request/Response Logging Helper
fun debug(enabled: Boolean = true): RequestBuilder {
this.debugMode = enabled
return this
}
private fun logRequest() {
if (debugMode) {
println("=== REQUEST ===")
println("$method $path")
headers.forEach { k, v -> println("$k: $v") }
body?.let { println("Body: ${it.toString(Charsets.UTF_8)}") }
}
}9. Support for Testing File Downloads
fun downloadToFile(file: File): TestResponse {
bytes()?.let { file.writeBytes(it) }
return this
}
fun assertFileContent(expected: ByteArray): TestResponse {
val actual = bytes()
if (!actual.contentEquals(expected)) {
fail("File content mismatch")
}
return this
}10. Add Fluent Expectation DSL
fun expect(block: TestResponse.() -> Unit): TestResponse {
this.block()
return this
}
// Usage:
response.expect {
assertStatus(200)
assertContentType("application/json")
json<User>()?.let { user ->
assertEquals("Alice", user.name)
}
}Performance Considerations
1. Memory Usage with Large Files
When testing large file uploads, the current implementation loads entire files into memory. Consider adding streaming support:
fun fileStream(name: String, filename: String, stream: InputStream, size: Long): RequestBuilder {
// Don't load entire file into memory
}2. Reuse ObjectMapper
✅ Already done correctly - using app.config.objectMapper
3. Pool Boundary Generators
For high-volume testing, consider reusing boundaries:
companion object {
private val boundaryPool = ArrayDeque<String>()
private fun getBoundary(): String {
return boundaryPool.removeFirstOrNull()
?: "----TestBoundary${UUID.randomUUID()}"
}
}Security Considerations
1. Path Traversal Protection
Add validation to prevent path traversal attacks in test scenarios:
private fun validatePath(path: String) {
require(!path.contains("..")) { "Path traversal not allowed" }
require(path.startsWith("/")) { "Path must start with /" }
}2. Header Injection Prevention
Validate header values don't contain CRLF:
fun header(key: String, value: String): RequestBuilder {
require(!value.contains("\r") && !value.contains("\n")) {
"Header value cannot contain CRLF"
}
headers[key] = value
return this
}Documentation Improvements
Add More Examples
/**
* Example: Testing authentication
* ```kotlin
* val token = client.post("/login")
* .json(mapOf("user" to "admin", "pass" to "secret"))
* .send()
* .json<LoginResponse>()
* ?.token
*
* client.get("/protected")
* .header("Authorization", "Bearer $token")
* .send()
* .assertStatus(200)
* ```
*/Add Common Patterns Documentation
Testing pagination
Testing rate limiting
Testing concurrent requests
Testing error scenarios
Testing file uploads/downloads
Compatibility Notes
JVM Version
Uses
readAllBytes()which requires Java 9+Consider adding Java 8 compatibility with manual buffer reading
Charset Handling
Currently defaults to UTF-8, which is correct for most cases
Consider adding charset parameter for edge cases
Summary Priority List
Critical (Fix Immediately):
✅ Missing logger declaration
✅ Content-Length timing issue
✅ Null safety in stream reading
High Priority: 4. HTTP method validation 5. Better error messages 6. Cookie attribute support
Medium Priority: 7. Redirect following 8. Streaming response support 9. Timeout configuration
Nice to Have: 10. Debug logging 11. Performance optimizations 12. Enhanced documentation