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

Compatibility Notes

JVM Version

Charset Handling

Summary Priority List

Critical (Fix Immediately):

  1. ✅ Missing logger declaration

  2. ✅ Content-Length timing issue

  3. ✅ 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