All Downloads are FREE. Search and download functionalities are using the official Maven repository.

run.qontract.core.HttpResponse.kt Maven / Gradle / Ivy

Go to download

A Contract Testing Tool that leverages Gherkin to describe APIs in a human readable and machine enforceable manner

There is a newer version: 0.23.1
Show newest version
package run.qontract.core

import io.ktor.http.*
import run.qontract.conversions.guessType
import run.qontract.core.GherkinSection.Then
import run.qontract.core.pattern.ContractException
import run.qontract.core.pattern.Pattern
import run.qontract.core.pattern.parsedValue
import run.qontract.core.value.*

internal const val QONTRACT_RESULT_HEADER = "X-Qontract-Result"

data class HttpResponse(val status: Int = 0, val headers: Map = mapOf(CONTENT_TYPE to "text/plain"), val body: Value = EmptyString) {
    constructor(status: Int = 0, body: String? = "", headers: Map = mapOf(CONTENT_TYPE to "text/plain")) : this(status, headers, body?.let { parsedValue(it) } ?: EmptyString)

    private val statusText: String
        get() =
            when(status) {
                0 -> ""
                else -> HttpStatusCode.fromValue(status).description
            }

    fun updateBodyWith(content: Value): HttpResponse {
        return copy(body = content, headers = headers.minus(CONTENT_TYPE).plus(CONTENT_TYPE to content.httpContentType))
    }

    fun toJSON(): JSONObjectValue =
        JSONObjectValue(mutableMapOf().also { json ->
            json["status"] = NumberValue(status)
            json["body"] = body
            if (statusText.isNotEmpty()) json["status-text"] = StringValue(statusText)
            if (headers.isNotEmpty()) json["headers"] = JSONObjectValue(headers.mapValues { StringValue(it.value) })
        })

    fun toLogString(prefix: String = ""): String {
        val statusLine = "$status $statusText"
        val headerString = headers.map { "${it.key}: ${it.value}" }.joinToString("\n")

        val firstPart = listOf(statusLine, headerString).joinToString("\n").trim()

        val formattedBody = body.toStringValue()

        val responseString = listOf(firstPart, "", formattedBody).joinToString("\n")
        return startLinesWith(responseString, prefix)
    }

    companion object {
        val ERROR_400 = HttpResponse(400, "This request did not match any scenario.", emptyMap())
        val OK = HttpResponse(200, emptyMap())
        fun OK(body: Number): HttpResponse {
            val bodyValue = NumberValue(body)
            return HttpResponse(200, mapOf(CONTENT_TYPE to bodyValue.httpContentType), bodyValue)
        }
        fun OK(body: String): HttpResponse {
            val bodyValue = StringValue(body)
            return HttpResponse(200, mapOf(CONTENT_TYPE to bodyValue.httpContentType), bodyValue)
        }
        fun OK(body: Value) = HttpResponse(200, mapOf(CONTENT_TYPE to body.httpContentType), body)
        val EMPTY = HttpResponse(0, emptyMap())

        fun jsonResponse(jsonData: String?): HttpResponse {
            return HttpResponse(200, jsonData, mapOf(CONTENT_TYPE to "application/json"))
        }

        fun xmlResponse(xmlData: String?): HttpResponse {
            return HttpResponse(200, xmlData, mapOf(CONTENT_TYPE to "application/xml"))
        }

        fun from(status: Int, body: String?) = bodyToHttpResponse(body, status)

        private fun bodyToHttpResponse(body: String?, status: Int): HttpResponse {
            val bodyValue = parsedValue(body)
            return HttpResponse(status, mutableMapOf(CONTENT_TYPE to bodyValue.httpContentType), bodyValue)
        }

        fun fromJSON(jsonObject: Map): HttpResponse {
            val body = jsonObject["body"]
            if(body is NullValue)
                throw ContractException("Either body should have a value or the key should be absent from http-request")

            return HttpResponse(
                    nativeInteger(jsonObject, "status") ?: throw ContractException("http-response must contain a key named status, whose value is the http status in the response"),
                    nativeStringStringMap(jsonObject, "headers").toMutableMap(),
                    jsonObject.getOrDefault("body", StringValue()))
        }
    }
}

fun nativeInteger(json: Map, key: String): Int? {
    val keyValue = json[key] ?: return null

    val errorMessage = "$key must be an integer"
    if(keyValue is StringValue)
        return try { keyValue.string.toInt() } catch(e: Throwable) { throw ContractException(errorMessage) }

    if(keyValue !is NumberValue)
        throw ContractException("Expected $key to be a string value")

    return try { keyValue.number.toInt() } catch(e: Throwable) { throw ContractException(errorMessage) }
}


val responseHeadersToExcludeFromConversion = listOf("Vary", QONTRACT_RESULT_HEADER)

fun toGherkinClauses(response: HttpResponse, types: Map = emptyMap()): Triple, Map, ExampleDeclarations> {
    return try {
        val cleanedUpResponse = dropContentAndCORSResponseHeaders(response)

        return Triple(emptyList(), types, DiscardExampleDeclarations()).let { (clauses, types, examples) ->
            val status = when {
                cleanedUpResponse.status > 0 -> cleanedUpResponse.status
                else -> throw ContractException("Can't generate a contract without a response status")
            }
            Triple(clauses.plus(GherkinClause("status $status", Then)), types, examples)
        }.let { (clauses, types, _) ->
            val (newClauses, newTypes, _) = headersToGherkin(cleanedUpResponse.headers, "response-header", types, DiscardExampleDeclarations(), Then)
            Triple(clauses.plus(newClauses), newTypes, DiscardExampleDeclarations())
        }.let { (clauses, types, examples) ->
            when (val result = responseBodyToGherkinClauses("ResponseBody", guessType(cleanedUpResponse.body), types)) {
                null -> Triple(clauses, types, examples)
                else -> {
                    val (newClauses, newTypes, _) = result
                    Triple(clauses.plus(newClauses), newTypes, DiscardExampleDeclarations())
                }
            }
        }
    } catch(e: NotImplementedError) {
        Triple(emptyList(), types, DiscardExampleDeclarations())
    }
}

fun dropContentAndCORSResponseHeaders(response: HttpResponse) =
        response.copy(headers = response.headers.filterNot { it.key in responseHeadersToExcludeFromConversion || it.key.startsWith("Content-") || it.key.startsWith("Access-Control-") })




© 2015 - 2024 Weber Informatics LLC | Privacy Policy