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