Skip to content

Commit be1989d

Browse files
committed
fix: IndexOutOfBoundsException when query param without value #1788
1 parent 78b265e commit be1989d

File tree

13 files changed

+128
-30
lines changed

13 files changed

+128
-30
lines changed

consumer/groovy/src/main/kotlin/au/com/dius/pact/consumer/groovy/BaseBuilder.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,7 @@ open class BaseBuilder(
130130
query: Any,
131131
matchers: MatchingRules,
132132
generators: Generators
133-
): Map<String, List<String>> {
133+
): Map<String, List<String?>> {
134134
return if (query is Map<*, *>) {
135135
query.entries.associate { (key, value) ->
136136
when (value) {

consumer/src/main/kotlin/au/com/dius/pact/consumer/dsl/PactDslRequestBase.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ open class PactDslRequestBase(
3737
@JvmField
3838
var requestHeaders: MutableMap<String, List<String>> = mutableMapOf()
3939
@JvmField
40-
var query: MutableMap<String, List<String>> = mutableMapOf()
40+
var query: MutableMap<String, List<String?>> = mutableMapOf()
4141
@JvmField
4242
var requestBody = missing()
4343
@JvmField

consumer/src/main/kotlin/au/com/dius/pact/consumer/dsl/PactDslRequestWithPath.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ open class PactDslRequestWithPath : PactDslRequestBase {
5555
path: String,
5656
requestMethod: String,
5757
requestHeaders: MutableMap<String, List<String>>,
58-
query: MutableMap<String, List<String>>,
58+
query: MutableMap<String, List<String?>>,
5959
requestBody: OptionalBody,
6060
requestMatchers: MatchingRules,
6161
requestGenerators: Generators,

core/matchers/src/main/kotlin/au/com/dius/pact/core/matchers/Mismatches.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -88,8 +88,8 @@ data class MethodMismatch(val expected: String, val actual: String) : Mismatch()
8888

8989
data class QueryMismatch(
9090
val queryParameter: String,
91-
val expected: String,
92-
val actual: String,
91+
val expected: String?,
92+
val actual: String?,
9393
val mismatch: String? = null,
9494
val path: String = "/"
9595
) : Mismatch() {

core/matchers/src/main/kotlin/au/com/dius/pact/core/matchers/QueryMatcher.kt

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@ object QueryMatcher : KLogging() {
88

99
private fun compare(
1010
parameter: String,
11-
expected: String,
12-
actual: String,
11+
expected: String?,
12+
actual: String?,
1313
context: MatchingContext
1414
): List<QueryMismatch> {
1515
return if (context.matcherDefined(listOf(parameter))) {
@@ -29,8 +29,8 @@ object QueryMatcher : KLogging() {
2929

3030
private fun compareQueryParameterValues(
3131
parameter: String,
32-
expected: List<String>,
33-
actual: List<String>,
32+
expected: List<String?>,
33+
actual: List<String?>,
3434
path: List<String>,
3535
context: MatchingContext
3636
): List<QueryMismatch> {
@@ -52,8 +52,8 @@ object QueryMatcher : KLogging() {
5252
@JvmStatic
5353
fun compareQuery(
5454
parameter: String,
55-
expected: List<String>,
56-
actual: List<String>,
55+
expected: List<String?>,
56+
actual: List<String?>,
5757
context: MatchingContext
5858
): List<QueryMismatch> {
5959
val path = listOf(parameter)

core/model/src/main/kotlin/au/com/dius/pact/core/model/BaseRequest.kt

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
package au.com.dius.pact.core.model
22

3-
import au.com.dius.pact.core.support.Json
43
import au.com.dius.pact.core.support.json.JsonValue
54
import java.io.ByteArrayOutputStream
65
import javax.mail.internet.InternetHeaders
@@ -45,12 +44,14 @@ abstract class BaseRequest : HttpPart() {
4544
fun isMultipartFileUpload() = determineContentType().isMultipartFormData()
4645

4746
companion object {
48-
fun parseQueryParametersToMap(query: JsonValue?): Map<String, List<String>> {
47+
@JvmStatic
48+
fun parseQueryParametersToMap(query: JsonValue?): Map<String, List<String?>> {
4949
return when (query) {
5050
null -> emptyMap()
5151
is JsonValue.Object -> query.entries.entries.associate { entry ->
5252
val list = when (val value = entry.value) {
53-
is JsonValue.Array -> value.values.map { Json.toString(it) }
53+
is JsonValue.Array -> value.values.map { it.asString() }
54+
is JsonValue.StringValue -> listOf(value.toString())
5455
else -> emptyList()
5556
}
5657
entry.key to list

core/model/src/main/kotlin/au/com/dius/pact/core/model/PactReader.kt

Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -152,22 +152,28 @@ private fun basicAuth(baseUrl: String, username: String, password: String, build
152152
* Parses the query string into a Map
153153
*/
154154
@JvmOverloads
155-
fun queryStringToMap(query: String?, decode: Boolean = true): Map<String, List<String>> {
155+
fun queryStringToMap(query: String?, decode: Boolean = true): Map<String, List<String?>> {
156156
return if (query.isNullOrEmpty()) {
157157
emptyMap()
158158
} else {
159159
query.split("&")
160-
.filter { it.isNotEmpty() }.map { val nv = it.split("=", limit = 2); nv[0] to nv[1] }
161-
.fold(mutableMapOf<String, MutableList<String>>()) { map, nameAndValue ->
162-
val name = if (decode) URLDecoder.decode(nameAndValue.first, "UTF-8") else nameAndValue.first
163-
val value = if (decode) URLDecoder.decode(nameAndValue.second, "UTF-8") else nameAndValue.second
164-
if (map.containsKey(name)) {
165-
map[name]!!.add(value)
166-
} else {
167-
map[name] = mutableListOf(value)
160+
.filter { it.isNotEmpty() }
161+
.map {
162+
val nv = it.split("=", limit = 2)
163+
val value = if (nv.size > 1) nv[1] else null
164+
nv[0] to value
165+
}
166+
.fold(mutableMapOf<String, MutableList<String?>>()) { map, nameAndValue ->
167+
val name = if (decode) URLDecoder.decode(nameAndValue.first, "UTF-8") else nameAndValue.first
168+
val value = if (nameAndValue.second != null && decode) URLDecoder.decode(nameAndValue.second, "UTF-8")
169+
else nameAndValue.second
170+
if (map.containsKey(name)) {
171+
map[name]!!.add(value)
172+
} else {
173+
map[name] = mutableListOf(value)
174+
}
175+
map
168176
}
169-
map
170-
}
171177
}
172178
}
173179

core/model/src/main/kotlin/au/com/dius/pact/core/model/Request.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import io.github.oshai.kotlinlogging.KLogging
1616
interface IRequest: IHttpPart {
1717
var method: String
1818
var path: String
19-
val query: MutableMap<String, List<String>>
19+
val query: MutableMap<String, List<String?>>
2020
override val headers: MutableMap<String, List<String>>
2121
override var body: OptionalBody
2222
override val matchingRules: MatchingRules
@@ -41,7 +41,7 @@ interface IRequest: IHttpPart {
4141
class Request @Suppress("LongParameterList") @JvmOverloads constructor(
4242
override var method: String = DEFAULT_METHOD,
4343
override var path: String = DEFAULT_PATH,
44-
override var query: MutableMap<String, List<String>> = mutableMapOf(),
44+
override var query: MutableMap<String, List<String?>> = mutableMapOf(),
4545
override var headers: MutableMap<String, List<String>> = mutableMapOf(),
4646
override var body: OptionalBody = OptionalBody.missing(),
4747
override var matchingRules: MatchingRules = MatchingRulesImpl(),

core/model/src/main/kotlin/au/com/dius/pact/core/model/RequestResponseInteraction.kt

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -137,9 +137,12 @@ open class RequestResponseInteraction @JvmOverloads constructor(
137137
return map
138138
}
139139

140-
private fun mapToQueryStr(query: Map<String, List<String>>): String {
140+
private fun mapToQueryStr(query: Map<String, List<String?>>): String {
141141
return query.entries.joinToString("&") { (k, v) ->
142-
v.joinToString("&") { "$k=${URLEncoder.encode(it, "UTF-8")}" }
142+
v.joinToString("&") {
143+
if (it != null) "$k=${URLEncoder.encode(it, "UTF-8")}"
144+
else k
145+
}
143146
}
144147
}
145148

core/model/src/main/kotlin/au/com/dius/pact/core/model/V4HttpParts.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ private fun headersFromJson(json: JsonValue): Map<String, List<String>> {
2828
data class HttpRequest @JvmOverloads constructor(
2929
override var method: String = "GET",
3030
override var path: String = "/",
31-
override var query: MutableMap<String, List<String>> = mutableMapOf(),
31+
override var query: MutableMap<String, List<String?>> = mutableMapOf(),
3232
override var headers: MutableMap<String, List<String>> = mutableMapOf(),
3333
override var body: OptionalBody = OptionalBody.missing(),
3434
override val matchingRules: MatchingRules = MatchingRulesImpl(),
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
package au.com.dius.pact.core.model
2+
3+
import au.com.dius.pact.core.support.json.JsonParser
4+
import au.com.dius.pact.core.support.json.JsonValue
5+
import spock.lang.Specification
6+
7+
class BaseRequestSpec extends Specification {
8+
def 'parseQueryParametersToMap'() {
9+
expect:
10+
BaseRequest.parseQueryParametersToMap(json) == value
11+
12+
where:
13+
14+
json | value
15+
null | [:]
16+
JsonValue.Null.INSTANCE | [:]
17+
JsonValue.True.INSTANCE | [:]
18+
JsonValue.False.INSTANCE | [:]
19+
new JsonValue.Integer(100) | [:]
20+
new JsonValue.Decimal(100.0) | [:]
21+
new JsonValue.Array([]) | [:]
22+
new JsonValue.StringValue('a=1&b=2') | [a: ['1'], b: ['2']]
23+
}
24+
25+
def 'parseQueryParametersToMap - with a JSON map'() {
26+
expect:
27+
BaseRequest.parseQueryParametersToMap(JsonParser.parseString(json).asObject()) == value
28+
29+
where:
30+
31+
json | value
32+
'{}' | [:]
33+
'{"a": "1"}' | [a: ['1']]
34+
'{"a": ["1"]}' | [a: ['1']]
35+
'{"a": ["", ""]}' | [a: ['', '']]
36+
'{"a": [null, ""]}' | [a: [null, '']]
37+
}
38+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
package au.com.dius.pact.core.model
2+
3+
import spock.lang.Issue
4+
import spock.lang.Specification
5+
6+
import static au.com.dius.pact.core.model.PactReaderKt.queryStringToMap
7+
8+
class PactReaderKtSpec extends Specification {
9+
def 'parsing a query string'() {
10+
expect:
11+
queryStringToMap(query) == result
12+
13+
where:
14+
15+
query | result
16+
null | [:]
17+
'' | [:]
18+
'p=1' | [p: ['1']]
19+
'p=1&q=2' | [p: ['1'], q: ['2']]
20+
'p=1&q=2&p=3' | [p: ['1', '3'], q: ['2']]
21+
'p=1&q=2=&p=3' | [p: ['1', '3'], q: ['2=']]
22+
'&&' | [:]
23+
}
24+
25+
@Issue('#1788')
26+
def 'parsing a query string with empty or missing values'() {
27+
expect:
28+
queryStringToMap(query) == result
29+
30+
where:
31+
32+
query | result
33+
'p=' | [p: ['']]
34+
'p=1&q=&p=3' | [p: ['1', '3'], q: ['']]
35+
'p&q=1&q=2' | [p: [null], q: ['1', '2']]
36+
'p&p&p' | [p: [null, null, null]]
37+
}
38+
}

core/model/src/test/groovy/au/com/dius/pact/core/model/RequestResponseInteractionSpec.groovy

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,18 @@ class RequestResponseInteractionSpec extends Specification {
135135
'include[]=course_image&include[]=favorites'
136136
}
137137

138+
@Issue('#1788')
139+
def 'correctly encodes the query parameters with no values or empty ones'() {
140+
given:
141+
request.query = [p: [null, null, null], q: ['', '', '']]
142+
143+
when:
144+
def map = interaction.toMap(PactSpecVersion.V2)
145+
146+
then:
147+
map.request.query == 'p&p&p&q=&q=&q='
148+
}
149+
138150
@Issue('#1611')
139151
def 'supports empty bodies'() {
140152
expect:

0 commit comments

Comments
 (0)