Skip to content

Commit

Permalink
Merge pull request #899 from znsio/content_type_header_fixes
Browse files Browse the repository at this point in the history
Content-type request header should be based on OpenAPI specification
  • Loading branch information
joelrosario authored Jan 3, 2024
2 parents f249fdc + c218632 commit b6fe37b
Show file tree
Hide file tree
Showing 5 changed files with 96 additions and 10 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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<String, String?> = mediaType.examples?.mapValues {
Expand All @@ -761,14 +770,24 @@ 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)
}
}
}
}
}
}

private fun headersPatternWithContentType(
requestPattern: HttpRequestPattern,
contentType: String
) = requestPattern.headersPattern.copy(
contentType = contentType
)

private fun <T: Parameter> namedExampleParams(operation: Operation, parameterType: Class<T>): Map<String, Map<String, String>> = operation.parameters.orEmpty()
.filterIsInstance(parameterType)
.fold(emptyMap<String, Map<String, String>>()) { acc, parameter ->
Expand Down
15 changes: 10 additions & 5 deletions core/src/main/kotlin/in/specmatic/core/HttpHeadersPattern.kt
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ import io.ktor.http.*

data class HttpHeadersPattern(
val pattern: Map<String, Pattern> = emptyMap(),
val ancestorHeaders: Map<String, Pattern>? = null
val ancestorHeaders: Map<String, Pattern>? = null,
val contentType: String? = null
) {
init {
val uniqueHeaders = pattern.keys.map { it.lowercase() }.distinct()
Expand Down Expand Up @@ -127,13 +128,17 @@ data class HttpHeadersPattern(
}

fun generate(resolver: Resolver): Map<String, String> {
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) {
Expand All @@ -154,17 +159,17 @@ data class HttpHeadersPattern(
fun newBasedOn(row: Row, resolver: Resolver): List<HttpHeadersPattern> =
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<HttpHeadersPattern> =
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<HttpHeadersPattern> =
allOrNothingCombinationIn(pattern) { pattern ->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
}
Expand Down
51 changes: 51 additions & 0 deletions core/src/test/kotlin/in/specmatic/conversions/OpenApiKtTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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<String, Value>) {

}
})

assertThat(result.success()).withFailMessage(result.report()).isTrue
}
}

data class CycleRoot(
Expand Down
12 changes: 12 additions & 0 deletions core/src/test/kotlin/in/specmatic/core/HttpRequestPatternTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
}

0 comments on commit b6fe37b

Please sign in to comment.