diff --git a/README.md b/README.md index cf343fc..159077b 100644 --- a/README.md +++ b/README.md @@ -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 { it.path.contains("user-service") }, + HttpMethod.GET, + any(), + any>>() + ) +} 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: diff --git a/pact-jvm-mockk-core/src/main/kotlin/io/github/ludorival/pactjvm/mockk/InteractionBuilder.kt b/pact-jvm-mockk-core/src/main/kotlin/io/github/ludorival/pactjvm/mockk/InteractionBuilder.kt index 5fc5ae9..abe0d20 100644 --- a/pact-jvm-mockk-core/src/main/kotlin/io/github/ludorival/pactjvm/mockk/InteractionBuilder.kt +++ b/pact-jvm-mockk-core/src/main/kotlin/io/github/ludorival/pactjvm/mockk/InteractionBuilder.kt @@ -5,6 +5,10 @@ interface InteractionBuilder { fun providerState(providerState: String, params: Map ? = 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 + } diff --git a/pact-jvm-mockk-core/src/main/kotlin/io/github/ludorival/pactjvm/mockk/InteractionBuilderImpl.kt b/pact-jvm-mockk-core/src/main/kotlin/io/github/ludorival/pactjvm/mockk/InteractionBuilderImpl.kt index d0c70f8..efb6251 100644 --- a/pact-jvm-mockk-core/src/main/kotlin/io/github/ludorival/pactjvm/mockk/InteractionBuilderImpl.kt +++ b/pact-jvm-mockk-core/src/main/kotlin/io/github/ludorival/pactjvm/mockk/InteractionBuilderImpl.kt @@ -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 = mutableListOf() override fun description(description: String): InteractionBuilder = apply { this.description = description } @@ -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) + } + } \ No newline at end of file diff --git a/pact-jvm-mockk-core/src/main/kotlin/io/github/ludorival/pactjvm/mockk/Matcher.kt b/pact-jvm-mockk-core/src/main/kotlin/io/github/ludorival/pactjvm/mockk/Matcher.kt new file mode 100644 index 0000000..f2157c2 --- /dev/null +++ b/pact-jvm-mockk-core/src/main/kotlin/io/github/ludorival/pactjvm/mockk/Matcher.kt @@ -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 + } +} diff --git a/pact-jvm-mockk-core/src/main/kotlin/io/github/ludorival/pactjvm/mockk/MatchingRulesBuilder.kt b/pact-jvm-mockk-core/src/main/kotlin/io/github/ludorival/pactjvm/mockk/MatchingRulesBuilder.kt new file mode 100644 index 0000000..d372036 --- /dev/null +++ b/pact-jvm-mockk-core/src/main/kotlin/io/github/ludorival/pactjvm/mockk/MatchingRulesBuilder.kt @@ -0,0 +1,21 @@ +package io.github.ludorival.pactjvm.mockk + +class MatchingRulesBuilder { + private val body = mutableMapOf() + private val header = mutableMapOf() + private val path = mutableMapOf() + private val query = mutableMapOf() + + 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 } + } diff --git a/pact-jvm-mockk-core/src/main/kotlin/io/github/ludorival/pactjvm/mockk/Pact.kt b/pact-jvm-mockk-core/src/main/kotlin/io/github/ludorival/pactjvm/mockk/Pact.kt index ae2d8b8..2c6b6da 100644 --- a/pact-jvm-mockk-core/src/main/kotlin/io/github/ludorival/pactjvm/mockk/Pact.kt +++ b/pact-jvm-mockk-core/src/main/kotlin/io/github/ludorival/pactjvm/mockk/Pact.kt @@ -33,17 +33,26 @@ data class Pact( val path: String, val query: String? = null, val headers: Map? = 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? = null, + val header: Map? = null, + val path: Map? = null, + val query: Map? = null + ) + data class Response( val body: Any?, val status: Int = 200, - val headers: Map? = null + val headers: Map? = null, + val matchingRules: MatchingRules? = null ) } diff --git a/pact-jvm-mockk-core/src/main/kotlin/io/github/ludorival/pactjvm/mockk/Utils.kt b/pact-jvm-mockk-core/src/main/kotlin/io/github/ludorival/pactjvm/mockk/Utils.kt index 305ca28..6709b46 100644 --- a/pact-jvm-mockk-core/src/main/kotlin/io/github/ludorival/pactjvm/mockk/Utils.kt +++ b/pact-jvm-mockk-core/src/main/kotlin/io/github/ludorival/pactjvm/mockk/Utils.kt @@ -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 serializerWith(crossinline supplier: (JsonGenerator) -> Unit) = object : JsonSerializer() { override fun serialize(value: T, gen: JsonGenerator, serializers: SerializerProvider?) { @@ -17,5 +18,6 @@ inline fun 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) } diff --git a/pact-jvm-mockk-spring/src/test/kotlin/io/github/ludorival/pactjvm/mockk/spring/NonDeterministicPact.kt b/pact-jvm-mockk-spring/src/test/kotlin/io/github/ludorival/pactjvm/mockk/spring/NonDeterministicPact.kt index 94735a5..d9a858b 100644 --- a/pact-jvm-mockk-spring/src/test/kotlin/io/github/ludorival/pactjvm/mockk/spring/NonDeterministicPact.kt +++ b/pact-jvm-mockk-spring/src/test/kotlin/io/github/ludorival/pactjvm/mockk/spring/NonDeterministicPact.kt @@ -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 @@ -18,7 +19,7 @@ object NonDeterministicPact : BeforeAllCallback, AfterAllCallback { val CUSTOM_OBJECT_MAPPER: ObjectMapper = Jackson2ObjectMapperBuilder().serializerByType( LocalDate::class.java, serializerAsDefault("2023-01-01") - ).build() + ).serializationInclusion(JsonInclude.Include.NON_NULL).build() override fun beforeAll(context: ExtensionContext?) { diff --git a/pact-jvm-mockk-spring/src/test/kotlin/io/github/ludorival/pactjvm/mockk/spring/contracts/ShoppingServiceContracts.kt b/pact-jvm-mockk-spring/src/test/kotlin/io/github/ludorival/pactjvm/mockk/spring/contracts/ShoppingServiceContracts.kt index 20423b2..91247bc 100644 --- a/pact-jvm-mockk-spring/src/test/kotlin/io/github/ludorival/pactjvm/mockk/spring/contracts/ShoppingServiceContracts.kt +++ b/pact-jvm-mockk-spring/src/test/kotlin/io/github/ludorival/pactjvm/mockk/spring/contracts/ShoppingServiceContracts.kt @@ -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 @@ -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()} - ${secondArg()} ${thirdArg()} ${lastArg()}" diff --git a/pact-jvm-mockk-spring/src/test/resources/pacts-deterministic/shopping-list-shopping-service.json b/pact-jvm-mockk-spring/src/test/resources/pacts-deterministic/shopping-list-shopping-service.json index 52fe1a9..35e8756 100644 --- a/pact-jvm-mockk-spring/src/test/resources/pacts-deterministic/shopping-list-shopping-service.json +++ b/pact-jvm-mockk-spring/src/test/resources/pacts-deterministic/shopping-list-shopping-service.json @@ -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" } @@ -29,7 +26,6 @@ "headers" : { } } }, { - "providerStates" : null, "description" : "list two shopping lists", "request" : { "method" : "GET", @@ -38,7 +34,14 @@ "headers" : { "Content-Type" : "application/json" }, - "body" : null + "matchingRules" : { + "header" : { + "Authorization" : { + "match" : "regex", + "regex" : "Bearer .*" + } + } + } }, "response" : { "body" : [ { @@ -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" : { @@ -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", @@ -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" } @@ -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" } diff --git a/pact-jvm-mockk-spring/src/test/resources/pacts-deterministic/shopping-list-user-service.json b/pact-jvm-mockk-spring/src/test/resources/pacts-deterministic/shopping-list-user-service.json index 7850af1..0c5c9ce 100644 --- a/pact-jvm-mockk-spring/src/test/resources/pacts-deterministic/shopping-list-user-service.json +++ b/pact-jvm-mockk-spring/src/test/resources/pacts-deterministic/shopping-list-user-service.json @@ -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 @@ -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" : { diff --git a/pact-jvm-mockk-spring/src/test/resources/pacts/shopping-list-shopping-service.json b/pact-jvm-mockk-spring/src/test/resources/pacts/shopping-list-shopping-service.json index 52fe1a9..35e8756 100644 --- a/pact-jvm-mockk-spring/src/test/resources/pacts/shopping-list-shopping-service.json +++ b/pact-jvm-mockk-spring/src/test/resources/pacts/shopping-list-shopping-service.json @@ -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" } @@ -29,7 +26,6 @@ "headers" : { } } }, { - "providerStates" : null, "description" : "list two shopping lists", "request" : { "method" : "GET", @@ -38,7 +34,14 @@ "headers" : { "Content-Type" : "application/json" }, - "body" : null + "matchingRules" : { + "header" : { + "Authorization" : { + "match" : "regex", + "regex" : "Bearer .*" + } + } + } }, "response" : { "body" : [ { @@ -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" : { @@ -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", @@ -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" } @@ -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" } diff --git a/pact-jvm-mockk-spring/src/test/resources/pacts/shopping-list-user-service.json b/pact-jvm-mockk-spring/src/test/resources/pacts/shopping-list-user-service.json index 7850af1..0c5c9ce 100644 --- a/pact-jvm-mockk-spring/src/test/resources/pacts/shopping-list-user-service.json +++ b/pact-jvm-mockk-spring/src/test/resources/pacts/shopping-list-user-service.json @@ -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 @@ -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" : {