From f3018eebd8610dd2353cce66b2a5ae3275ec326e Mon Sep 17 00:00:00 2001 From: Ludovic Dorival Date: Tue, 21 Jan 2025 19:12:44 +0100 Subject: [PATCH] Support of Spring RabbitMQ Fixes #256 --- .idea/.gitignore | 8 - .idea/gradle.xml | 20 -- .idea/inspectionProfiles/Project_Default.xml | 6 - .idea/kotlinc.xml | 6 - .idea/misc.xml | 10 - .idea/vcs.xml | 6 - README.md | 34 ++- build.gradle.kts | 2 + .../mock/spring/SpringRabbitMQMockAdapter.kt | 61 +++++ .../spring/SpringRestTemplateMockAdapter.kt | 7 +- .../test/orderservice/OrderConsumerService.kt | 15 ++ .../mock/test/orderservice/OrderMessage.kt | 29 +++ .../orderservice/OrderServiceApplication.kt | 15 ++ .../orderservice/RabbitMQConfiguration.kt | 37 +++ .../shoppingservice/config/RabbitMQConfig.kt | 30 +++ .../messaging/ShoppingListOrderPublisher.kt | 20 ++ .../src/main/resources/application.yml | 10 + .../pactjvm/mock/test/MockitoAdapterTest.java | 2 +- .../mock/test/MockitoCoverageTest.java | 22 +- .../pactjvm/mock/test/DeterministicPact.kt | 2 +- .../pactjvm/mock/test/MockkCoverageTest.kt | 30 ++- .../pactjvm/mock/test/NoPactConsumerTest.kt | 5 +- .../pactjvm/mock/test/NonDeterministicPact.kt | 11 +- .../mock/test/rabbitmq/RabbitMQPactTest.kt | 92 ++++++++ .../test/verifier/RabbitMQPactVerifier.kt | 74 ++++++ .../verifier/ShoppingServicePactVerifier.kt | 2 +- .../pacts/order-service-shopping-list.json | 49 ++++ .../pacts/shopping-list-shopping-service.json | 222 ------------------ .../pactjvm/mock/PactConfiguration.kt | 4 +- .../github/ludorival/pactjvm/mock/PactMock.kt | 27 ++- .../ludorival/pactjvm/mock/PactMockAdapter.kt | 2 +- .../ludorival/pactjvm/mock/PactToWrite.kt | 38 +-- .../io/github/ludorival/pactjvm/mock/Utils.kt | 9 +- 33 files changed, 565 insertions(+), 342 deletions(-) delete mode 100644 .idea/.gitignore delete mode 100644 .idea/gradle.xml delete mode 100644 .idea/inspectionProfiles/Project_Default.xml delete mode 100644 .idea/kotlinc.xml delete mode 100644 .idea/misc.xml delete mode 100644 .idea/vcs.xml create mode 100644 pact-jvm-mock-spring/src/main/kotlin/io/github/ludorival/pactjvm/mock/spring/SpringRabbitMQMockAdapter.kt create mode 100644 pact-jvm-mock-test/src/main/kotlin/io/github/ludorival/pactjvm/mock/test/orderservice/OrderConsumerService.kt create mode 100644 pact-jvm-mock-test/src/main/kotlin/io/github/ludorival/pactjvm/mock/test/orderservice/OrderMessage.kt create mode 100644 pact-jvm-mock-test/src/main/kotlin/io/github/ludorival/pactjvm/mock/test/orderservice/OrderServiceApplication.kt create mode 100644 pact-jvm-mock-test/src/main/kotlin/io/github/ludorival/pactjvm/mock/test/orderservice/RabbitMQConfiguration.kt create mode 100644 pact-jvm-mock-test/src/main/kotlin/io/github/ludorival/pactjvm/mock/test/shoppingservice/config/RabbitMQConfig.kt create mode 100644 pact-jvm-mock-test/src/main/kotlin/io/github/ludorival/pactjvm/mock/test/shoppingservice/messaging/ShoppingListOrderPublisher.kt create mode 100644 pact-jvm-mock-test/src/test/kotlin/io/github/ludorival/pactjvm/mock/test/rabbitmq/RabbitMQPactTest.kt create mode 100644 pact-jvm-mock-test/src/test/kotlin/io/github/ludorival/pactjvm/mock/test/verifier/RabbitMQPactVerifier.kt create mode 100644 pact-jvm-mock-test/src/test/resources/pacts/order-service-shopping-list.json delete mode 100644 pact-jvm-mock-test/src/test/resources/pacts/shopping-list-shopping-service.json diff --git a/.idea/.gitignore b/.idea/.gitignore deleted file mode 100644 index 13566b8..0000000 --- a/.idea/.gitignore +++ /dev/null @@ -1,8 +0,0 @@ -# Default ignored files -/shelf/ -/workspace.xml -# Editor-based HTTP Client requests -/httpRequests/ -# Datasource local storage ignored files -/dataSources/ -/dataSources.local.xml diff --git a/.idea/gradle.xml b/.idea/gradle.xml deleted file mode 100644 index 4780b98..0000000 --- a/.idea/gradle.xml +++ /dev/null @@ -1,20 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml deleted file mode 100644 index df543e3..0000000 --- a/.idea/inspectionProfiles/Project_Default.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - \ No newline at end of file diff --git a/.idea/kotlinc.xml b/.idea/kotlinc.xml deleted file mode 100644 index 0fc3113..0000000 --- a/.idea/kotlinc.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml deleted file mode 100644 index fe0b0da..0000000 --- a/.idea/misc.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml deleted file mode 100644 index 94a25f7..0000000 --- a/.idea/vcs.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/README.md b/README.md index 82ca49b..62d61fc 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ and reducing the risk of human error. - Automatically generate Pact contracts from existing [Mockk](https://github.com/mockk/mockk) mocks or [Mockito](https://github.com/mockito/mockito) mocks - Supports all common mock interactions, such as method calls and property accesses -- Compatible with Spring Rest Template client and fully extensible +- Compatible with Spring Rest Template client and Spring RabbitMQ messaging - Easy to integrate with your existing testing workflow ## Getting Started in 5 minutes @@ -37,7 +37,7 @@ Or if you are using Maven: **Maven** -````xml +```xml io.github.ludorival @@ -61,7 +61,7 @@ Or if you are using Maven: ${pactJvmMockVersion} test -```` +``` ### Configure the pacts @@ -77,6 +77,18 @@ import io.github.ludorival.pactjvm.mock.spring.SpringRestTemplateMockAdapter object MyServicePactConfig : PactConfiguration("my-service", SpringRestTemplateMockAdapter()) ``` +For RabbitMQ messaging, you can use the `SpringRabbitMQMockAdapter`: + +```kotlin +import io.github.ludorival.pactjvm.mock.PactConfiguration +import io.github.ludorival.pactjvm.mock.spring.SpringRabbitMQMockAdapter +import com.fasterxml.jackson.databind.ObjectMapper + +object MyServicePactConfig : PactConfiguration( + SpringRabbitMQMockAdapter("my-service", customObjectMapper) // ObjectMapper is optional +) +``` + ### Extend your tests files Then, to start writing contract, @@ -107,6 +119,22 @@ every { uponReceiving { restTemplate.getForEntity(match { it.contains("user-service") }, UserProfile::class.java) } returns ResponseEntity.ok(USER_PROFILE) + +// For RabbitMQ messaging: +uponReceiving { + rabbitTemplate.convertAndSend( + any(), // exchange + any(), // routing key + any() // message + ) +}.withDescription { + "Order message sent" +}.given { + state("order created", mapOf( + "exchange" to "orders", + "routing_key" to "order.created" + )) +}.returns(Unit) ``` All your existing Mockk matchers and features continue to work exactly the same way. The library supports: diff --git a/build.gradle.kts b/build.gradle.kts index 6dd8721..5628e16 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -191,6 +191,7 @@ project(":pact-jvm-mock-spring") { api(project(":pact-jvm-mock")) implementation(kotlin("stdlib-jdk8")) compileOnly("org.springframework:spring-web:6.2.1") + compileOnly("org.springframework.amqp:spring-rabbit:3.2.1") compileOnly("com.fasterxml.jackson.core:jackson-databind:2.18.2") } } @@ -200,6 +201,7 @@ project(":pact-jvm-mock-test") { implementation(kotlin("stdlib-jdk8")) implementation("org.springframework.boot:spring-boot-starter-web:3.0.2") implementation("org.springframework:spring-web:6.0.4") + implementation("org.springframework.boot:spring-boot-starter-amqp:3.0.2") testImplementation("io.mockk:mockk:1.13.16") testImplementation(project(":pact-jvm-mock-spring")) diff --git a/pact-jvm-mock-spring/src/main/kotlin/io/github/ludorival/pactjvm/mock/spring/SpringRabbitMQMockAdapter.kt b/pact-jvm-mock-spring/src/main/kotlin/io/github/ludorival/pactjvm/mock/spring/SpringRabbitMQMockAdapter.kt new file mode 100644 index 0000000..36c7ca6 --- /dev/null +++ b/pact-jvm-mock-spring/src/main/kotlin/io/github/ludorival/pactjvm/mock/spring/SpringRabbitMQMockAdapter.kt @@ -0,0 +1,61 @@ +package io.github.ludorival.pactjvm.mock.spring + +import au.com.dius.pact.core.model.OptionalBody +import au.com.dius.pact.core.model.ProviderState +import au.com.dius.pact.core.model.messaging.Message +import au.com.dius.pact.core.model.matchingrules.MatchingRulesImpl +import io.github.ludorival.pactjvm.mock.Call +import io.github.ludorival.pactjvm.mock.InteractionBuilder +import io.github.ludorival.pactjvm.mock.PactMockAdapter +import org.springframework.amqp.rabbit.core.RabbitTemplate +import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.databind.SerializationFeature + +open class SpringRabbitMQMockAdapter( + private val provider: String, + private val objectMapper: ObjectMapper = defaultObjectMapper() +) : PactMockAdapter() { + + constructor(provider: String) : this(provider, defaultObjectMapper()) + + override fun support(call: Call<*>): Boolean { + return call.self is RabbitTemplate + } + + override fun buildInteraction( + interactionBuilder: InteractionBuilder, + providerName: String + ): Message { + val call = interactionBuilder.call + val exchange = call.args.firstOrNull { it is String } as? String ?: "" + val routingKey = call.args.getOrNull(1) as? String ?: "" + val message = call.args.lastOrNull() + + val messageContent = when (message) { + is String -> message + null -> "" + else -> objectMapper.writeValueAsString(message) + } + + return interactionBuilder.build { Message( + description = description, + providerStates = providerStates, + contents = OptionalBody.body(messageContent.toByteArray()), + metadata = mutableMapOf( + "exchange" to exchange, + "routing_key" to routingKey + ), + matchingRules = requestMatchingRules + ) + } + } + + override fun determineConsumerAndProvider(call: Call<*>): Pair { + val exchange = call.args.firstOrNull { it is String } as? String + return Pair(exchange?.split(".")?.firstOrNull() ?: "default", provider) + } + + companion object { + private fun defaultObjectMapper(): ObjectMapper = ObjectMapper() + } +} \ No newline at end of file diff --git a/pact-jvm-mock-spring/src/main/kotlin/io/github/ludorival/pactjvm/mock/spring/SpringRestTemplateMockAdapter.kt b/pact-jvm-mock-spring/src/main/kotlin/io/github/ludorival/pactjvm/mock/spring/SpringRestTemplateMockAdapter.kt index 678f945..90d660a 100644 --- a/pact-jvm-mock-spring/src/main/kotlin/io/github/ludorival/pactjvm/mock/spring/SpringRestTemplateMockAdapter.kt +++ b/pact-jvm-mock-spring/src/main/kotlin/io/github/ludorival/pactjvm/mock/spring/SpringRestTemplateMockAdapter.kt @@ -15,9 +15,10 @@ import io.github.ludorival.pactjvm.mock.* import org.springframework.http.* @Suppress("TooManyFunctions") -class SpringRestTemplateMockAdapter(private val objectMapperByProvider: (String) -> ObjectMapper? = { null }) : +open class SpringRestTemplateMockAdapter(private val consumer: String, private val objectMapperByProvider: (String) -> ObjectMapper? = { null }) : PactMockAdapter() { + constructor(consumer: String) : this(consumer, { null }) private val defaultObjectMapper = ObjectMapper().apply { setSerializationInclusion(JsonInclude.Include.NON_NULL) } @@ -73,9 +74,9 @@ class SpringRestTemplateMockAdapter(private val objectMapperByProvider: (String) } } - override fun determineProvider(call: Call<*>): String { + override fun determineConsumerAndProvider(call: Call<*>): Pair { val uri = call.getUri() - return uri.path.split("/").first { it.isNotBlank() } + return Pair(consumer, uri.path.split("/").first { it.isNotBlank() }) } private fun serializeBody( diff --git a/pact-jvm-mock-test/src/main/kotlin/io/github/ludorival/pactjvm/mock/test/orderservice/OrderConsumerService.kt b/pact-jvm-mock-test/src/main/kotlin/io/github/ludorival/pactjvm/mock/test/orderservice/OrderConsumerService.kt new file mode 100644 index 0000000..8e8dfcf --- /dev/null +++ b/pact-jvm-mock-test/src/main/kotlin/io/github/ludorival/pactjvm/mock/test/orderservice/OrderConsumerService.kt @@ -0,0 +1,15 @@ +package io.github.ludorival.pactjvm.mock.test.orderservice + +import io.github.ludorival.pactjvm.mock.test.shoppingservice.config.RabbitMQConfig +import org.springframework.amqp.rabbit.annotation.RabbitListener +import org.springframework.stereotype.Service + +@Service +open class OrderConsumerService { + + @RabbitListener(queues = [RabbitMQConfig.ROUTING_KEY]) + fun handleShoppingListOrdered(orderMessage: OrderMessage) { + // Process the order message + println("Received order message: $orderMessage") + } +} \ No newline at end of file diff --git a/pact-jvm-mock-test/src/main/kotlin/io/github/ludorival/pactjvm/mock/test/orderservice/OrderMessage.kt b/pact-jvm-mock-test/src/main/kotlin/io/github/ludorival/pactjvm/mock/test/orderservice/OrderMessage.kt new file mode 100644 index 0000000..28cc748 --- /dev/null +++ b/pact-jvm-mock-test/src/main/kotlin/io/github/ludorival/pactjvm/mock/test/orderservice/OrderMessage.kt @@ -0,0 +1,29 @@ +package io.github.ludorival.pactjvm.mock.test.orderservice + +import io.github.ludorival.pactjvm.mock.test.shoppingservice.ShoppingList + +data class OrderMessage( + val shoppingListId: String, + val userId: Long, + val items: List +) { + data class OrderItem( + val name: String, + val quantity: Int + ) + + companion object { + fun fromShoppingList(shoppingList: ShoppingList): OrderMessage { + return OrderMessage( + shoppingListId = shoppingList.id.toString(), + userId = shoppingList.userId, + items = shoppingList.items.map { item -> + OrderItem( + name = item.name, + quantity = item.quantity + ) + } + ) + } + } +} \ No newline at end of file diff --git a/pact-jvm-mock-test/src/main/kotlin/io/github/ludorival/pactjvm/mock/test/orderservice/OrderServiceApplication.kt b/pact-jvm-mock-test/src/main/kotlin/io/github/ludorival/pactjvm/mock/test/orderservice/OrderServiceApplication.kt new file mode 100644 index 0000000..3307cda --- /dev/null +++ b/pact-jvm-mock-test/src/main/kotlin/io/github/ludorival/pactjvm/mock/test/orderservice/OrderServiceApplication.kt @@ -0,0 +1,15 @@ +package io.github.ludorival.pactjvm.mock.test.orderservice + +import org.springframework.boot.autoconfigure.SpringBootApplication +import org.springframework.context.annotation.ComponentScan + +@SpringBootApplication +@ComponentScan(basePackages = ["io.github.ludorival.pactjvm.mock.test.orderservice"]) +open class OrderServiceApplication { + companion object { + @JvmStatic + fun main(args: Array) { + org.springframework.boot.runApplication(*args) + } + } +} \ No newline at end of file diff --git a/pact-jvm-mock-test/src/main/kotlin/io/github/ludorival/pactjvm/mock/test/orderservice/RabbitMQConfiguration.kt b/pact-jvm-mock-test/src/main/kotlin/io/github/ludorival/pactjvm/mock/test/orderservice/RabbitMQConfiguration.kt new file mode 100644 index 0000000..797007b --- /dev/null +++ b/pact-jvm-mock-test/src/main/kotlin/io/github/ludorival/pactjvm/mock/test/orderservice/RabbitMQConfiguration.kt @@ -0,0 +1,37 @@ +package io.github.ludorival.pactjvm.mock.test.orderservice + +import org.springframework.amqp.core.Binding +import org.springframework.amqp.core.BindingBuilder +import org.springframework.amqp.core.Queue +import org.springframework.amqp.core.TopicExchange +import org.springframework.amqp.rabbit.connection.ConnectionFactory +import org.springframework.amqp.rabbit.core.RabbitTemplate +import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter +import org.springframework.amqp.support.converter.MessageConverter +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.boot.autoconfigure.amqp.RabbitAutoConfiguration +import io.github.ludorival.pactjvm.mock.test.shoppingservice.config.RabbitMQConfig + +@Configuration +class RabbitMQConfiguration { + + @Bean + fun queue(): Queue = Queue(RabbitMQConfig.ROUTING_KEY) + + @Bean + fun exchange(): TopicExchange = TopicExchange(RabbitMQConfig.EXCHANGE_NAME) + + @Bean + fun binding(queue: Queue, exchange: TopicExchange): Binding = + BindingBuilder.bind(queue).to(exchange).with(RabbitMQConfig.ROUTING_KEY) + + @Bean + fun messageConverter(): MessageConverter = Jackson2JsonMessageConverter() + + @Bean + fun rabbitTemplate(connectionFactory: ConnectionFactory, messageConverter: MessageConverter): RabbitTemplate = + RabbitTemplate(connectionFactory).apply { + this.messageConverter = messageConverter + } +} \ No newline at end of file diff --git a/pact-jvm-mock-test/src/main/kotlin/io/github/ludorival/pactjvm/mock/test/shoppingservice/config/RabbitMQConfig.kt b/pact-jvm-mock-test/src/main/kotlin/io/github/ludorival/pactjvm/mock/test/shoppingservice/config/RabbitMQConfig.kt new file mode 100644 index 0000000..f8bbe87 --- /dev/null +++ b/pact-jvm-mock-test/src/main/kotlin/io/github/ludorival/pactjvm/mock/test/shoppingservice/config/RabbitMQConfig.kt @@ -0,0 +1,30 @@ +package io.github.ludorival.pactjvm.mock.test.shoppingservice.config + +import org.springframework.amqp.core.TopicExchange +import org.springframework.amqp.rabbit.connection.ConnectionFactory +import org.springframework.amqp.rabbit.core.RabbitTemplate +import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration + +@Configuration +open class RabbitMQConfig { + + companion object { + const val EXCHANGE_NAME = "shopping.topic" + const val ROUTING_KEY = "shopping.list.ordered" + } + + @Bean + open fun exchange(): TopicExchange = TopicExchange(EXCHANGE_NAME) + + @Bean + open fun messageConverter() = Jackson2JsonMessageConverter() + + @Bean + open fun rabbitTemplate(connectionFactory: ConnectionFactory, messageConverter: Jackson2JsonMessageConverter): RabbitTemplate { + return RabbitTemplate(connectionFactory).apply { + this.messageConverter = messageConverter + } + } +} \ No newline at end of file diff --git a/pact-jvm-mock-test/src/main/kotlin/io/github/ludorival/pactjvm/mock/test/shoppingservice/messaging/ShoppingListOrderPublisher.kt b/pact-jvm-mock-test/src/main/kotlin/io/github/ludorival/pactjvm/mock/test/shoppingservice/messaging/ShoppingListOrderPublisher.kt new file mode 100644 index 0000000..6623ba2 --- /dev/null +++ b/pact-jvm-mock-test/src/main/kotlin/io/github/ludorival/pactjvm/mock/test/shoppingservice/messaging/ShoppingListOrderPublisher.kt @@ -0,0 +1,20 @@ +package io.github.ludorival.pactjvm.mock.test.shoppingservice.messaging + +import io.github.ludorival.pactjvm.mock.test.orderservice.OrderMessage +import io.github.ludorival.pactjvm.mock.test.shoppingservice.ShoppingList +import io.github.ludorival.pactjvm.mock.test.shoppingservice.config.RabbitMQConfig +import org.springframework.amqp.rabbit.core.RabbitTemplate +import org.springframework.stereotype.Component + +@Component +class ShoppingListOrderPublisher(private val rabbitTemplate: RabbitTemplate) { + + fun publishOrderFromShoppingList(shoppingList: ShoppingList) { + val orderMessage = OrderMessage.fromShoppingList(shoppingList) + rabbitTemplate.convertAndSend( + RabbitMQConfig.EXCHANGE_NAME, + RabbitMQConfig.ROUTING_KEY, + orderMessage + ) + } +} \ No newline at end of file diff --git a/pact-jvm-mock-test/src/main/resources/application.yml b/pact-jvm-mock-test/src/main/resources/application.yml index b6927d1..e42401c 100644 --- a/pact-jvm-mock-test/src/main/resources/application.yml +++ b/pact-jvm-mock-test/src/main/resources/application.yml @@ -12,6 +12,11 @@ spring: config: activate: on-profile: shopping-service + rabbitmq: + host: localhost + port: 5672 + username: guest + password: guest server: port: 4000 @@ -20,6 +25,11 @@ spring: config: activate: on-profile: user-service + rabbitmq: + host: localhost + port: 5672 + username: guest + password: guest server: port: 4001 diff --git a/pact-jvm-mock-test/src/test/java/io/github/ludorival/pactjvm/mock/test/MockitoAdapterTest.java b/pact-jvm-mock-test/src/test/java/io/github/ludorival/pactjvm/mock/test/MockitoAdapterTest.java index dc9b0cc..b2a9dd3 100644 --- a/pact-jvm-mock-test/src/test/java/io/github/ludorival/pactjvm/mock/test/MockitoAdapterTest.java +++ b/pact-jvm-mock-test/src/test/java/io/github/ludorival/pactjvm/mock/test/MockitoAdapterTest.java @@ -37,7 +37,7 @@ public class MockitoAdapterTest { public static class TestPactConfig extends PactConfiguration { public TestPactConfig() { - super("mockito-test-consumer", new SpringRestTemplateMockAdapter()); + super(new SpringRestTemplateMockAdapter("mockito-test-consumer")); } } diff --git a/pact-jvm-mock-test/src/test/java/io/github/ludorival/pactjvm/mock/test/MockitoCoverageTest.java b/pact-jvm-mock-test/src/test/java/io/github/ludorival/pactjvm/mock/test/MockitoCoverageTest.java index 136cdbe..e93f9aa 100644 --- a/pact-jvm-mock-test/src/test/java/io/github/ludorival/pactjvm/mock/test/MockitoCoverageTest.java +++ b/pact-jvm-mock-test/src/test/java/io/github/ludorival/pactjvm/mock/test/MockitoCoverageTest.java @@ -2,6 +2,7 @@ import au.com.dius.pact.core.model.Interaction; import au.com.dius.pact.core.model.Pact; +import au.com.dius.pact.core.model.RequestResponsePact; import au.com.dius.pact.core.model.RequestResponseInteraction; import au.com.dius.pact.core.model.matchingrules.RegexMatcher; import au.com.dius.pact.core.model.matchingrules.TypeMatcher; @@ -37,7 +38,7 @@ public class MockitoCoverageTest { @BeforeEach void setUp() { MockitoAnnotations.openMocks(this); - UtilsKt.clearPact(API_1); + UtilsKt.clearPact("shopping-list", API_1); } @Test @@ -50,7 +51,7 @@ void shouldInterceptSimpleStub() { restTemplate.getForEntity(TEST_API_1_URL, String.class); // then - Pact pact = UtilsKt.getCurrentPact(API_1); + Pact pact = currentPact(); assertNotNull(pact); assertEquals(1, pact.getInteractions().size()); } @@ -67,7 +68,7 @@ void shouldInterceptgivenAndDescription() { restTemplate.getForEntity(TEST_API_1_URL + "/users/123", String.class); // then - Pact pact = UtilsKt.getCurrentPact(API_1); + Pact pact = currentPact(); assertNotNull(pact); assertEquals(1, pact.getInteractions().size()); RequestResponseInteraction interaction = (RequestResponseInteraction) pact.getInteractions().get(0); @@ -99,7 +100,7 @@ void shouldInterceptmatching() { restTemplate.postForEntity(TEST_API_1_URL + "/users", request, String.class); // then - Pact pact = UtilsKt.getCurrentPact(API_1); + Pact pact = currentPact(); assertNotNull(pact); assertEquals(1, pact.getInteractions().size()); RequestResponseInteraction interaction = (RequestResponseInteraction) pact.getInteractions().get(0); @@ -127,7 +128,7 @@ void shouldHandleMultipleResponses() { // Verify pact interactions - Pact pact = UtilsKt.getCurrentPact(API_1); + Pact pact = currentPact(); assertNotNull(pact); assertEquals(3, pact.getInteractions().size()); @@ -162,7 +163,7 @@ void shouldHandleErrorResponses() { ); // then - Pact pact = UtilsKt.getCurrentPact(API_1); + Pact pact = currentPact(); assertNotNull(pact); assertEquals(1, pact.getInteractions().size()); RequestResponseInteraction interaction = (RequestResponseInteraction) pact.getInteractions().get(0); @@ -195,7 +196,7 @@ void shouldHandleChainedResponsesWithThen() { responses.add(restTemplate.getForEntity(TEST_API_1_URL + "/chain", String.class)); // then - Pact pact = UtilsKt.getCurrentPact(API_1); + Pact pact = currentPact(); assertNotNull(pact); List interactions = pact.getInteractions().stream().map(interaction -> (RequestResponseInteraction)interaction).toList(); assertEquals(4, interactions.size()); @@ -220,4 +221,9 @@ void shouldHandleChainedResponsesWithThen() { assertEquals(404, interactions.get(3).getResponse().getStatus()); assertTrue(interactions.get(3).getResponse().getBody().isNotPresent()); } -} \ No newline at end of file + + @SuppressWarnings("unchecked") + private static RequestResponsePact currentPact() { + return UtilsKt.getCurrentPact("shopping-list", API_1); + } +} diff --git a/pact-jvm-mock-test/src/test/kotlin/io/github/ludorival/pactjvm/mock/test/DeterministicPact.kt b/pact-jvm-mock-test/src/test/kotlin/io/github/ludorival/pactjvm/mock/test/DeterministicPact.kt index bbbdfd6..a824efc 100644 --- a/pact-jvm-mock-test/src/test/kotlin/io/github/ludorival/pactjvm/mock/test/DeterministicPact.kt +++ b/pact-jvm-mock-test/src/test/kotlin/io/github/ludorival/pactjvm/mock/test/DeterministicPact.kt @@ -4,7 +4,7 @@ import io.github.ludorival.pactjvm.mock.PactConfiguration import io.github.ludorival.pactjvm.mock.spring.SpringRestTemplateMockAdapter import com.fasterxml.jackson.databind.ObjectMapper -object DeterministicPact : PactConfiguration("shopping-list", SpringRestTemplateMockAdapter(ObjectMapperConfig::by)) { +object DeterministicPact : PactConfiguration(SpringRestTemplateMockAdapter("shopping-list", ObjectMapperConfig::by)) { override fun getPactDirectory(): String = "./src/test/resources/pacts-deterministic" override fun isDeterministic(): Boolean = true diff --git a/pact-jvm-mock-test/src/test/kotlin/io/github/ludorival/pactjvm/mock/test/MockkCoverageTest.kt b/pact-jvm-mock-test/src/test/kotlin/io/github/ludorival/pactjvm/mock/test/MockkCoverageTest.kt index f431d7a..952c242 100644 --- a/pact-jvm-mock-test/src/test/kotlin/io/github/ludorival/pactjvm/mock/test/MockkCoverageTest.kt +++ b/pact-jvm-mock-test/src/test/kotlin/io/github/ludorival/pactjvm/mock/test/MockkCoverageTest.kt @@ -1,6 +1,7 @@ package io.github.ludorival.pactjvm.mock.test import au.com.dius.pact.core.model.RequestResponseInteraction +import au.com.dius.pact.core.model.RequestResponsePact import au.com.dius.pact.core.model.matchingrules.RegexMatcher import au.com.dius.pact.core.model.matchingrules.TypeMatcher import io.github.ludorival.kotlintdd.SimpleGivenWhenThen.given @@ -28,7 +29,7 @@ class MockkCoverageTest { @BeforeEach fun setUp() { - clearPact(API_1) + clearPact("shopping-list", API_1) } @Test @@ -40,7 +41,7 @@ class MockkCoverageTest { } `when`{ restTemplate.getForEntity(TEST_API_1_URL, String::class.java) } then { - with(getCurrentPact(API_1)!!) { + with(currentPact()) { assertEquals(1, interactions.size) } } @@ -60,7 +61,7 @@ class MockkCoverageTest { } `when` { restTemplate.getForEntity("$TEST_API_1_URL/users/123", String::class.java) } then { - with(getCurrentPact(API_1)!!) { + with(currentPact()) { assertEquals(1, interactions.size) with(interactions.first()) { assertEquals("Get user profile", description) @@ -97,7 +98,7 @@ class MockkCoverageTest { ) restTemplate.postForEntity("$TEST_API_1_URL/users", request, String::class.java) } then { - with(getCurrentPact(API_1)!!) { + with(currentPact()) { assertEquals(1, interactions.size) with(interactions.first() as RequestResponseInteraction) { assertNotNull(request.matchingRules) @@ -122,7 +123,7 @@ class MockkCoverageTest { restTemplate.getForEntity("$TEST_API_1_URL/data", String::class.java) restTemplate.getForEntity("$TEST_API_1_URL/data", String::class.java) } then { - with(getCurrentPact(API_1)!!.interactions.filterIsInstance()) { + with(currentPact().interactions.filterIsInstance()) { assertEquals(3, size) assertEquals(200, this[0].response.status) assertEquals(200, this[1].response.status) @@ -142,7 +143,7 @@ class MockkCoverageTest { restTemplate.getForEntity("$TEST_API_1_URL/error", String::class.java) } } then { - with(getCurrentPact(API_1)!!.interactions.filterIsInstance()) { + with(currentPact().interactions.filterIsInstance()) { assertEquals(1, size) assertEquals(500, this[0].response.status) assertEquals("Service unavailable", this[0].response.body.valueAsString()) @@ -161,7 +162,7 @@ class MockkCoverageTest { restTemplate.getForEntity("$TEST_API_1_URL/error", String::class.java) } } then { - with(getCurrentPact(API_1)!!.interactions.filterIsInstance()) { + with(currentPact().interactions.filterIsInstance()) { assertEquals(1, size) assertEquals(400, this[0].response.status) assertTrue(this[0].response.body.contentType.isJson()) @@ -194,7 +195,7 @@ class MockkCoverageTest { responses.add(restTemplate.getForEntity("$TEST_API_1_URL/chain", String::class.java)) } then { - with(getCurrentPact(API_1)!!.interactions.filterIsInstance()) { + with(currentPact().interactions.filterIsInstance()) { assertEquals(4, size) with(this[0]) { assertEquals("Initial successful response", description) @@ -254,7 +255,7 @@ class MockkCoverageTest { ) restTemplate.postForEntity("$TEST_API_1_URL/users", request, String::class.java) } then { - with(getCurrentPact(API_1)!!) { + with(currentPact()) { assertEquals(1, interactions.size) with(interactions.first()) { assertEquals("POST request to http://localhost:8080/service1/api/v1/users with 2 parameters", description) @@ -282,12 +283,21 @@ class MockkCoverageTest { restTemplate.getForEntity("$TEST_API_1_URL/unsupported-error", String::class.java) } } then { - assertNull(getCurrentPact(API_1)) + assertNull(currentPactOrNull()) } } companion object { val API_1 = "service1" val TEST_API_1_URL = "http://localhost:8080/$API_1/api/v1" + + private fun currentPact(): RequestResponsePact { + return getCurrentPact("shopping-list", API_1) + ?: throw AssertionError("Expected pact to be non-null") + } + + private fun currentPactOrNull(): RequestResponsePact? { + return getCurrentPact("shopping-list", API_1) + } } } diff --git a/pact-jvm-mock-test/src/test/kotlin/io/github/ludorival/pactjvm/mock/test/NoPactConsumerTest.kt b/pact-jvm-mock-test/src/test/kotlin/io/github/ludorival/pactjvm/mock/test/NoPactConsumerTest.kt index 6066dc4..95ade0b 100644 --- a/pact-jvm-mock-test/src/test/kotlin/io/github/ludorival/pactjvm/mock/test/NoPactConsumerTest.kt +++ b/pact-jvm-mock-test/src/test/kotlin/io/github/ludorival/pactjvm/mock/test/NoPactConsumerTest.kt @@ -11,6 +11,7 @@ import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import org.springframework.http.* import org.springframework.web.client.RestTemplate +import au.com.dius.pact.core.model.RequestResponsePact class NoPactConsumerTest { @@ -18,7 +19,7 @@ class NoPactConsumerTest { @BeforeEach fun setUp() { - clearPact(API_1) + clearPact("shopping-list", API_1) } @Test @@ -31,7 +32,7 @@ class NoPactConsumerTest { restTemplate.getForEntity(TEST_API_1_URL, String::class.java) } then { // Verify no interactions were recorded since @PactConsumer is missing - val pact = getCurrentPact(API_1) + val pact = getCurrentPact("shopping-list", API_1) assertTrue(pact == null || pact.interactions.isEmpty(), "Expected no interactions to be recorded") } } diff --git a/pact-jvm-mock-test/src/test/kotlin/io/github/ludorival/pactjvm/mock/test/NonDeterministicPact.kt b/pact-jvm-mock-test/src/test/kotlin/io/github/ludorival/pactjvm/mock/test/NonDeterministicPact.kt index 499d0e0..fb0dd44 100644 --- a/pact-jvm-mock-test/src/test/kotlin/io/github/ludorival/pactjvm/mock/test/NonDeterministicPact.kt +++ b/pact-jvm-mock-test/src/test/kotlin/io/github/ludorival/pactjvm/mock/test/NonDeterministicPact.kt @@ -4,10 +4,19 @@ import com.fasterxml.jackson.annotation.JsonInclude import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.databind.PropertyNamingStrategies import io.github.ludorival.pactjvm.mock.PactConfiguration +import io.github.ludorival.pactjvm.mock.spring.SpringRabbitMQMockAdapter import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder import java.time.LocalDate import io.github.ludorival.pactjvm.mock.test.shoppingservice.objectMapperBuilder import io.github.ludorival.pactjvm.mock.spring.SpringRestTemplateMockAdapter import io.github.ludorival.pactjvm.mock.spring.serializerAsDefault +import io.github.ludorival.pactjvm.mock.Call -object NonDeterministicPact : PactConfiguration("shopping-list", SpringRestTemplateMockAdapter(ObjectMapperConfig::by)) +object NonDeterministicPact : PactConfiguration( + SpringRestTemplateMockAdapter("shopping-list", ObjectMapperConfig::by), + object : SpringRabbitMQMockAdapter("order-service", objectMapperBuilder().build()) { + override fun determineConsumerAndProvider(call: Call<*>): Pair { + return "order-service" to "shopping-list" + } + } +) \ No newline at end of file diff --git a/pact-jvm-mock-test/src/test/kotlin/io/github/ludorival/pactjvm/mock/test/rabbitmq/RabbitMQPactTest.kt b/pact-jvm-mock-test/src/test/kotlin/io/github/ludorival/pactjvm/mock/test/rabbitmq/RabbitMQPactTest.kt new file mode 100644 index 0000000..99f3efb --- /dev/null +++ b/pact-jvm-mock-test/src/test/kotlin/io/github/ludorival/pactjvm/mock/test/rabbitmq/RabbitMQPactTest.kt @@ -0,0 +1,92 @@ +package io.github.ludorival.pactjvm.mock.test.rabbitmq + +import io.github.ludorival.kotlintdd.SimpleGivenWhenThen.given +import io.github.ludorival.kotlintdd.then +import io.github.ludorival.kotlintdd.`when` +import io.github.ludorival.pactjvm.mock.PactConsumer +import io.github.ludorival.pactjvm.mock.clearPact +import io.github.ludorival.pactjvm.mock.getCurrentPact +import io.github.ludorival.pactjvm.mock.mockk.uponReceiving +import io.github.ludorival.pactjvm.mock.test.NonDeterministicPact +import io.github.ludorival.pactjvm.mock.test.orderservice.OrderMessage +import io.github.ludorival.pactjvm.mock.test.shoppingservice.ShoppingList +import io.github.ludorival.pactjvm.mock.test.shoppingservice.config.RabbitMQConfig +import io.github.ludorival.pactjvm.mock.test.shoppingservice.messaging.ShoppingListOrderPublisher +import io.mockk.every +import io.mockk.mockk +import io.mockk.slot +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.springframework.amqp.rabbit.core.RabbitTemplate +import java.time.LocalDate +import au.com.dius.pact.core.model.messaging.MessagePact + +@PactConsumer(NonDeterministicPact::class) +class RabbitMQPactTest { + + private val rabbitTemplate = mockk() + private val publisher = ShoppingListOrderPublisher(rabbitTemplate) + + @BeforeEach + fun setUp() { + clearPact(ORDER_SERVICE, SHOPPING_LIST) + } + + @Test + fun `test publisher sends shopping list as order message`() { + val shoppingList = ShoppingList( + id = 123L, + title = "My Shopping List", + userId = 456L, + items = listOf( + ShoppingList.Item(1L, "Apple", 2), + ShoppingList.Item(2L, "Banana", 3) + ), + createdAt = LocalDate.parse("2024-01-21") + ) + + given { + uponReceiving { + rabbitTemplate.convertAndSend( + any(), + any(), + any() + ) + }.withDescription { + "Shopping list ordered message" + }.given { + state("shopping list ordered", mapOf( + "exchange" to RabbitMQConfig.EXCHANGE_NAME, + "routing_key" to RabbitMQConfig.ROUTING_KEY, + "shopping_list_id" to shoppingList.id.toString() + )) + }.returns(Unit) + } `when` { + publisher.publishOrderFromShoppingList(shoppingList) + } then { + with(getCurrentPact(ORDER_SERVICE, SHOPPING_LIST)!!) { + assertEquals(1, messages.size) + with(messages.first()) { + assertEquals("Shopping list ordered message", description) + assertEquals("shopping list ordered", providerStates.first().name) + with(providerStates.first().params) { + assertEquals(RabbitMQConfig.EXCHANGE_NAME, get("exchange")) + assertEquals(RabbitMQConfig.ROUTING_KEY, get("routing_key")) + assertEquals("123", get("shopping_list_id")) + } + assertEquals( + """{"shopping_list_id":"123","user_id":456,"items":[{"name":"Apple","quantity":2},{"name":"Banana","quantity":3}]}""", + contents.valueAsString() + ) + } + } + } + } + + companion object { + const val ORDER_SERVICE = "order-service" + const val SHOPPING_LIST = "shopping-list" + } +} \ No newline at end of file diff --git a/pact-jvm-mock-test/src/test/kotlin/io/github/ludorival/pactjvm/mock/test/verifier/RabbitMQPactVerifier.kt b/pact-jvm-mock-test/src/test/kotlin/io/github/ludorival/pactjvm/mock/test/verifier/RabbitMQPactVerifier.kt new file mode 100644 index 0000000..a003450 --- /dev/null +++ b/pact-jvm-mock-test/src/test/kotlin/io/github/ludorival/pactjvm/mock/test/verifier/RabbitMQPactVerifier.kt @@ -0,0 +1,74 @@ +package io.github.ludorival.pactjvm.mock.test.verifier + +import au.com.dius.pact.provider.PactVerifyProvider +import au.com.dius.pact.provider.junit5.MessageTestTarget +import au.com.dius.pact.provider.junit5.PactVerificationContext +import au.com.dius.pact.provider.junitsupport.Provider +import au.com.dius.pact.provider.junitsupport.State +import au.com.dius.pact.provider.junitsupport.loader.PactFolder +import au.com.dius.pact.provider.spring.junit5.PactVerificationSpringProvider +import io.github.ludorival.pactjvm.mock.test.orderservice.OrderMessage +import io.github.ludorival.pactjvm.mock.test.shoppingservice.ShoppingList +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Tag +import org.junit.jupiter.api.TestTemplate +import org.junit.jupiter.api.extension.ExtendWith +import org.springframework.boot.test.context.SpringBootTest +import org.springframework.test.context.ActiveProfiles +import com.fasterxml.jackson.databind.ObjectMapper +import org.springframework.beans.factory.annotation.Autowired +import java.time.LocalDate + +@ActiveProfiles("order-service") +@Tag("contract-test") +@SpringBootTest(classes = [io.github.ludorival.pactjvm.mock.test.shoppingservice.ShoppingServiceApplication::class]) +@Provider("shopping-list") +@PactFolder("pacts") +open class RabbitMQPactVerifier { + + @Autowired + private lateinit var objectMapper: ObjectMapper + + @BeforeEach + fun setUp(context: PactVerificationContext) { + context.target = MessageTestTarget() + } + + @TestTemplate + @ExtendWith(PactVerificationSpringProvider::class) + fun pactVerificationTestTemplate(context: PactVerificationContext) { + context.verifyInteraction() + } + + @State("shopping list ordered") + fun setupShoppingListOrderedState(params: Map) { + // State setup if needed + // The state data is available in the params map + println("Setting up state with params: $params") + } + + @PactVerifyProvider("Shopping list ordered message") + fun verifyShoppingListOrderedMessage(): String { + // Create a sample shopping list that matches the expected format + val shoppingList = ShoppingList( + id = 123L, + title = "My Shopping List", + userId = 456L, + items = listOf( + ShoppingList.Item(1L, "Apple", 2), + ShoppingList.Item(2L, "Banana", 3) + ), + createdAt = LocalDate.parse("2024-01-21") + ) + + // Convert the shopping list to an order message + val orderMessage = OrderMessage( + shoppingListId = shoppingList.id.toString(), + userId = shoppingList.userId, + items = shoppingList.items.map { OrderMessage.OrderItem(it.name, it.quantity) } + ) + + // Serialize the message to JSON + return objectMapper.writeValueAsString(orderMessage) + } +} \ No newline at end of file diff --git a/pact-jvm-mock-test/src/test/kotlin/io/github/ludorival/pactjvm/mock/test/verifier/ShoppingServicePactVerifier.kt b/pact-jvm-mock-test/src/test/kotlin/io/github/ludorival/pactjvm/mock/test/verifier/ShoppingServicePactVerifier.kt index d79f5a8..4c06f32 100644 --- a/pact-jvm-mock-test/src/test/kotlin/io/github/ludorival/pactjvm/mock/test/verifier/ShoppingServicePactVerifier.kt +++ b/pact-jvm-mock-test/src/test/kotlin/io/github/ludorival/pactjvm/mock/test/verifier/ShoppingServicePactVerifier.kt @@ -57,7 +57,7 @@ class ShoppingServicePactVerifier { } @State("the shopping list is empty") - fun setupEmptyShoppingList(params: Map) { + fun setupEmptyShoppingList() { restController.clearAll() } diff --git a/pact-jvm-mock-test/src/test/resources/pacts/order-service-shopping-list.json b/pact-jvm-mock-test/src/test/resources/pacts/order-service-shopping-list.json new file mode 100644 index 0000000..2b7ef5a --- /dev/null +++ b/pact-jvm-mock-test/src/test/resources/pacts/order-service-shopping-list.json @@ -0,0 +1,49 @@ +{ + "consumer": { + "name": "order-service" + }, + "messages": [ + { + "contents": { + "items": [ + { + "name": "Apple", + "quantity": 2 + }, + { + "name": "Banana", + "quantity": 3 + } + ], + "shopping_list_id": "123", + "user_id": 456 + }, + "description": "Shopping list ordered message", + "metaData": { + "exchange": "shopping.topic", + "routing_key": "shopping.list.ordered" + }, + "providerStates": [ + { + "name": "shopping list ordered", + "params": { + "exchange": "shopping.topic", + "routing_key": "shopping.list.ordered", + "shopping_list_id": "123" + } + } + ] + } + ], + "metadata": { + "pact-jvm": { + "version": "4.6.16" + }, + "pactSpecification": { + "version": "3.0.0" + } + }, + "provider": { + "name": "shopping-list" + } +} diff --git a/pact-jvm-mock-test/src/test/resources/pacts/shopping-list-shopping-service.json b/pact-jvm-mock-test/src/test/resources/pacts/shopping-list-shopping-service.json deleted file mode 100644 index 1864df3..0000000 --- a/pact-jvm-mock-test/src/test/resources/pacts/shopping-list-shopping-service.json +++ /dev/null @@ -1,222 +0,0 @@ -{ - "consumer" : { - "name" : "shopping-list" - }, - "provider" : { - "name" : "shopping-service" - }, - "interactions" : [ { - "providerStates" : [ { - "name" : "the shopping list is empty", - "params" : { - "userId" : 123 - } - } ], - "description" : "should set preferred shopping list", - "request" : { - "method" : "POST", - "path" : "/shopping-service/user/123", - "body" : { - "title" : "My Shopping list" - } - }, - "response" : { - "body" : { - "id" : 1, - "title" : "My Shopping list", - "user_id" : 123, - "items" : [ ], - "created_at" : "2023-01-01" - }, - "status" : 200, - "headers" : { }, - "matchingRules" : { - "$.body.created_at" : { - "match" : "type" - } - } - } - }, { - "description" : "list two shopping lists", - "request" : { - "method" : "GET", - "path" : "/shopping-service/user/123", - "query" : "limit=30", - "headers" : { - "Content-Type" : "application/json", - "Authorization" : "Bearer token123" - }, - "matchingRules" : { - "$.header.Authorization" : { - "match" : "regex", - "regex" : "Bearer .*" - } - } - }, - "response" : { - "body" : [ { - "id" : 1, - "title" : "My Favorite Shopping list", - "user_id" : 123, - "items" : [ { - "id" : 1, - "name" : "Apple", - "quantity" : 2 - }, { - "id" : 2, - "name" : "Banana", - "quantity" : 2 - } ], - "created_at" : "2023-01-01" - }, { - "id" : 2, - "title" : "My Shopping list to delete", - "user_id" : 123, - "items" : [ { - "id" : 1, - "name" : "Chicken", - "quantity" : 2 - }, { - "id" : 2, - "name" : "Beed", - "quantity" : 1 - } ], - "created_at" : "2023-01-01" - } ], - "status" : 200, - "headers" : { }, - "matchingRules" : { - "$.body[*].id" : { - "match" : "type" - }, - "$.body[*].created_at" : { - "match" : "type" - } - } - } - }, { - "description" : "delete shopping item", - "request" : { - "method" : "DELETE", - "path" : "/shopping-service/user/123/list/2" - }, - "response" : { - "status" : 200, - "headers" : { } - } - }, { - "description" : "update shopping list", - "request" : { - "method" : "PUT", - "path" : "/shopping-service/user/123/list/2", - "body" : { - "name" : "My updated shopping list" - } - }, - "response" : { - "status" : 200, - "headers" : { } - } - }, { - "description" : "should get the current shopping list and update the item quantity", - "request" : { - "method" : "GET", - "path" : "/shopping-service/user/123/list/1" - }, - "response" : { - "body" : { - "id" : 1, - "title" : "My Favorite Shopping list", - "user_id" : 123, - "items" : [ { - "id" : 1, - "name" : "Apple", - "quantity" : 2 - }, { - "id" : 2, - "name" : "Banana", - "quantity" : 2 - } ], - "created_at" : "2023-01-01" - }, - "status" : 200, - "headers" : { } - } - }, { - "description" : "Patch a shopping item", - "request" : { - "method" : "PATCH", - "path" : "/shopping-service/user/123/list/1", - "body" : { - "id" : 2, - "name" : "Banana", - "quantity" : 3 - } - }, - "response" : { - "body" : { - "id" : 2, - "name" : "Banana", - "quantity" : 3 - }, - "status" : 200, - "headers" : { } - } - }, { - "providerStates" : [ { - "name" : "The request should return a 400 Bad request" - } ], - "description" : "should return a 400 Bad request", - "request" : { - "method" : "POST", - "path" : "/shopping-service/user/123", - "body" : { - "title" : "Unexpected character \\s" - } - }, - "response" : { - "body" : "The title contains unexpected character", - "status" : 400, - "headers" : { } - } - }, { - "providerStates" : [ { - "name" : "the shopping list is empty", - "params" : { - "userId" : 123 - } - } ], - "description" : "create empty shopping list", - "request" : { - "method" : "POST", - "path" : "/shopping-service/user/123", - "body" : { - "title" : "My Shopping list" - } - }, - "response" : { - "body" : { - "id" : 1, - "title" : "My Shopping list", - "user_id" : 123, - "items" : [ ], - "created_at" : "2023-01-01" - }, - "status" : 200, - "headers" : { }, - "matchingRules" : { - "$.body.created_at" : { - "match" : "type" - } - } - } - } ], - "metadata" : { - "pactSpecification" : { - "version" : "3.0.0" - }, - "pactJvm" : { - "version" : "4.0.10" - } - } -} \ No newline at end of file diff --git a/pact-jvm-mock/src/main/kotlin/io/github/ludorival/pactjvm/mock/PactConfiguration.kt b/pact-jvm-mock/src/main/kotlin/io/github/ludorival/pactjvm/mock/PactConfiguration.kt index eb11ff8..5e2d764 100644 --- a/pact-jvm-mock/src/main/kotlin/io/github/ludorival/pactjvm/mock/PactConfiguration.kt +++ b/pact-jvm-mock/src/main/kotlin/io/github/ludorival/pactjvm/mock/PactConfiguration.kt @@ -2,14 +2,14 @@ package io.github.ludorival.pactjvm.mock import au.com.dius.pact.core.model.PactSpecVersion -abstract class PactConfiguration(val consumer: String, vararg adapters: PactMockAdapter<*>) { +abstract class PactConfiguration(vararg adapters: PactMockAdapter<*>) { private val adapters: List> = adapters.toList() open fun getPactDirectory(): String = "./src/test/resources/pacts" open fun isDeterministic(): Boolean = false - open fun getPactMetaData(): PactSpecVersion = PactSpecVersion.V3 + open fun getPactVersion(): PactSpecVersion = PactSpecVersion.V3 fun getAdapterFor(call: Call<*>) = adapters.find { it.support(call) } } diff --git a/pact-jvm-mock/src/main/kotlin/io/github/ludorival/pactjvm/mock/PactMock.kt b/pact-jvm-mock/src/main/kotlin/io/github/ludorival/pactjvm/mock/PactMock.kt index e116585..2208fa4 100644 --- a/pact-jvm-mock/src/main/kotlin/io/github/ludorival/pactjvm/mock/PactMock.kt +++ b/pact-jvm-mock/src/main/kotlin/io/github/ludorival/pactjvm/mock/PactMock.kt @@ -5,7 +5,7 @@ import java.util.concurrent.ConcurrentHashMap internal object PactMock : CallInterceptor { - private var pactConfiguration: PactConfiguration = object : PactConfiguration("") {} + private var pactConfiguration: PactConfiguration = object : PactConfiguration() {} internal fun setPactConfiguration(config: PactConfiguration) { this.pactConfiguration = config @@ -21,24 +21,25 @@ internal object PactMock : CallInterceptor { } } - fun clearPact(providerName: String) { + fun clearPact(consumerName: String, providerName: String) { LOGGER.debug { "Clearing pact for provider: $providerName" } - pacts.remove(getId(providerName)) + pacts.remove(getId(consumerName, providerName)) } - fun getCurrentPact(providerName: String) = pacts[getId(providerName)]?.pact + fun getCurrentPact(consumerName: String, providerName: String) = pacts[getId(consumerName, providerName)]?.pact - private fun getId(providerName: String) = "${pactConfiguration.consumer}-$providerName-${pactConfiguration.isDeterministic()}" - private fun getPact(providerName: String) = pacts.getOrPut(getId(providerName)) { - LOGGER.debug { "Creating new pact for provider: $providerName" } - PactToWrite(providerName, pactConfiguration) + private fun getId(consumerName: String, providerName: String) = "${consumerName}-$providerName-${pactConfiguration.isDeterministic()}" + + private fun getPact(consumerName: String, providerName: String) = pacts.getOrPut(getId(consumerName, providerName)) { + LOGGER.debug { "Creating new pact for consumer: $consumerName, provider: $providerName" } + PactToWrite(consumerName, providerName, pactConfiguration) } - private fun addInteraction(interaction: I, providerName: String) { + private fun addInteraction(interaction: I, consumerName: String, providerName: String) { LOGGER.debug { "Adding interaction for provider: $providerName, description: ${interaction.description}" } - val pactToWrite = getPact(providerName) - pacts[getId(providerName)] = pactToWrite.addInteraction( + val pactToWrite = getPact(consumerName, providerName) + pacts[getId(consumerName, providerName)] = pactToWrite.addInteraction( interaction ) } @@ -53,11 +54,11 @@ internal object PactMock : CallInterceptor { LOGGER.debug { "No adapter found for call, skipping pact recording" } return call.result.getOrThrow() } - val providerName = adapter.determineProvider(call) + val (consumerName, providerName) = adapter.determineConsumerAndProvider(call) runCatching { adapter.buildInteraction(interactionBuilder, providerName) } .onFailure { LOGGER.warn { "Failed to build interaction: ${it.message}" } } .getOrNull() - ?.let { addInteraction(it, providerName) } + ?.let { addInteraction(it, consumerName, providerName) } return adapter.returnsResult(call.result) } diff --git a/pact-jvm-mock/src/main/kotlin/io/github/ludorival/pactjvm/mock/PactMockAdapter.kt b/pact-jvm-mock/src/main/kotlin/io/github/ludorival/pactjvm/mock/PactMockAdapter.kt index 0ad15db..2b80f6d 100644 --- a/pact-jvm-mock/src/main/kotlin/io/github/ludorival/pactjvm/mock/PactMockAdapter.kt +++ b/pact-jvm-mock/src/main/kotlin/io/github/ludorival/pactjvm/mock/PactMockAdapter.kt @@ -13,7 +13,7 @@ abstract class PactMockAdapter { providerName: String ): I - abstract fun determineProvider(call: Call<*>): String + abstract fun determineConsumerAndProvider(call: Call<*>): Pair open fun returnsResult(result: Result) = result.getOrThrow() diff --git a/pact-jvm-mock/src/main/kotlin/io/github/ludorival/pactjvm/mock/PactToWrite.kt b/pact-jvm-mock/src/main/kotlin/io/github/ludorival/pactjvm/mock/PactToWrite.kt index 98a2f90..43d7b4a 100644 --- a/pact-jvm-mock/src/main/kotlin/io/github/ludorival/pactjvm/mock/PactToWrite.kt +++ b/pact-jvm-mock/src/main/kotlin/io/github/ludorival/pactjvm/mock/PactToWrite.kt @@ -1,6 +1,8 @@ package io.github.ludorival.pactjvm.mock import au.com.dius.pact.core.model.* +import au.com.dius.pact.core.model.messaging.Message +import au.com.dius.pact.core.model.messaging.MessagePact import au.com.dius.pact.core.support.Json import org.bitbucket.cowwoc.diffmatchpatch.DiffMatchPatch import java.io.File @@ -9,20 +11,20 @@ import java.util.concurrent.ConcurrentHashMap internal data class PactToWrite( val consumer: String, - val providerMetaData: ProviderMetaData, + val provider: String, + val version: PactSpecVersion, private val isDeterministic: Boolean = false, private val outputDirectory: String ) { constructor( + consumerName: String, providerName: String, pactConfiguration: PactConfiguration ) : this( - pactConfiguration.consumer, - ProviderMetaData( - providerName, - pactConfiguration.getPactMetaData() - ), + consumerName, + providerName, + pactConfiguration.getPactVersion(), pactConfiguration.isDeterministic(), pactConfiguration.getPactDirectory() ) @@ -31,7 +33,7 @@ internal data class PactToWrite( if (consumer.isBlank()) error("The consumer should not be empty") } - val id = "${consumer}-${providerMetaData.name}-${isDeterministic}" + val id = "${consumer}-${provider}-${isDeterministic}" private val interactionsByDescription = ConcurrentHashMap() @@ -42,7 +44,8 @@ internal data class PactToWrite( val pact: Pact get() = createPact( consumer, - providerMetaData, + provider, + version, descriptions.map { interactionsByDescription.getValue(it) } ) @@ -76,15 +79,22 @@ internal data class PactToWrite( private fun createPact( consumer: String, - providerMetaData: ProviderMetaData, + provider: String, + version: PactSpecVersion, interactions: List - ): Pact = + ): Pact = if (interactions.firstOrNull() is Message) MessagePact( + consumer = Consumer(consumer), + provider = Provider(provider), + messages = interactions.mapNotNull { it as? Message }.toMutableList(), + source = UnknownPactSource, + metadata = BasePact.metaData(null, version) + ) else RequestResponsePact( consumer = Consumer(consumer), - provider = Provider(providerMetaData.name), + provider = Provider(provider), interactions = interactions.toMutableList(), source = UnknownPactSource, - metadata = BasePact.metaData(null, providerMetaData.pactMetaData) + metadata = BasePact.metaData(null, version) ) private fun printDifferences(old: Interaction, current: Interaction): String { @@ -136,14 +146,14 @@ internal data class PactToWrite( } private val pactFile - get() = "${consumer}-${providerMetaData.name}.json" + get() = "${consumer}-${provider}.json" internal fun write() { if (descriptions.isEmpty()) return val previousProperty = System.getProperty("pact.writer.overwrite") ?: "false" System.setProperty("pact.writer.overwrite", "true") val pactDirectory = File(outputDirectory, pactFile).toString() - val result = pact.write(outputDirectory, providerMetaData.pactMetaData) + val result = pact.write(outputDirectory, version) System.setProperty("pact.writer.overwrite", previousProperty) if (result.errorValue() == null) LOGGER.info { "[$id] Successfully written pact to $pactDirectory" } diff --git a/pact-jvm-mock/src/main/kotlin/io/github/ludorival/pactjvm/mock/Utils.kt b/pact-jvm-mock/src/main/kotlin/io/github/ludorival/pactjvm/mock/Utils.kt index 53e0cc2..96a86de 100644 --- a/pact-jvm-mock/src/main/kotlin/io/github/ludorival/pactjvm/mock/Utils.kt +++ b/pact-jvm-mock/src/main/kotlin/io/github/ludorival/pactjvm/mock/Utils.kt @@ -8,12 +8,13 @@ fun RequestResponseInteraction.getConsumerName() = request.path.split("/").first fun anError(error: E) = PactMockResponseError(error) -fun getCurrentPact(providerName: String): RequestResponsePact? { - return PactMock.getCurrentPact(providerName) as? RequestResponsePact +fun getCurrentPact(consumerName: String, providerName: String): P? { + return PactMock.getCurrentPact(consumerName, providerName) as? P } -fun clearPact(providerName: String) { - PactMock.clearPact(providerName) + +fun clearPact(consumerName: String, providerName: String) { + PactMock.clearPact(consumerName, providerName) } typealias InteractionHandler = InteractionBuilder<*>.() -> R