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

io.specmatic.core.HttpResponsePattern.kt Maven / Gradle / Ivy

Go to download

Turn your contracts into executable specifications. Contract Driven Development - Collaboratively Design & Independently Deploy MicroServices & MicroFrontends.

There is a newer version: 2.0.37
Show newest version
package io.specmatic.core

import io.specmatic.core.pattern.*
import io.specmatic.core.value.StringValue
import io.specmatic.core.value.Value
import io.specmatic.stub.softCastValueToXML

const val DEFAULT_RESPONSE_CODE = 1000

data class HttpResponsePattern(
    val headersPattern: HttpHeadersPattern = HttpHeadersPattern(),
    val status: Int = 0,
    val body: Pattern = EmptyStringPattern,
    val responseValueAssertion: ResponseValueAssertion = AnyResponse
) {
    constructor(response: HttpResponse) : this(HttpHeadersPattern(response.headers.mapValues { stringToPattern(it.value, it.key) }), response.status, response.body.exactMatchElseType())

    fun generateResponse(resolver: Resolver): HttpResponse {
        return attempt(breadCrumb = "RESPONSE") {
            val value = softCastValueToXML(resolver.withCyclePrevention(body, body::generate))
            val headers = headersPattern.generate(resolver).plus(SPECMATIC_RESULT_HEADER to "success").let { headers ->
                if ((headers.containsKey("Content-Type").not() && value.httpContentType.isBlank().not()))
                    headers.plus("Content-Type" to value.httpContentType)
                else headers
            }
            HttpResponse(status, headers, value)
        }
    }

    fun generateResponseWithAll(resolver: Resolver): HttpResponse {
        return attempt(breadCrumb = "RESPONSE") {
            val value = softCastValueToXML(body.generateWithAll(resolver))
            val headers = headersPattern.generateWithAll(resolver).plus(SPECMATIC_RESULT_HEADER to "success").let { headers ->
                when {
                    !headers.containsKey("Content-Type") -> headers.plus("Content-Type" to value.httpContentType)
                    else -> headers
                }
            }
            HttpResponse(status, headers, value)
        }
    }

    fun matches(response: HttpResponse, resolver: Resolver): Result {
        val result = matchesResponse(response, resolver)

        return when(result) {
            is Result.Failure -> result.breadCrumb("RESPONSE")
            else -> result
        }
    }

    fun matchesResponse(response: HttpResponse, resolver: Resolver): Result {
        return response to resolver to
            ::matchStatus then
            ::matchHeaders then
            ::matchResponseBodySchema then
            ::matchExactResponseBodyValue then
            ::summarize otherwise
            ::handleError toResult
            ::returnResult
    }

    fun withResponseExampleValue(row: Row, resolver: Resolver): HttpResponsePattern =
        attempt(breadCrumb = "RESPONSE") {
            val responseExample: ResponseExample = row.responseExampleForValidation ?: return@attempt this

            val responseExampleMatchResult = matches(responseExample.responseExample, resolver)

            if(responseExampleMatchResult is Result.Failure)
                throw ContractException("""Error in response in example "${row.name}": ${responseExampleMatchResult.reportString()}""")

            val expectedResponseValue: HttpResponsePattern =
                HttpResponsePattern(
                    responseExample.headersPattern(),
                    responseExample.responseExample.status,
                    responseExample.bodyPattern()
                )

            val responseValueAssertion: ResponseValueAssertion = ValueAssertion(expectedResponseValue)

            this.copy(responseValueAssertion = responseValueAssertion)
        }

    fun matchesMock(response: HttpResponse, resolver: Resolver) = matches(response, resolver)

    private fun matchStatus(parameters: Pair): MatchingResult> {
        if(status == DEFAULT_RESPONSE_CODE)
            return MatchSuccess(parameters)

        val (response, _) = parameters

        val body = response.body

        return when (response.status) {
            status -> MatchSuccess(parameters)
            else -> MatchFailure(mismatchResult("status $status", "status ${response.status}").copy(breadCrumb = "STATUS", failureReason = FailureReason.StatusMismatch))
        }
    }

    private fun matchHeaders(parameters: Pair): MatchingResult>> {
        val (response, resolver) = parameters
        return when (val result = headersPattern.matches(response.headers, resolver)) {
            is Result.Failure -> MatchSuccess(Triple(response, resolver, listOf(result)))
            else -> MatchSuccess(Triple(response, resolver, emptyList()))
        }
    }

    private fun matchResponseBodySchema(parameters: Triple>): MatchingResult>> {
        val (response, resolver, failures) = parameters

        val parsedValue = when (response.body) {
            is StringValue -> try { body.parse(response.body.string, resolver) } catch(e: Throwable) { response.body }
            else -> response.body
        }

        val result = body.matches(parsedValue, resolver)
        if(result is Result.Failure)
            return MatchSuccess(Triple(response, resolver, failures.plus(result.breadCrumb("BODY"))))

        return MatchSuccess(parameters)
    }

    private fun matchExactResponseBodyValue(parameters: Triple>): MatchingResult>> {
        val (response, resolver, failures) = parameters

        val result = responseValueAssertion.matches(response, resolver.copy(mismatchMessages = valueMismatchMessages))

        if(result is Result.Failure)
            return MatchSuccess(Triple(response, resolver, failures.plus(result)))

        return MatchSuccess(parameters)
    }

    fun bodyPattern(newBody: Pattern): HttpResponsePattern = this.copy(body = newBody)

    fun encompasses(other: HttpResponsePattern, olderResolver: Resolver, newerResolver: Resolver): Result {
        if(status != other.status)
            return Result.Failure("The status didn't match", breadCrumb = "STATUS", failureReason = FailureReason.StatusMismatch)

        val headerResult = headersPattern.encompasses(other.headersPattern, Resolver(), Resolver())
        val bodyResult = resolvedHop(body, olderResolver).encompasses(resolvedHop(other.body, newerResolver), olderResolver, newerResolver).breadCrumb("BODY")

        return Result.fromResults(listOf(headerResult, bodyResult)).breadCrumb("RESPONSE")
    }

    fun fromResponseExpectation(response: HttpResponse): HttpResponsePattern {
        val responseHeaders = response.headers.mapValues { stringToPattern(it.value, it.key) }

        val contentTypeHeader = if("content-type" !in responseHeaders.keys.map { it.lowercase() } && headersPattern.contentType != null)
            mapOf("Content-Type" to ExactValuePattern(StringValue(headersPattern.contentType)))
        else
            emptyMap()

        return HttpResponsePattern(
            HttpHeadersPattern(responseHeaders + contentTypeHeader),
            response.status,
            response.body.exactMatchElseType()
        )
    }

    fun resolveSubstitutions(substitution: Substitution, response: HttpResponse): HttpResponse {
        val substitutedHeaders = substitution.resolveHeaderSubstitutions(response.headers, headersPattern.pattern).breadCrumb("RESPONSE.HEADERS").value
        val substitutedBody = body.resolveSubstitutions(substitution, response.body, substitution.resolver).breadCrumb("RESPONSE.BODY").value

        return response.copy(
            headers = substitutedHeaders,
            body = substitutedBody
        )
    }

    fun generateResponse(partial: HttpResponse, dictionary: Dictionary, resolver: Resolver): HttpResponse {
        val headers = headersPattern.fillInTheBlanks(partial.headers, dictionary, resolver).breadCrumb("HEADERS")
        val body: ReturnValue = body.fillInTheBlanks(partial.body, dictionary, resolver).breadCrumb("BODY")

        return headers.combine(body) { fullHeaders, fullBody ->
            partial.copy(
                headers = fullHeaders,
                body = fullBody
            )
        }.breadCrumb("RESPONSE").value
    }
}

private val valueMismatchMessages = object : MismatchMessages {
    override fun mismatchMessage(expected: String, actual: String): String {
        return "Value mismatch: Expected $expected, got value $actual"
    }

    override fun unexpectedKey(keyLabel: String, keyName: String): String {
        return "Value mismatch: $keyLabel $keyName in value was unexpected"
    }

    override fun expectedKeyWasMissing(keyLabel: String, keyName: String): String {
        return "Value mismatch: $keyLabel $keyName was missing"
    }

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy