diff --git a/core/src/main/kotlin/in/specmatic/conversions/OpenApiSpecification.kt b/core/src/main/kotlin/in/specmatic/conversions/OpenApiSpecification.kt index e88ebb2b4..a2698b4e8 100644 --- a/core/src/main/kotlin/in/specmatic/conversions/OpenApiSpecification.kt +++ b/core/src/main/kotlin/in/specmatic/conversions/OpenApiSpecification.kt @@ -731,13 +731,22 @@ class OpenApiSpecification(private val openApiFilePath: String, private val pars } } - Pair(requestPattern.copy(multiPartFormDataPattern = parts), emptyMap()) + Pair(requestPattern.copy( + multiPartFormDataPattern = parts, + headersPattern = headersPatternWithContentType(requestPattern, contentType) + ), emptyMap()) } "application/x-www-form-urlencoded" -> { - Pair(requestPattern.copy(formFieldsPattern = toFormFields(mediaType)), emptyMap()) + Pair(requestPattern.copy( + formFieldsPattern = toFormFields(mediaType), + headersPattern = headersPatternWithContentType(requestPattern, contentType) + ), emptyMap()) } "application/xml" -> { - Pair(requestPattern.copy(body = toXMLPattern(mediaType)), emptyMap()) + Pair(requestPattern.copy( + body = toXMLPattern(mediaType), + headersPattern = headersPatternWithContentType(requestPattern, contentType) + ), emptyMap()) } else -> { val exampleBodies: Map = mediaType.examples?.mapValues { @@ -761,7 +770,10 @@ class OpenApiSpecification(private val openApiFilePath: String, private val pars it.key to requestsWithSecurityParams }.toMap() - Pair(requestPattern.copy(body = toSpecmaticPattern(mediaType)), examples) + Pair(requestPattern.copy( + body = toSpecmaticPattern(mediaType), + headersPattern = headersPatternWithContentType(requestPattern, contentType) + ), examples) } } } @@ -769,6 +781,13 @@ class OpenApiSpecification(private val openApiFilePath: String, private val pars } } + private fun headersPatternWithContentType( + requestPattern: HttpRequestPattern, + contentType: String + ) = requestPattern.headersPattern.copy( + contentType = contentType + ) + private fun namedExampleParams(operation: Operation, parameterType: Class): Map> = operation.parameters.orEmpty() .filterIsInstance(parameterType) .fold(emptyMap>()) { acc, parameter -> diff --git a/core/src/main/kotlin/in/specmatic/core/HttpHeadersPattern.kt b/core/src/main/kotlin/in/specmatic/core/HttpHeadersPattern.kt index 8040fc093..982af6ba2 100644 --- a/core/src/main/kotlin/in/specmatic/core/HttpHeadersPattern.kt +++ b/core/src/main/kotlin/in/specmatic/core/HttpHeadersPattern.kt @@ -8,7 +8,8 @@ import io.ktor.http.* data class HttpHeadersPattern( val pattern: Map = emptyMap(), - val ancestorHeaders: Map? = null + val ancestorHeaders: Map? = null, + val contentType: String? = null ) { init { val uniqueHeaders = pattern.keys.map { it.lowercase() }.distinct() @@ -127,13 +128,17 @@ data class HttpHeadersPattern( } fun generate(resolver: Resolver): Map { - return attempt(breadCrumb = "HEADERS") { + val headers = attempt(breadCrumb = "HEADERS") { pattern.mapValues { (key, pattern) -> attempt(breadCrumb = key) { toStringLiteral(resolver.withCyclePrevention(pattern) { it.generate(key, pattern) }) } } }.map { (key, value) -> withoutOptionality(key) to value }.toMap() + return when { + !contentType.isNullOrBlank() -> headers.plus(CONTENT_TYPE to contentType) + else -> headers + } } private fun toStringLiteral(headerValue: Value) = when (headerValue) { @@ -154,17 +159,17 @@ data class HttpHeadersPattern( fun newBasedOn(row: Row, resolver: Resolver): List = forEachKeyCombinationIn(row.withoutOmittedKeys(pattern), row, resolver) { pattern -> newBasedOn(pattern, row, resolver) - }.map { HttpHeadersPattern(it.mapKeys { withoutOptionality(it.key) }) } + }.map { HttpHeadersPattern(it.mapKeys { withoutOptionality(it.key) }, contentType = contentType) } fun negativeBasedOn(row: Row, resolver: Resolver) = forEachKeyCombinationIn(row.withoutOmittedKeys(pattern), row, resolver) { pattern -> negativeBasedOn(pattern, row, resolver, true) - }.map { HttpHeadersPattern(it.mapKeys { withoutOptionality(it.key) }) } + }.map { HttpHeadersPattern(it.mapKeys { withoutOptionality(it.key) }, contentType = contentType) } fun newBasedOn(resolver: Resolver): List = allOrNothingCombinationIn(pattern) { pattern -> newBasedOn(pattern, resolver) - }.map { HttpHeadersPattern(it.mapKeys { withoutOptionality(it.key) }) } + }.map { HttpHeadersPattern(it.mapKeys { withoutOptionality(it.key) }, contentType = contentType) } fun negativeBasedOn(resolver: Resolver): List = allOrNothingCombinationIn(pattern) { pattern -> diff --git a/core/src/main/kotlin/in/specmatic/core/HttpRequestPattern.kt b/core/src/main/kotlin/in/specmatic/core/HttpRequestPattern.kt index 4afd8167a..9ef7ddc5d 100644 --- a/core/src/main/kotlin/in/specmatic/core/HttpRequestPattern.kt +++ b/core/src/main/kotlin/in/specmatic/core/HttpRequestPattern.kt @@ -334,7 +334,6 @@ data class HttpRequestPattern( resolver.withCyclePrevention(body) {cyclePreventedResolver -> body.generate(cyclePreventedResolver).let { value -> newRequest = newRequest.updateBody(value) - newRequest = newRequest.updateHeader(CONTENT_TYPE, value.httpContentType) } } } diff --git a/core/src/test/kotlin/in/specmatic/conversions/OpenApiKtTest.kt b/core/src/test/kotlin/in/specmatic/conversions/OpenApiKtTest.kt index 855bd085b..dcf233a2e 100644 --- a/core/src/test/kotlin/in/specmatic/conversions/OpenApiKtTest.kt +++ b/core/src/test/kotlin/in/specmatic/conversions/OpenApiKtTest.kt @@ -2526,6 +2526,57 @@ components: assertThat(result.results).isNotEmpty } + + @Test + fun `should send content-type header based on media-type in spec rather than payload data type`() { + val contract = OpenApiSpecification.fromYAML( + """ + openapi: "3.0.3" + info: + version: 1.0.0 + title: Petstore + description: A sample API that uses a petstore as an example to demonstrate features in the OpenAPI 3.0 specification + license: + name: Apache 2.0 + url: https://www.apache.org/licenses/LICENSE-2.0.html + paths: + /hello/: + post: + summary: create a pet + description: Creates a new pet in the store. Duplicates are allowed + operationId: addPet + requestBody: + description: Pet to add to the store + required: true + content: + application/json: + schema: + type: string + responses: + '200': + description: new pet record + content: + application/json: + schema: + type: string +""".trimIndent(), "" + ).toFeature() + + val result = contract.executeTests(object : TestExecutor { + override fun execute(request: HttpRequest): HttpResponse { + println(request.toLogString()) + assertThat(request.headers[CONTENT_TYPE]).isEqualTo("application/json") + + return HttpResponse.OK + } + + override fun setServerState(serverState: Map) { + + } + }) + + assertThat(result.success()).withFailMessage(result.report()).isTrue + } } data class CycleRoot( diff --git a/core/src/test/kotlin/in/specmatic/core/HttpRequestPatternTest.kt b/core/src/test/kotlin/in/specmatic/core/HttpRequestPatternTest.kt index 94ab105d8..b11b0325f 100644 --- a/core/src/test/kotlin/in/specmatic/core/HttpRequestPatternTest.kt +++ b/core/src/test/kotlin/in/specmatic/core/HttpRequestPatternTest.kt @@ -427,4 +427,16 @@ internal class HttpRequestPatternTest { assertThat(patterns).hasSize(1) } + + @Test + fun `content-type should be sent when available`() { + val httpRequestPattern = HttpRequestPattern( + headersPattern = HttpHeadersPattern(contentType = "application/json"), + method = "POST", + urlMatcher = toURLMatcherWithOptionalQueryParams(URI("/matching_path")), + body = StringPattern() + ) + val httpRequest: HttpRequest = httpRequestPattern.generate(Resolver()) + assertThat(httpRequest.headers[CONTENT_TYPE]).isEqualTo("application/json") + } }