code
package site.daydream.colleen
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Nested
import org.junit.jupiter.api.assertThrows
import java.io.File
import kotlin.test.assertEquals
import kotlin.test.assertNotNull
import kotlin.test.assertNull
import kotlin.test.assertTrue
/**
* Comprehensive test suite for TestClient
* Tests all HTTP methods, headers, cookies, query parameters,
* form data, multipart uploads, and assertion helpers
*/
class TestClientTest {
private lateinit var app: Colleen
@BeforeEach
fun setup() {
app = Colleen()
}
@Nested
inner class BasicHttpMethods {
@Test
fun `test GET request`() {
app.get("/hello") { "Hello World" }
val response = app.testGet("/hello")
response.assertStatus(200)
.assertBodyEquals("Hello World")
}
@Test
fun `test POST request`() {
app.post("/echo") { ctx ->
ctx.request.body()
}
val response = app.testClient()
.post("/echo")
.body("test content")
.send()
response.assertStatus(200)
.assertBodyEquals("test content")
}
@Test
fun `test PUT request`() {
app.put("/update/:id") { ctx ->
"Updated ${ctx.pathParam("id")}"
}
val response = app.testClient()
.put("/update/123")
.send()
response.assertStatus(200)
.assertBodyContains("123")
}
@Test
fun `test DELETE request`() {
app.delete("/users/:id") { ctx ->
mapOf("deleted" to ctx.pathParam("id"))
}
val response = app.testClient()
.delete("/users/456")
.send()
response.assertStatus(200)
val json = response.json<Map<String, String>>()
assertEquals("456", json?.get("deleted"))
}
@Test
fun `test PATCH request`() {
app.patch("/partial/:id") { ctx ->
"Patched ${ctx.pathParam("id")}"
}
val response = app.testClient()
.patch("/partial/789")
.send()
response.assertStatus(200)
.assertBodyEquals("Patched 789")
}
@Test
fun `test HEAD request`() {
app.head("/resource") { ctx ->
ctx.response.header("X-Resource-Size", "1024")
""
}
val response = app.testClient()
.head("/resource")
.send()
response.assertStatus(200)
.assertHeader("X-Resource-Size", "1024")
}
@Test
fun `test OPTIONS request`() {
app.options("/api") { ctx ->
ctx.response.header("Allow", "GET, POST, PUT, DELETE")
""
}
val response = app.testClient()
.options("/api")
.send()
response.assertHeader("Allow")
}
}
@Nested
inner class HeaderTests {
@Test
fun `test single header`() {
app.get("/auth") { ctx ->
ctx.request.header("Authorization") ?: "No auth"
}
val response = app.testClient()
.get("/auth")
.header("Authorization", "Bearer token123")
.send()
response.assertBodyEquals("Bearer token123")
}
@Test
fun `test multiple headers`() {
app.post("/headers") { ctx ->
val auth = ctx.request.header("Authorization")
val contentType = ctx.request.header("Content-Type")
"$auth | $contentType"
}
val response = app.testClient()
.post("/headers")
.header("Authorization", "Bearer xyz")
.header("Content-Type", "application/json")
.body("{}")
.send()
response.assertBodyContains("Bearer xyz")
.assertBodyContains("application/json")
}
@Test
fun `test custom headers`() {
app.get("/custom") { ctx ->
mapOf(
"xRequestId" to ctx.request.header("X-Request-ID"),
"xApiKey" to ctx.request.header("X-API-Key")
)
}
val response = app.testClient()
.get("/custom")
.header("X-Request-ID", "req-123")
.header("X-API-Key", "key-456")
.send()
val json = response.json<Map<String, String>>()
assertEquals("req-123", json?.get("xRequestId"))
assertEquals("key-456", json?.get("xApiKey"))
}
}
@Nested
inner class QueryParameterTests {
@Test
fun `test single query parameter`() {
app.get("/search") { ctx ->
"Query: ${ctx.queryParam("q")}"
}
val response = app.testClient()
.get("/search")
.query("q", "kotlin")
.send()
response.assertBodyEquals("Query: kotlin")
}
@Test
fun `test multiple query parameters`() {
app.get("/filter") { ctx ->
val page = ctx.queryParam("page")
val size = ctx.queryParam("size")
val sort = ctx.queryParam("sort")
"page=$page&size=$size&sort=$sort"
}
val response = app.testClient()
.get("/filter")
.query("page", "1")
.query("size", "10")
.query("sort", "name")
.send()
response.assertBodyContains("page=1")
.assertBodyContains("size=10")
.assertBodyContains("sort=name")
}
@Test
fun `test query parameters with special characters`() {
app.get("/special") { ctx ->
ctx.queryParam("text") ?: ""
}
val response = app.testClient()
.get("/special")
.query("text", "hello world & special=chars")
.send()
response.assertBodyEquals("hello world & special=chars")
}
@Test
fun `test multiple values for same query parameter`() {
app.get("/tags") { ctx ->
ctx.queryParams("tag").joinToString(",")
}
val response = app.testClient()
.get("/tags")
.query("tag", "kotlin")
.query("tag", "java")
.query("tag", "spring")
.send()
response.assertBodyContains("kotlin")
.assertBodyContains("java")
.assertBodyContains("spring")
}
}
@Nested
inner class CookieTests {
@Test
fun `test single cookie`() {
app.get("/check-session") { ctx ->
ctx.cookie("session") ?: "No session"
}
val response = app.testClient()
.get("/check-session")
.cookie("session", "abc123")
.send()
response.assertBodyEquals("abc123")
}
@Test
fun `test multiple cookies`() {
app.get("/cookies") { ctx ->
val session = ctx.cookie("session")
val user = ctx.cookie("user")
val pref = ctx.cookie("preference")
"session=$session, user=$user, pref=$pref"
}
val response = app.testClient()
.get("/cookies")
.cookie("session", "sess123")
.cookie("user", "alice")
.cookie("preference", "dark")
.send()
response.assertBodyContains("sess123")
.assertBodyContains("alice")
.assertBodyContains("dark")
}
}
@Nested
inner class RequestBodyTests {
@Test
fun `test string body`() {
app.post("/echo") { ctx ->
ctx.request.body()
}
val response = app.testClient()
.post("/echo")
.body("Hello from test")
.send()
response.assertBodyEquals("Hello from test")
}
@Test
fun `test byte array body`() {
app.post("/bytes") { ctx ->
ctx.request.bodyAsBytes()?.size ?: 0
}
val testBytes = byteArrayOf(1, 2, 3, 4, 5)
val response = app.testClient()
.post("/bytes")
.body(testBytes)
.send()
val size = response.json<Int>()
assertEquals(5, size)
}
@Test
fun `test JSON body with data class`() {
data class User(val name: String, val age: Int)
app.post("/user") { ctx ->
val user = ctx.bodyAs<User>()
"User: ${user.name}, Age: ${user.age}"
}
val response = app.testClient()
.post("/user")
.json(User("Alice", 30))
.send()
response.assertBodyContains("Alice")
.assertBodyContains("30")
}
@Test
fun `test JSON body with map`() {
app.post("/data") { ctx ->
val data = ctx.bodyAs<Map<String, Any>>()
data
}
val inputData = mapOf(
"key1" to "value1",
"key2" to 42,
"key3" to true
)
val response = app.testClient()
.post("/data")
.json(inputData)
.send()
val result = response.json<Map<String, Any>>()
assertEquals("value1", result?.get("key1"))
assertEquals(42, (result?.get("key2") as? Number)?.toInt())
assertEquals(true, result?.get("key3"))
}
@Test
fun `test JSON array body`() {
app.post("/list") { ctx ->
val list = ctx.bodyAs<List<String>>()
list.size
}
val response = app.testClient()
.post("/list")
.json(listOf("a", "b", "c"))
.send()
val size = response.json<Int>()
assertEquals(3, size)
}
}
@Nested
inner class FormDataTests {
@Test
fun `test form data with vararg`() {
app.post("/login") { ctx ->
val username = ctx.formParam("username")
val password = ctx.formParam("password")
"Logged in: $username"
}
val response = app.testClient()
.post("/login")
.form("username" to "alice", "password" to "secret123")
.send()
response.assertBodyEquals("Logged in: alice")
}
@Test
fun `test form data with map`() {
app.post("/register") { ctx ->
val data = mapOf(
"email" to ctx.formParam("email"),
"name" to ctx.formParam("name")
)
data
}
val formData = mapOf(
"email" to "alice@example.com",
"name" to "Alice Smith"
)
val response = app.testClient()
.post("/register")
.form(formData)
.send()
val result = response.json<Map<String, String>>()
assertEquals("alice@example.com", result?.get("email"))
assertEquals("Alice Smith", result?.get("name"))
}
@Test
fun `test form data with special characters`() {
app.post("/feedback") { ctx ->
ctx.formParam("message") ?: ""
}
val response = app.testClient()
.post("/feedback")
.form("message" to "Hello & goodbye! Special=chars")
.send()
response.assertBodyContains("Hello")
.assertBodyContains("Special")
}
}
@Nested
inner class MultipartTests {
@Test
fun `test file upload with bytes`() {
app.post("/upload") { ctx ->
val file = ctx.file("file")
mapOf(
"filename" to file?.filename,
"size" to file?.size,
"contentType" to file?.contentType
)
}
val fileContent = "Hello, this is a test file!".toByteArray()
val response = app.testClient()
.post("/upload")
.file("file", "test.txt", fileContent, "text/plain")
.send()
val result = response.json<Map<String, Any>>()
assertEquals("test.txt", result?.get("filename"))
assertEquals(fileContent.size.toLong(), (result?.get("size") as? Number)?.toLong())
assertEquals("text/plain", result?.get("contentType"))
}
@Test
fun `test file upload from File object`() {
app.post("/upload-doc") { ctx ->
val file = ctx.file("document")
file?.filename ?: "No file"
}
// Create temporary file
val tempFile = File.createTempFile("test", ".txt")
tempFile.writeText("Document content")
try {
val response = app.testClient()
.post("/upload-doc")
.file("document", tempFile)
.send()
response.assertBodyEquals(tempFile.name)
} finally {
tempFile.delete()
}
}
@Test
fun `test multiple file upload`() {
app.post("/upload-multiple") { ctx ->
val files = ctx.files()
files.size
}
val response = app.testClient()
.post("/upload-multiple")
.file("file1", "doc1.txt", "Content 1".toByteArray())
.file("file2", "doc2.txt", "Content 2".toByteArray())
.file("file3", "doc3.txt", "Content 3".toByteArray())
.send()
val count = response.json<Int>()
assertEquals(3, count)
}
@Test
fun `test multipart with form fields and files`() {
app.post("/upload-with-metadata") { ctx ->
val title = ctx.formParam("title")
val description = ctx.formParam("description")
val file = ctx.file("attachment")
mapOf(
"title" to title,
"description" to description,
"filename" to file?.filename
)
}
val response = app.testClient()
.post("/upload-with-metadata")
.multipart(
FormItem("title", "My Document"),
FormItem("description", "Important file"),
FileItem(
"attachment",
"doc.pdf",
"application/pdf",
100L,
"PDF content".toByteArray().inputStream()
)
)
.send()
val result = response.json<Map<String, String>>()
assertEquals("My Document", result?.get("title"))
assertEquals("Important file", result?.get("description"))
assertEquals("doc.pdf", result?.get("filename"))
}
}
@Nested
inner class ResponseAssertionTests {
@Test
fun `test assertStatus`() {
app.get("/ok") { "OK" }
app.get("/not-found") { ctx ->
ctx.status(404)
"Not found"
}
app.testGet("/ok").assertStatus(200)
app.testGet("/not-found").assertStatus(404)
}
@Test
fun `test assertSuccess`() {
app.get("/success") { ctx ->
ctx.status(201)
"Created"
}
app.testGet("/success").assertSuccess()
}
@Test
fun `test assertClientError`() {
app.get("/bad-request") { ctx ->
ctx.status(400)
"Bad request"
}
app.testGet("/bad-request").assertClientError()
}
@Test
fun `test assertServerError`() {
app.get("/error") { ctx ->
ctx.status(500)
"Internal error"
}
app.testGet("/error").assertServerError()
}
@Test
fun `test assertBodyContains`() {
app.get("/message") { "Hello World from Kotlin" }
app.testGet("/message")
.assertBodyContains("Hello")
.assertBodyContains("World")
.assertBodyContains("Kotlin")
}
@Test
fun `test assertBodyEquals`() {
app.get("/exact") { "Exact match" }
app.testGet("/exact").assertBodyEquals("Exact match")
}
@Test
fun `test assertHeader exists`() {
app.get("/with-header") { ctx ->
ctx.response.header("X-Custom-Header", "value")
"OK"
}
app.testGet("/with-header").assertHeader("X-Custom-Header")
}
@Test
fun `test assertHeader with value`() {
app.get("/content") { ctx ->
ctx.response.header("Content-Type", "application/json")
"{}"
}
app.testGet("/content")
.assertHeader("Content-Type", "application/json")
}
@Test
fun `test assertContentType`() {
app.get("/json") { ctx ->
ctx.json(mapOf("key" to "value"))
}
app.testGet("/json").assertContentType("application/json")
}
@Test
fun `test assertion failures throw AssertionError`() {
app.get("/test") { "Hello" }
assertThrows<AssertionError> {
app.testGet("/test").assertStatus(404)
}
assertThrows<AssertionError> {
app.testGet("/test").assertBodyEquals("Wrong")
}
assertThrows<AssertionError> {
app.testGet("/test").assertBodyContains("NotThere")
}
}
@Test
fun `test chained assertions`() {
app.post("/api/create") { ctx ->
ctx.status(201)
ctx.response.header("Location", "/api/items/123")
ctx.json(mapOf("id" to 123, "status" to "created"))
}
app.testClient()
.post("/api/create")
.json(mapOf("name" to "New Item"))
.send()
.assertStatus(201)
.assertSuccess()
.assertHeader("Location")
.assertContentType("application/json")
}
}
@Nested
inner class ResponseDataExtractionTests {
@Test
fun `test text extraction`() {
app.get("/text") { "Plain text response" }
val text = app.testGet("/text").text()
assertEquals("Plain text response", text)
}
@Test
fun `test json extraction with inline reified`() {
data class Person(val name: String, val age: Int)
app.get("/person") { ctx ->
ctx.json(Person("Bob", 25))
}
val person = app.testGet("/person").json<Person>()
assertNotNull(person)
assertEquals("Bob", person.name)
assertEquals(25, person.age)
}
@Test
fun `test json extraction with class parameter`() {
app.get("/number") { ctx ->
ctx.json(42)
}
val number = app.testGet("/number").json(Int::class.java)
assertEquals(42, number)
}
@Test
fun `test bytes extraction`() {
app.get("/binary") { ctx ->
ctx.bytes(byteArrayOf(1, 2, 3, 4, 5))
}
val bytes = app.testGet("/binary").bytes()
assertNotNull(bytes)
assertEquals(5, bytes.size)
assertEquals(1, bytes[0])
assertEquals(5, bytes[4])
}
@Test
fun `test header extraction`() {
app.get("/with-headers") { ctx ->
ctx.response.header("X-Request-ID", "req-123")
ctx.response.header("X-API-Version", "v2")
"OK"
}
val response = app.testGet("/with-headers")
assertEquals("req-123", response.header("X-Request-ID"))
assertEquals("v2", response.header("X-API-Version"))
}
@Test
fun `test headers extraction returns list`() {
app.get("/multi-header") { ctx ->
ctx.response.header("Set-Cookie", "session=abc")
ctx.response.header("Set-Cookie", "user=xyz")
"OK"
}
val response = app.testGet("/multi-header")
val cookies = response.headers("Set-Cookie")
assertEquals(2, cookies.size)
assertTrue(cookies.contains("session=abc"))
assertTrue(cookies.contains("user=xyz"))
}
@Test
fun `test null responses`() {
app.get("/empty") { ctx ->
ctx.status(204)
ctx.response.body = Response.Body.Empty
}
val response = app.testGet("/empty")
assertNull(response.text())
assertNull(response.bytes())
assertNull(response.json<Map<String, Any>>())
}
}
@Nested
inner class ConvenienceMethodTests {
@Test
fun `test testGet convenience method`() {
app.get("/quick") { "Quick test" }
val response = app.testGet("/quick")
response.assertStatus(200).assertBodyEquals("Quick test")
}
@Test
fun `test testPost convenience method`() {
app.post("/quick-post") { ctx ->
val data = ctx.bodyAs<Map<String, String>>()
"Received: ${data["message"]}"
}
val response = app.testPost("/quick-post", mapOf("message" to "Hello"))
response.assertBodyContains("Hello")
}
@Test
fun `test testClient factory method`() {
app.get("/client") { "Test client" }
val client = app.testClient()
val response = client.get("/client").send()
response.assertStatus(200)
}
@Test
fun `test invoke operator for GET requests`() {
app.get("/operator") { "Operator test" }
val response = app.testClient().get("/operator")()
response.assertStatus(200)
}
}
@Nested
inner class EdgeCasesAndErrorHandling {
@Test
fun `test empty path`() {
app.get("/") { "Root" }
val response = app.testGet("/")
response.assertBodyEquals("Root")
}
@Test
fun `test path with special characters`() {
app.get("/api/items/:id") { ctx ->
ctx.pathParam("id") ?: ""
}
val response = app.testGet("/api/items/abc-123_def")
response.assertBodyContains("abc-123_def")
}
@Test
fun `test very large body`() {
app.post("/large") { ctx ->
ctx.request.bodyAsBytes()?.size ?: 0
}
val largeContent = "x".repeat(1_000_000)
val response = app.testClient()
.post("/large")
.body(largeContent)
.send()
val size = response.json<Int>()
assertEquals(1_000_000, size)
}
@Test
fun `test empty query parameters`() {
app.get("/no-params") { ctx ->
ctx.request.queryString.isEmpty()
}
val response = app.testGet("/no-params")
val isEmpty = response.json<Boolean>()
assertEquals(true, isEmpty)
}
@Test
fun `test empty headers`() {
app.get("/minimal") { "Minimal" }
val response = app.testClient()
.get("/minimal")
.send()
response.assertStatus(200)
}
@Test
fun `test dump method for debugging`() {
app.get("/debug") { ctx ->
ctx.response.header("X-Debug", "true")
"Debug info"
}
// dump() should not throw and should return self
val response = app.testGet("/debug").dump()
response.assertStatus(200)
}
}
@Nested
inner class IntegrationTests {
@Test
fun `test complete REST API workflow`() {
data class Todo(val id: Int, val title: String, val completed: Boolean)
val todos = mutableListOf<Todo>()
// Create
app.post("/todos") { ctx ->
val todo = ctx.bodyAs<Todo>()
todos.add(todo)
ctx.status(201)
todo
}
// List
app.get("/todos") { todos }
// Get one
app.get("/todos/:id") { ctx ->
val id = ctx.pathParam("id")?.toInt()
todos.find { it.id == id } ?: ctx.status(404)
}
// Update
app.put("/todos/:id") { ctx ->
val id = ctx.pathParam("id")?.toInt()
val updated = ctx.bodyAs<Todo>()
val index = todos.indexOfFirst { it.id == id }
if (index >= 0) {
todos[index] = updated
updated
} else {
ctx.status(404)
}
}
// Delete
app.delete("/todos/:id") { ctx ->
val id = ctx.pathParam("id")?.toInt()
todos.removeIf { it.id == id }
ctx.status(204)
}
val client = app.testClient()
// Create todo
client.post("/todos")
.json(Todo(1, "Learn Kotlin", false))
.send()
.assertStatus(201)
// List todos
client.get("/todos")
.send()
.assertStatus(200)
.json<List<Todo>>()
.let { list ->
assertEquals(1, list?.size)
}
// Update todo
client.put("/todos/1")
.json(Todo(1, "Learn Kotlin", true))
.send()
.assertStatus(200)
// Delete todo
client.delete("/todos/1")
.send()
.assertStatus(204)
}
@Test
fun `test authentication flow`() {
var sessionToken: String? = null
app.post("/login") { ctx ->
val creds = ctx.bodyAs<Map<String, String>>()
if (creds["username"] == "admin" && creds["password"] == "pass") {
sessionToken = "token-${System.currentTimeMillis()}"
ctx.response.header("Set-Cookie", "session=$sessionToken")
mapOf("success" to true, "token" to sessionToken)
} else {
ctx.status(401)
mapOf("success" to false)
}
}
app.get("/protected") { ctx ->
val token = ctx.cookie("session")
if (token == sessionToken) {
"Protected data"
} else {
ctx.status(403)
"Forbidden"
}
}
val client = app.testClient()
// Login
val loginResponse = client.post("/login")
.json(mapOf("username" to "admin", "password" to "pass"))
.send()
loginResponse.assertStatus(200)
val token = loginResponse.header("Set-Cookie")?.substringAfter("session=")
// Access protected resource
client.get("/protected")
.cookie("session", token ?: "")
.send()
.assertStatus(200)
.assertBodyEquals("Protected data")
}
}
}