Skip to content

Commit

Permalink
Support of Matching Rules (#221)
Browse files Browse the repository at this point in the history
  • Loading branch information
ludorival authored Nov 29, 2024
1 parent da430d6 commit 5037c9d
Show file tree
Hide file tree
Showing 13 changed files with 159 additions and 80 deletions.
30 changes: 30 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,36 @@ init {

```

### Configure matching rules

You can specify matching rules for both requests and responses to make your contracts more flexible. This is useful when you want to define patterns rather than exact values.

```kotlin
every {
restTemplate.exchange(
match<URI> { it.path.contains("user-service") },
HttpMethod.GET,
any(),
any<ParameterizedTypeReference<List<User>>>()
)
} willRespondWith {
description("list users")
// Define rules for request headers
requestMatchingRules {
header("Authorization", Matcher(Matcher.MatchEnum.REGEX, "Bearer .*"))
}
// Define rules for response body
responseMatchingRules {
body("[*].id", Matcher(Matcher.MatchEnum.TYPE))
}
ResponseEntity.ok(listOf(USER_1, USER_2))
}
```

In this example:
- The request matching rule ensures the Authorization header matches the pattern "Bearer" followed by any string
- The response matching rule specifies that each user ID in the array should match by type rather than exact value

### Change the pact directory

By default, the generated pacts are stored in `src/test/resources/pacts`. You can configure that in the pact options:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ interface InteractionBuilder {

fun providerState(providerState: String, params: Map<String, Any?> ? = null): InteractionBuilder

fun requestMatchingRules(block: MatchingRulesBuilder.() -> Unit): InteractionBuilder

fun responseMatchingRules(block: MatchingRulesBuilder.() -> Unit): InteractionBuilder

fun build(request: Pact.Interaction.Request, response: Pact.Interaction.Response): Pact.Interaction

}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ package io.github.ludorival.pactjvm.mockk
class InteractionBuilderImpl : InteractionBuilder {

private var description: String? = null
private val requestMatchingRulesBuilder: MatchingRulesBuilder = MatchingRulesBuilder()
private val responseMatchingRulesBuilder: MatchingRulesBuilder = MatchingRulesBuilder()
private val providerStates: MutableList<Pact.Interaction.ProviderState> = mutableListOf()
override fun description(description: String): InteractionBuilder = apply { this.description = description }

Expand All @@ -15,9 +17,17 @@ class InteractionBuilderImpl : InteractionBuilder {
description = description
?: "${request.method} ${request.path}?${request.query ?: ""} returns ${response.status}",
providerStates = providerStates.ifEmpty { null },
request = request,
response = response
request = request.copy(matchingRules = requestMatchingRulesBuilder.build()) ,
response = response.copy(matchingRules = responseMatchingRulesBuilder.build())
)
}

override fun responseMatchingRules(block: MatchingRulesBuilder.() -> Unit): InteractionBuilder = apply {
responseMatchingRulesBuilder.apply(block)
}

override fun requestMatchingRules(block: MatchingRulesBuilder.() -> Unit): InteractionBuilder = apply {
requestMatchingRulesBuilder.apply(block)
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package io.github.ludorival.pactjvm.mockk

import com.fasterxml.jackson.annotation.JsonProperty

data class Matcher(
val match: MatchEnum,
val regex: String? = null,
val max: Number? = null,
val min: Number? = null,
val value: String? = null,
val format: String? = null
) {
enum class MatchEnum {
@JsonProperty("regex") REGEX,
@JsonProperty("type") TYPE,
@JsonProperty("boolean") BOOLEAN,
@JsonProperty("contentType") CONTENT_TYPE,
@JsonProperty("date") DATE,
@JsonProperty("datetime") DATETIME,
@JsonProperty("decimal") DECIMAL,
@JsonProperty("equality") EQUALITY,
@JsonProperty("include") INCLUDE,
@JsonProperty("integer") INTEGER,
@JsonProperty("null") NULL,
@JsonProperty("number") NUMBER,
@JsonProperty("time") TIME,
@JsonProperty("values") VALUES
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package io.github.ludorival.pactjvm.mockk

class MatchingRulesBuilder {
private val body = mutableMapOf<String, Matcher>()
private val header = mutableMapOf<String, Matcher>()
private val path = mutableMapOf<String, Matcher>()
private val query = mutableMapOf<String, Matcher>()

fun body(path: String, matcher: Matcher ): MatchingRulesBuilder = apply { this.body[path] = matcher }
fun header(path: String, matcher: Matcher ): MatchingRulesBuilder = apply { this.header[path] = matcher }
fun path(path: String, matcher: Matcher ): MatchingRulesBuilder = apply { this.path[path] = matcher }
fun query(path: String, matcher: Matcher ): MatchingRulesBuilder = apply { this.query[path] = matcher }

fun build(): Pact.Interaction.MatchingRules? =
Pact.Interaction.MatchingRules(
body = body.toMap().ifEmpty { null },
header = header.toMap().ifEmpty { null },
path = path.toMap().ifEmpty { null },
query = query.toMap().ifEmpty { null }
).takeIf { it.body != null || it.header != null || it.path != null || it.query != null }
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,17 +33,26 @@ data class Pact(
val path: String,
val query: String? = null,
val headers: Map<String, String>? = null,
val body: Any? = null
val body: Any? = null,
val matchingRules: MatchingRules? = null
) {
enum class Method {
GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS, TRACE
}
}

data class MatchingRules(
val body: Map<String, Matcher>? = null,
val header: Map<String, Matcher>? = null,
val path: Map<String, Matcher>? = null,
val query: Map<String, Matcher>? = null
)

data class Response(
val body: Any?,
val status: Int = 200,
val headers: Map<String, String>? = null
val headers: Map<String, String>? = null,
val matchingRules: MatchingRules? = null
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import com.fasterxml.jackson.core.JsonGenerator
import com.fasterxml.jackson.databind.JsonSerializer
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.databind.SerializerProvider
import com.fasterxml.jackson.annotation.JsonInclude

inline fun <reified T> serializerWith(crossinline supplier: (JsonGenerator) -> Unit) = object : JsonSerializer<T>() {
override fun serialize(value: T, gen: JsonGenerator, serializers: SerializerProvider?) {
Expand All @@ -17,5 +18,6 @@ inline fun <reified T> serializerAsDefault(defaultValue: String) =

fun Pact.Interaction.getConsumerName() = request.path.split("/").first { it.isNotBlank() }

internal val DEFAULT_OBJECT_MAPPER = ObjectMapper()

internal val DEFAULT_OBJECT_MAPPER = ObjectMapper().apply { setSerializationInclusion(JsonInclude.Include.NON_NULL) }

Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package io.github.ludorival.pactjvm.mockk.spring

import com.fasterxml.jackson.annotation.JsonInclude
import com.fasterxml.jackson.databind.ObjectMapper
import io.github.ludorival.pactjvm.mockk.PactOptions
import io.github.ludorival.pactjvm.mockk.pactOptions
Expand All @@ -18,7 +19,7 @@ object NonDeterministicPact : BeforeAllCallback, AfterAllCallback {
val CUSTOM_OBJECT_MAPPER: ObjectMapper = Jackson2ObjectMapperBuilder().serializerByType(
LocalDate::class.java,
serializerAsDefault<LocalDate>("2023-01-01")
).build()
).serializationInclusion(JsonInclude.Include.NON_NULL).build()


override fun beforeAll(context: ExtensionContext?) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import io.github.ludorival.pactjvm.mockk.spring.USER_ID
import io.github.ludorival.pactjvm.mockk.spring.fakeapplication.infra.shoppingservice.ShoppingList
import io.github.ludorival.pactjvm.mockk.willRespond
import io.github.ludorival.pactjvm.mockk.willRespondWith
import io.github.ludorival.pactjvm.mockk.Matcher
import io.mockk.every
import org.springframework.core.ParameterizedTypeReference
import org.springframework.http.HttpHeaders
Expand Down Expand Up @@ -76,6 +77,12 @@ fun RestTemplate.willListTwoShoppingLists() = every {
)
} willRespondWith {
description("list two shopping lists")
requestMatchingRules {
header("Authorization", Matcher(Matcher.MatchEnum.REGEX, "Bearer .*"))
}
responseMatchingRules {
body("[*].id", Matcher(Matcher.MatchEnum.TYPE))
}
println(
"I can have access to $args - $matcher - " +
"$self - $nArgs -${firstArg<Any>()} - ${secondArg<Any>()} ${thirdArg<Any>()} ${lastArg<Any>()}"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,10 @@
"name" : "shopping-service"
},
"interactions" : [ {
"providerStates" : null,
"description" : "POST /shopping-service/user/123? returns 200",
"request" : {
"method" : "POST",
"path" : "/shopping-service/user/123",
"query" : null,
"headers" : null,
"body" : {
"title" : "My shopping list"
}
Expand All @@ -29,7 +26,6 @@
"headers" : { }
}
}, {
"providerStates" : null,
"description" : "list two shopping lists",
"request" : {
"method" : "GET",
Expand All @@ -38,7 +34,14 @@
"headers" : {
"Content-Type" : "application/json"
},
"body" : null
"matchingRules" : {
"header" : {
"Authorization" : {
"match" : "regex",
"regex" : "Bearer .*"
}
}
}
},
"response" : {
"body" : [ {
Expand Down Expand Up @@ -71,49 +74,43 @@
"createdAt" : "2023-01-01"
} ],
"status" : 200,
"headers" : { }
"headers" : { },
"matchingRules" : {
"body" : {
"[*].id" : {
"match" : "type"
}
}
}
}
}, {
"providerStates" : null,
"description" : "delete shopping item",
"request" : {
"method" : "DELETE",
"path" : "/shopping-service/user/123/list/2",
"query" : null,
"headers" : null,
"body" : null
"path" : "/shopping-service/user/123/list/2"
},
"response" : {
"body" : null,
"status" : 200,
"headers" : { }
}
}, {
"providerStates" : null,
"description" : "update shopping list",
"request" : {
"method" : "PUT",
"path" : "/shopping-service/user/123/list/2",
"query" : null,
"headers" : null,
"body" : {
"name" : "My updated shopping list"
}
},
"response" : {
"body" : null,
"status" : 200,
"headers" : { }
}
}, {
"providerStates" : null,
"description" : "GET /shopping-service/user/123/list/1? returns 200",
"request" : {
"method" : "GET",
"path" : "/shopping-service/user/123/list/1",
"query" : null,
"headers" : null,
"body" : null
"path" : "/shopping-service/user/123/list/1"
},
"response" : {
"body" : {
Expand All @@ -135,13 +132,10 @@
"headers" : { }
}
}, {
"providerStates" : null,
"description" : "Patch a shopping item",
"request" : {
"method" : "PATCH",
"path" : "/shopping-service/user/123/list/1",
"query" : null,
"headers" : null,
"body" : {
"id" : 2,
"name" : "Banana",
Expand All @@ -159,15 +153,12 @@
}
}, {
"providerStates" : [ {
"name" : "The request should return a 400 Bad request",
"params" : null
"name" : "The request should return a 400 Bad request"
} ],
"description" : "should return a 400 Bad request",
"request" : {
"method" : "POST",
"path" : "/shopping-service/user/123",
"query" : null,
"headers" : null,
"body" : {
"title" : "Unexpected character \\s"
}
Expand All @@ -178,13 +169,10 @@
"headers" : { }
}
}, {
"providerStates" : null,
"description" : "create empty shopping list",
"request" : {
"method" : "POST",
"path" : "/shopping-service/user/123",
"query" : null,
"headers" : null,
"body" : {
"title" : "My Shopping list"
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,10 @@
"name" : "user-service"
},
"interactions" : [ {
"providerStates" : null,
"description" : "PUT /user-service/v1/user/123? returns 200",
"request" : {
"method" : "PUT",
"path" : "/user-service/v1/user/123",
"query" : null,
"headers" : { },
"body" : {
"preferredShoppingListId" : 1
Expand Down Expand Up @@ -39,10 +37,7 @@
"description" : "get the user profile",
"request" : {
"method" : "GET",
"path" : "/user-service/v1/user/123",
"query" : null,
"headers" : null,
"body" : null
"path" : "/user-service/v1/user/123"
},
"response" : {
"body" : {
Expand Down
Loading

0 comments on commit 5037c9d

Please sign in to comment.