From a11b05cde527924fdc4cc988dc60f29e7c42783a Mon Sep 17 00:00:00 2001 From: Ronald Holshausen Date: Sat, 24 Jul 2021 16:09:46 +1000 Subject: [PATCH] fix: header values need to be parsed when loaded from older spec pact files #1398 --- .../dius/pact/core/matchers/HeaderMatcher.kt | 4 +- core/model/build.gradle | 1 + .../com/dius/pact/core/model/HeaderParser.kt | 30 ++++++++++++ .../au/com/dius/pact/core/model/Request.kt | 6 +-- .../au/com/dius/pact/core/model/Response.kt | 9 +--- .../pact/core/model/HeaderParserSpec.groovy | 27 +++++++++++ .../pact/core/model/PactReaderSpec.groovy | 46 +++++++++++++------ core/model/src/test/resources/v1-pact.json | 5 +- core/model/src/test/resources/v2-pact.json | 5 +- core/model/src/test/resources/v3-pact.json | 2 +- .../dius/pact/core/support/json/JsonValue.kt | 2 + 11 files changed, 106 insertions(+), 31 deletions(-) create mode 100644 core/model/src/main/kotlin/au/com/dius/pact/core/model/HeaderParser.kt create mode 100644 core/model/src/test/groovy/au/com/dius/pact/core/model/HeaderParserSpec.groovy diff --git a/core/matchers/src/main/kotlin/au/com/dius/pact/core/matchers/HeaderMatcher.kt b/core/matchers/src/main/kotlin/au/com/dius/pact/core/matchers/HeaderMatcher.kt index 3a43c4210c..3672c8d55e 100755 --- a/core/matchers/src/main/kotlin/au/com/dius/pact/core/matchers/HeaderMatcher.kt +++ b/core/matchers/src/main/kotlin/au/com/dius/pact/core/matchers/HeaderMatcher.kt @@ -31,7 +31,9 @@ object HeaderMatcher : KLogging() { @JvmStatic fun parseParameters(values: List): Map { - return values.map { it.split('=').map { it.trim() } }.associate { it.first() to it.component2() } + return values.map { value -> + value.split('=').map { it.trim() } + }.associate { it.first() to it.component2() } } fun stripWhiteSpaceAfterCommas(str: String): String = Regex(",\\s*").replace(str, ",") diff --git a/core/model/build.gradle b/core/model/build.gradle index 4b0fcfdc0c..1940006788 100644 --- a/core/model/build.gradle +++ b/core/model/build.gradle @@ -25,6 +25,7 @@ dependencies { compile "org.jetbrains.kotlin:kotlin-reflect:$kotlinVersion" implementation "org.slf4j:slf4j-api:${project.slf4jVersion}" implementation 'org.apache.tika:tika-core:1.27' + implementation 'io.ktor:ktor-http-jvm:1.3.1' testCompile "ch.qos.logback:logback-classic:${project.logbackVersion}" testCompile "io.github.http-builder-ng:http-builder-ng-apache:${project.httpBuilderVersion}" diff --git a/core/model/src/main/kotlin/au/com/dius/pact/core/model/HeaderParser.kt b/core/model/src/main/kotlin/au/com/dius/pact/core/model/HeaderParser.kt new file mode 100644 index 0000000000..e6e2fabb68 --- /dev/null +++ b/core/model/src/main/kotlin/au/com/dius/pact/core/model/HeaderParser.kt @@ -0,0 +1,30 @@ +package au.com.dius.pact.core.model + +import au.com.dius.pact.core.support.Json +import au.com.dius.pact.core.support.json.JsonValue +import io.ktor.http.HeaderValue +import io.ktor.http.parseHeaderValue + +object HeaderParser { + private val SINGLE_VALUE_HEADERS = setOf("date", "accept-datetime", "if-modified-since", "if-unmodified-since", + "expires", "retry-after") + + fun fromJson(key: String, value: JsonValue): List { + return when { + value is JsonValue.Array -> value.values.map { Json.toString(it).trim() } + SINGLE_VALUE_HEADERS.contains(key.toLowerCase()) -> listOf(Json.toString(value).trim()) + else -> { + val sval = Json.toString(value).trim() + parseHeaderValue(sval).map { hvToString(it) } + } + } + } + + private fun hvToString(headerValue: HeaderValue): String { + return if (headerValue.params.isEmpty()) { + headerValue.value.trim() + } else { + headerValue.value.trim() + ";" + headerValue.params.joinToString(";") { it.name + "=" + it.value } + } + } +} diff --git a/core/model/src/main/kotlin/au/com/dius/pact/core/model/Request.kt b/core/model/src/main/kotlin/au/com/dius/pact/core/model/Request.kt index d29dba226a..faa96d2ff7 100644 --- a/core/model/src/main/kotlin/au/com/dius/pact/core/model/Request.kt +++ b/core/model/src/main/kotlin/au/com/dius/pact/core/model/Request.kt @@ -105,11 +105,7 @@ class Request @JvmOverloads constructor( val query = parseQueryParametersToMap(json["query"]) val headers = if (json.has("headers") && json["headers"] is JsonValue.Object) { json["headers"].asObject().entries.entries.associate { (key, value) -> - if (value is JsonValue.Array) { - key to value.values.map { Json.toString(it) } - } else { - key to listOf(Json.toString(value).trim()) - } + key to HeaderParser.fromJson(key, value) } } else { emptyMap() diff --git a/core/model/src/main/kotlin/au/com/dius/pact/core/model/Response.kt b/core/model/src/main/kotlin/au/com/dius/pact/core/model/Response.kt index 7072b311f3..2aeb9ecf4a 100644 --- a/core/model/src/main/kotlin/au/com/dius/pact/core/model/Response.kt +++ b/core/model/src/main/kotlin/au/com/dius/pact/core/model/Response.kt @@ -5,7 +5,6 @@ import au.com.dius.pact.core.model.generators.GeneratorTestMode import au.com.dius.pact.core.model.generators.Generators import au.com.dius.pact.core.model.matchingrules.MatchingRules import au.com.dius.pact.core.model.matchingrules.MatchingRulesImpl -import au.com.dius.pact.core.support.Json import au.com.dius.pact.core.support.json.JsonValue import mu.KLogging @@ -34,7 +33,7 @@ class Response @JvmOverloads constructor( generators.applyGenerator(Category.HEADER, mode) { key, g -> r.headers[key] = listOf(g.generate(context).toString()) } - r.body = generators.applyBodyGenerators(r.body, ContentType.fromString(contentType()), context, mode) + r.body = generators.applyBodyGenerators(r.body, determineContentType(), context, mode) return r } @@ -80,11 +79,7 @@ class Response @JvmOverloads constructor( } val headers = if (json.has("headers") && json["headers"] is JsonValue.Object) { json["headers"].asObject().entries.entries.associate { (key, value) -> - if (value is JsonValue.Array) { - key to value.values.map { Json.toString(it) } - } else { - key to listOf(Json.toString(value).trim()) - } + key to HeaderParser.fromJson(key, value) } } else { emptyMap() diff --git a/core/model/src/test/groovy/au/com/dius/pact/core/model/HeaderParserSpec.groovy b/core/model/src/test/groovy/au/com/dius/pact/core/model/HeaderParserSpec.groovy new file mode 100644 index 0000000000..4b642e7437 --- /dev/null +++ b/core/model/src/test/groovy/au/com/dius/pact/core/model/HeaderParserSpec.groovy @@ -0,0 +1,27 @@ +package au.com.dius.pact.core.model + +import au.com.dius.pact.core.support.json.JsonValue +import spock.lang.Specification +import spock.lang.Unroll + +@SuppressWarnings('LineLength') +class HeaderParserSpec extends Specification { + + private static final String ACCEPT = + 'application/prs.hal-forms+json;q=1.0, application/hal+json;q=0.9, application/vnd.api+json;q=0.8, application/vnd.siren+json;q=0.8, application/vnd.collection+json;q=0.8, application/json;q=0.7, text/html;q=0.6, application/vnd.pactbrokerextended.v1+json;q=1.0' + + @Unroll + def 'loading string headers from JSON - #desc'() { + expect: + HeaderParser.INSTANCE.fromJson(key, new JsonValue.StringValue(value)) == result + + where: + + desc | key | value | result + 'simple header' | 'HeaderA' | 'A' | ['A'] + 'date header' | 'date' | 'Sat, 24 Jul 2021 04:16:53 GMT' | ['Sat, 24 Jul 2021 04:16:53 GMT'] + 'header with parameter' | 'content-type' | 'text/html; charset=utf-8' | ['text/html;charset=utf-8'] + 'header with multiple values' | 'access-control-allow-methods' | 'POST, GET, PUT, HEAD, DELETE, OPTIONS, PATCH' | ['POST', 'GET', 'PUT', 'HEAD', 'DELETE', 'OPTIONS', 'PATCH'] + 'header with multiple values with parameters' | 'Accept' | ACCEPT | ['application/prs.hal-forms+json;q=1.0', 'application/hal+json;q=0.9', 'application/vnd.api+json;q=0.8', 'application/vnd.siren+json;q=0.8', 'application/vnd.collection+json;q=0.8', 'application/json;q=0.7', 'text/html;q=0.6', 'application/vnd.pactbrokerextended.v1+json;q=1.0'] + } +} diff --git a/core/model/src/test/groovy/au/com/dius/pact/core/model/PactReaderSpec.groovy b/core/model/src/test/groovy/au/com/dius/pact/core/model/PactReaderSpec.groovy index 9c4cd55f94..c8e52aa003 100644 --- a/core/model/src/test/groovy/au/com/dius/pact/core/model/PactReaderSpec.groovy +++ b/core/model/src/test/groovy/au/com/dius/pact/core/model/PactReaderSpec.groovy @@ -33,28 +33,44 @@ class PactReaderSpec extends Specification { } def 'loads a pact with V1 version using existing loader'() { - given: - def pactUrl = PactReaderSpec.classLoader.getResource('v1-pact.json') + given: + def pactUrl = PactReaderSpec.classLoader.getResource('v1-pact.json') - when: - def pact = DefaultPactReader.INSTANCE.loadPact(pactUrl) + when: + def pact = DefaultPactReader.INSTANCE.loadPact(pactUrl) + def interaction = pact.interactions.first() - then: - pact instanceof RequestResponsePact - pact.source instanceof UrlPactSource - pact.metadata == [pactSpecification: [version: '2.0.0'], 'pact-jvm': [version: '']] + then: + pact instanceof RequestResponsePact + pact.source instanceof UrlPactSource + pact.metadata == [pactSpecification: [version: '2.0.0'], 'pact-jvm': [version: '']] + + interaction instanceof RequestResponseInteraction + interaction.response.headers['Content-Type'] == ['text/html'] + interaction.response.headers['access-control-allow-credentials'] == ['true'] + interaction.response.headers['access-control-allow-headers'] == ['Content-Type', 'Authorization'] + interaction.response.headers['access-control-allow-methods'] == ['POST', 'GET', 'PUT', 'HEAD', 'DELETE', 'OPTIONS', + 'PATCH'] } def 'loads a pact with V2 version using existing loader'() { - given: - def pactUrl = PactReaderSpec.classLoader.getResource('v2-pact.json') + given: + def pactUrl = PactReaderSpec.classLoader.getResource('v2-pact.json') - when: - def pact = DefaultPactReader.INSTANCE.loadPact(pactUrl) + when: + def pact = DefaultPactReader.INSTANCE.loadPact(pactUrl) + def interaction = pact.interactions.first() - then: - pact instanceof RequestResponsePact - pact.metadata == [pactSpecification: [version: '2.0.0'], 'pact-jvm': [version: '']] + then: + pact instanceof RequestResponsePact + pact.metadata == [pactSpecification: [version: '2.0.0'], 'pact-jvm': [version: '']] + + interaction instanceof RequestResponseInteraction + interaction.response.headers['Content-Type'] == ['text/html'] + interaction.response.headers['access-control-allow-credentials'] == ['true'] + interaction.response.headers['access-control-allow-headers'] == ['Content-Type', 'Authorization'] + interaction.response.headers['access-control-allow-methods'] == ['POST', 'GET', 'PUT', 'HEAD', 'DELETE', 'OPTIONS', + 'PATCH'] } def 'loads a pact with V3 version using V3 loader'() { diff --git a/core/model/src/test/resources/v1-pact.json b/core/model/src/test/resources/v1-pact.json index 273fe983b1..7dfe3a5942 100644 --- a/core/model/src/test/resources/v1-pact.json +++ b/core/model/src/test/resources/v1-pact.json @@ -16,7 +16,10 @@ "response": { "status": 200, "headers": { - "Content-Type": "text/html" + "Content-Type": "text/html", + "access-control-allow-credentials": "true", + "access-control-allow-headers": "Content-Type, Authorization", + "access-control-allow-methods": "POST, GET, PUT, HEAD, DELETE, OPTIONS, PATCH" }, "body": "\"That is some good Mallory.\"" } diff --git a/core/model/src/test/resources/v2-pact.json b/core/model/src/test/resources/v2-pact.json index f2556c9de9..bef9755d29 100644 --- a/core/model/src/test/resources/v2-pact.json +++ b/core/model/src/test/resources/v2-pact.json @@ -16,7 +16,10 @@ "response": { "status": 200, "headers": { - "Content-Type": "text/html" + "Content-Type": "text/html", + "access-control-allow-credentials": "true", + "access-control-allow-headers": "Content-Type, Authorization", + "access-control-allow-methods": "POST, GET, PUT, HEAD, DELETE, OPTIONS, PATCH" }, "body": "\"That is some good Mallory.\"" } diff --git a/core/model/src/test/resources/v3-pact.json b/core/model/src/test/resources/v3-pact.json index 82b6c05cc8..6b34d6333c 100644 --- a/core/model/src/test/resources/v3-pact.json +++ b/core/model/src/test/resources/v3-pact.json @@ -19,7 +19,7 @@ "response": { "status": 200, "headers": { - "Content-Type": "text/html" + "Content-Type": ["text/html"] }, "body": "\"That is some good Mallory.\"" } diff --git a/core/support/src/main/kotlin/au/com/dius/pact/core/support/json/JsonValue.kt b/core/support/src/main/kotlin/au/com/dius/pact/core/support/json/JsonValue.kt index b04f55dc4c..81d5fe1e90 100644 --- a/core/support/src/main/kotlin/au/com/dius/pact/core/support/json/JsonValue.kt +++ b/core/support/src/main/kotlin/au/com/dius/pact/core/support/json/JsonValue.kt @@ -15,8 +15,10 @@ sealed class JsonValue { class StringValue(val value: JsonToken.StringValue) : JsonValue() { constructor(value: CharArray) : this(JsonToken.StringValue(value)) + constructor(value: String) : this(JsonToken.StringValue(value.toCharArray())) override fun toString() = String(value.chars) } + object True : JsonValue() object False : JsonValue() object Null : JsonValue()