diff --git a/pact-jvm-server/src/test/groovy/au/com/dius/pact/server/MainSpec.groovy b/pact-jvm-server/src/test/groovy/au/com/dius/pact/server/MainSpec.groovy index ae889adf6c..eee87f66f0 100644 --- a/pact-jvm-server/src/test/groovy/au/com/dius/pact/server/MainSpec.groovy +++ b/pact-jvm-server/src/test/groovy/au/com/dius/pact/server/MainSpec.groovy @@ -66,7 +66,7 @@ class MainSpec extends Specification { def process = invokeApp(true, '--daemon', '--debug', '31311') when: - process.waitFor(500, TimeUnit.MILLISECONDS) + process.waitFor(2000, TimeUnit.MILLISECONDS) def result = createMock('31311', pact) then: diff --git a/provider/spring7/README.md b/provider/spring7/README.md new file mode 100644 index 0000000000..e1c37d014b --- /dev/null +++ b/provider/spring7/README.md @@ -0,0 +1,168 @@ +# Pact Spring7/Spring Boot4 + JUnit5 Support + +This module extends the base [Pact JUnit5 module](/provider/junit5/README.md) (See that for more details) and adds support +for Spring 7 and Spring Boot 4. + +**NOTE: This module requires JDK 17+** + +## Dependency +The combined library (JUnit5 + Spring7) is available on maven central using: + +group-id = au.com.dius.pact.provider +artifact-id = spring7 +version-id = 4.5.x + +## Usage +For writing Spring Pact verification tests with JUnit 5, there is an JUnit 5 Invocation Context Provider that you can use with +the `@TestTemplate` annotation. This will generate a test for each interaction found for the pact files for the provider. + +To use it, add the `@Provider` and `@ExtendWith(SpringExtension.class)` or `@SpringbootTest` and one of the pact source +annotations to your test class (as per a JUnit 5 test), then add a method annotated with `@TestTemplate` and +`@ExtendWith(PactVerificationSpring7Provider.class)` that takes a `PactVerificationContext` parameter. You will need to +call `verifyInteraction()` on the context parameter in your test template method. + +For example: + +```java +@AutoConfigureMockMvc +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT) +@Provider("Animal Profile Service") +@PactBroker +public class ContractVerificationTest { + + @TestTemplate + @ExtendWith(PactVerificationSpring7Provider.class) + void pactVerificationTestTemplate(PactVerificationContext context) { + context.verifyInteraction(); + } + +} +``` + +You will now be able to setup all the required properties using the Spring context, e.g. creating an application +YAML file in the test resources: + +```yaml +pactbroker: + host: your.broker.host + auth: + username: broker-user + password: broker.password +``` + +You can also run pact tests against `MockMvc` without need to spin up the whole application context which takes time +and often requires more additional setup (e.g. database). In order to run lightweight tests just use `@WebMvcTest` +from Spring and `Spring7MockMvcTestTarget` as a test target before each test. + +For example: +```java +@WebMvcTest +@Provider("myAwesomeService") +@PactBroker +class ContractVerificationTest { + + @Autowired + private MockMvc mockMvc; + + @TestTemplate + @ExtendWith(PactVerificationSpring7Provider.class) + void pactVerificationTestTemplate(PactVerificationContext context) { + context.verifyInteraction(); + } + + @BeforeEach + void before(PactVerificationContext context) { + context.setTarget(new Spring7MockMvcTestTarget(mockMvc)); + } +} +``` + +You can also use `Spring7MockMvcTestTarget` for tests without spring context by providing the controllers manually. + +For example: +```java +@Provider("myAwesomeService") +@PactFolder("pacts") +class MockMvcTestTargetStandaloneMockMvcTestJava { + + @TestTemplate + @ExtendWith(PactVerificationSpring7Provider.class) + void pactVerificationTestTemplate(PactVerificationContext context) { + context.verifyInteraction(); + } + + @BeforeEach + void before(PactVerificationContext context) { + Spring7MockMvcTestTarget testTarget = new Spring7MockMvcTestTarget(); + testTarget.setControllers(new DataResource()); + context.setTarget(testTarget); + } + + @RestController + static class DataResource { + @GetMapping("/data") + @ResponseStatus(HttpStatus.NO_CONTENT) + void getData(@RequestParam("ticketId") String ticketId) { + } + } +} +``` + +**Important:** Since `@WebMvcTest` starts only Spring MVC components you can't use `PactVerificationSpring7Provider` +and need to fallback to `PactVerificationInvocationContextProvider` + +## Webflux tests + +You can test Webflux routing functions using the `WebFluxSpring7Target` target class. The easiest way to do it is to get Spring to +autowire your handler and router into the test and then pass the routing function to the target. + +For example: + +```java + @Autowired + YourRouter router; + + @Autowired + YourHandler handler; + + @BeforeEach + void setup(PactVerificationContext context) { + context.setTarget(new WebFluxSpring7Target(router.route(handler))); + } + + @TestTemplate + @ExtendWith(PactVerificationSpring7Provider.class) + void pactVerificationTestTemplate(PactVerificationContext context) { + context.verifyInteraction(); + } +``` + +## Modifying requests + +As documented in [Pact JUnit5 module](/provider/junit5/README.md#modifying-the-requests-before-they-are-sent), you can +inject a request object to modify the requests made. However, depending on the Pact test target you are using, +you need to use a different class. + +| Test Target | Class to use | +|-----------------------------------------------|----------------------------------| +| HttpTarget, HttpsTarget, SpringBootHttpTarget | org.apache.http.HttpRequest | +| Spring7MockMvcTestTarget | MockHttpServletRequestBuilder | +| WebFluxSpring7Target | WebTestClient.RequestHeadersSpec | + +# Verifying V4 Pact files that require plugins + +Pact files that require plugins can be verified with version 4.3.0+. For details on how plugins work, see the +[Pact plugin project](https://github.com/pact-foundation/pact-plugins). + +Each required plugin is defined in the `plugins` section in the Pact metadata in the Pact file. The plugins will be +loaded from the plugin directory. By default, this is `~/.pact/plugins` or the value of the `PACT_PLUGIN_DIR` environment +variable. Each plugin required by the Pact file must be installed there. You will need to follow the installation +instructions for each plugin, but the default is to unpack the plugin into a sub-directory `-` +(i.e., for the Protobuf plugin 0.0.0 it will be `protobuf-0.0.0`). The plugin manifest file must be present for the +plugin to be able to be loaded. + +# Test Analytics + +We are tracking anonymous analytics to gather important usage statistics like JVM version +and operating system. To disable tracking, set the 'pact_do_not_track' system property or environment +variable to 'true'. diff --git a/provider/spring7/build.gradle b/provider/spring7/build.gradle new file mode 100644 index 0000000000..4bc3f7ea37 --- /dev/null +++ b/provider/spring7/build.gradle @@ -0,0 +1,26 @@ +plugins { + id 'au.com.dius.pact.kotlin-library-conventions' +} + +description = 'Provider Spring7/Spring Boot4 + JUnit5 Support' +group = 'au.com.dius.pact.provider' + +dependencies { + api project(':provider:junit5') + + implementation 'org.springframework:spring-context:7.0.0' + implementation 'org.springframework:spring-test:7.0.0' + implementation 'org.springframework:spring-web:7.0.0' + implementation 'org.springframework:spring-webflux:7.0.0' + implementation 'jakarta.servlet:jakarta.servlet-api:6.1.0' + implementation 'org.hamcrest:hamcrest:3.0' + implementation 'org.apache.commons:commons-lang3' + implementation 'javax.mail:mail:1.5.0-b01' + + testImplementation 'org.springframework.boot:spring-boot-starter-test-classic:4.0.0' + testImplementation 'org.springframework.boot:spring-boot-starter-webmvc:4.0.0' + testImplementation 'org.springframework.boot:spring-boot-starter-security-test:4.0.0' + testImplementation 'org.apache.groovy:groovy' + testImplementation 'org.mockito:mockito-core:5.20.0' + testImplementation 'org.yaml:snakeyaml:2.5' +} diff --git a/provider/spring7/description.txt b/provider/spring7/description.txt new file mode 100644 index 0000000000..229dbbf91e --- /dev/null +++ b/provider/spring7/description.txt @@ -0,0 +1 @@ +Pact-JVM - Provider Spring7/Spring Boot4 + JUnit5 Support \ No newline at end of file diff --git a/provider/spring7/src/main/kotlin/au/com/dius/pact/provider/spring/spring7/PactVerificationSpring7Extension.kt b/provider/spring7/src/main/kotlin/au/com/dius/pact/provider/spring/spring7/PactVerificationSpring7Extension.kt new file mode 100644 index 0000000000..da39cb2c7f --- /dev/null +++ b/provider/spring7/src/main/kotlin/au/com/dius/pact/provider/spring/spring7/PactVerificationSpring7Extension.kt @@ -0,0 +1,42 @@ +package au.com.dius.pact.provider.spring.spring7 + +import au.com.dius.pact.core.model.Interaction +import au.com.dius.pact.core.model.Pact +import au.com.dius.pact.core.model.PactSource +import au.com.dius.pact.provider.junit5.PactVerificationContext +import au.com.dius.pact.provider.junit5.PactVerificationExtension +import org.junit.jupiter.api.extension.ExtensionContext +import org.junit.jupiter.api.extension.ParameterContext +import org.springframework.test.web.reactive.server.WebTestClient +import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder + +open class PactVerificationSpring7Extension( + pact: Pact, + pactSource: PactSource, + interaction: Interaction, + serviceName: String, + consumerName: String? +) : PactVerificationExtension(pact, pactSource, interaction, serviceName, consumerName) { + constructor(context: PactVerificationExtension) : this(context.pact, context.pactSource, context.interaction, + context.serviceName, context.consumerName) + + override fun supportsParameter(parameterContext: ParameterContext, extensionContext: ExtensionContext): Boolean { + val store = extensionContext.getStore(ExtensionContext.Namespace.create("pact-jvm")) + val testContext = store.get("interactionContext") as PactVerificationContext + val target = testContext.currentTarget() + return when (parameterContext.parameter.type) { + MockHttpServletRequestBuilder::class.java -> target is Spring7MockMvcTestTarget + WebTestClient.RequestHeadersSpec::class.java -> target is WebFluxSpring7Target + else -> super.supportsParameter(parameterContext, extensionContext) + } + } + + override fun resolveParameter(parameterContext: ParameterContext, extensionContext: ExtensionContext): Any? { + val store = extensionContext.getStore(ExtensionContext.Namespace.create("pact-jvm")) + return when (parameterContext.parameter.type) { + MockHttpServletRequestBuilder::class.java -> store.get("request") + WebTestClient.RequestHeadersSpec::class.java -> store.get("request") + else -> super.resolveParameter(parameterContext, extensionContext) + } + } +} diff --git a/provider/spring7/src/main/kotlin/au/com/dius/pact/provider/spring/spring7/PactVerificationSpring7Provider.kt b/provider/spring7/src/main/kotlin/au/com/dius/pact/provider/spring/spring7/PactVerificationSpring7Provider.kt new file mode 100644 index 0000000000..aefa6188c8 --- /dev/null +++ b/provider/spring7/src/main/kotlin/au/com/dius/pact/provider/spring/spring7/PactVerificationSpring7Provider.kt @@ -0,0 +1,32 @@ +package au.com.dius.pact.provider.spring.spring7 + +import au.com.dius.pact.core.support.expressions.ValueResolver +import au.com.dius.pact.provider.junit5.PactVerificationExtension +import au.com.dius.pact.provider.junit5.PactVerificationInvocationContextProvider +import org.junit.jupiter.api.extension.ExtensionContext +import org.junit.jupiter.api.extension.TestTemplateInvocationContext +import org.springframework.test.context.TestContextManager +import org.springframework.test.context.junit.jupiter.SpringExtension +import java.util.stream.Stream + +open class PactVerificationSpring7Provider : PactVerificationInvocationContextProvider() { + + override fun getValueResolver(context: ExtensionContext): ValueResolver? { + val store = context.root.getStore(ExtensionContext.Namespace.create(SpringExtension::class.java)) + val testClass = context.requiredTestClass + val testContextManager = store.getOrComputeIfAbsent(testClass, { TestContextManager(testClass) }, + TestContextManager::class.java) + val environment = testContextManager.testContext.applicationContext.environment + return Spring7EnvironmentResolver(environment) + } + + override fun provideTestTemplateInvocationContexts(context: ExtensionContext): Stream { + return super.provideTestTemplateInvocationContexts(context).map { + if (it is PactVerificationExtension) { + PactVerificationSpring7Extension(it) + } else { + it + } + } + } +} diff --git a/provider/spring7/src/main/kotlin/au/com/dius/pact/provider/spring/spring7/Spring7EnvironmentResolver.kt b/provider/spring7/src/main/kotlin/au/com/dius/pact/provider/spring/spring7/Spring7EnvironmentResolver.kt new file mode 100644 index 0000000000..67083e8602 --- /dev/null +++ b/provider/spring7/src/main/kotlin/au/com/dius/pact/provider/spring/spring7/Spring7EnvironmentResolver.kt @@ -0,0 +1,24 @@ +package au.com.dius.pact.provider.spring.spring7 + +import au.com.dius.pact.core.support.expressions.SystemPropertyResolver +import au.com.dius.pact.core.support.expressions.ValueResolver +import org.springframework.core.env.Environment + +class Spring7EnvironmentResolver(private val environment: Environment) : ValueResolver { + override fun resolveValue(property: String?): String? { + val tuple = SystemPropertyResolver.PropertyValueTuple(property).invoke() + + val name = tuple.propertyName ?: return null + val defaultValue = tuple.defaultValue ?: return null + + return environment.getProperty(name, defaultValue) + } + + override fun resolveValue(property: String?, default: String?): String? { + val name = property ?: return null + val defaultValue = default ?: return null + return environment.getProperty(name, defaultValue) + } + + override fun propertyDefined(property: String) = environment.containsProperty(property) +} diff --git a/provider/spring7/src/main/kotlin/au/com/dius/pact/provider/spring/spring7/Spring7MockMvcTestTarget.kt b/provider/spring7/src/main/kotlin/au/com/dius/pact/provider/spring/spring7/Spring7MockMvcTestTarget.kt new file mode 100644 index 0000000000..6ed609d03c --- /dev/null +++ b/provider/spring7/src/main/kotlin/au/com/dius/pact/provider/spring/spring7/Spring7MockMvcTestTarget.kt @@ -0,0 +1,228 @@ +package au.com.dius.pact.provider.spring.spring7 + +import au.com.dius.pact.core.model.ContentType +import au.com.dius.pact.core.model.IRequest +import au.com.dius.pact.core.model.Interaction +import au.com.dius.pact.core.model.OptionalBody +import au.com.dius.pact.core.model.Pact +import au.com.dius.pact.core.model.PactSource +import au.com.dius.pact.core.model.SynchronousRequestResponse +import au.com.dius.pact.core.model.generators.GeneratorTestMode +import au.com.dius.pact.provider.IProviderVerifier +import au.com.dius.pact.provider.ProviderInfo +import au.com.dius.pact.provider.ProviderResponse +import au.com.dius.pact.provider.junit5.TestTarget +import jakarta.servlet.http.Cookie +import io.github.oshai.kotlinlogging.KLogging +import org.hamcrest.core.IsAnything +import org.springframework.http.HttpHeaders +import org.springframework.http.HttpMethod +import org.springframework.http.MediaType +import org.springframework.http.converter.HttpMessageConverter +import org.springframework.mock.web.MockHttpServletResponse +import org.springframework.mock.web.MockMultipartFile +import org.springframework.mock.web.MockPart +import org.springframework.test.web.servlet.MockMvc +import org.springframework.test.web.servlet.RequestBuilder +import org.springframework.test.web.servlet.ResultActions +import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder +import org.springframework.test.web.servlet.request.MockMultipartHttpServletRequestBuilder +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders +import org.springframework.test.web.servlet.result.MockMvcResultHandlers +import org.springframework.test.web.servlet.result.MockMvcResultMatchers +import org.springframework.test.web.servlet.setup.MockMvcBuilders +import org.springframework.test.web.servlet.setup.StandaloneMockMvcBuilder +import org.springframework.util.FileCopyUtils +import org.springframework.web.util.UriComponentsBuilder +import java.net.URI +import java.util.Objects +import javax.mail.internet.ContentDisposition +import javax.mail.internet.MimeMultipart +import javax.mail.util.ByteArrayDataSource +import kotlin.collections.forEach + +/** + * Test target for tests using Spring MockMvc. + */ +class Spring7MockMvcTestTarget @JvmOverloads constructor( + var mockMvc: MockMvc? = null, + var controllers: List = mutableListOf(), + var controllerAdvices: List = mutableListOf(), + var messageConverters: List> = mutableListOf(), + var printRequestResponse: Boolean = false, + var servletPath: String? = null +) : TestTarget { + override val userConfig: Map = emptyMap() + + override fun getProviderInfo(serviceName: String, pactSource: PactSource?) = ProviderInfo(serviceName) + + override fun prepareRequest( + pact: Pact, + interaction: Interaction, + context: MutableMap + ): Pair? { + if (interaction is SynchronousRequestResponse) { + val request = interaction.request.generatedRequest(context, GeneratorTestMode.Provider) + return toMockRequestBuilder(request) to buildMockMvc() + } + throw UnsupportedOperationException("Only request/response interactions can be used with an MockMvc test target") + } + + fun setControllers(vararg controllers: Any) { + this.controllers = controllers.asList() + } + + fun setControllerAdvices(vararg controllerAdvices: Any) { + this.controllerAdvices = controllerAdvices.asList() + } + + fun setMessageConverters(vararg messageConverters: HttpMessageConverter<*>) { + this.messageConverters = messageConverters.asList() + } + + private fun buildMockMvc(): MockMvc { + if (mockMvc != null) { + return mockMvc!! + } + + val requestBuilder = MockMvcRequestBuilders.get("/") + if (!servletPath.isNullOrEmpty()) { + requestBuilder.servletPath(servletPath!!) + } + + return MockMvcBuilders.standaloneSetup(*controllers.toTypedArray()) + .setControllerAdvice(*controllerAdvices.toTypedArray()) + .setMessageConverters(*messageConverters.toTypedArray()) + .defaultRequest(requestBuilder) + .build() + } + + private fun toMockRequestBuilder(request: IRequest): RequestBuilder{ + val body = request.body + val cookies = cookies(request) + val servletRequestBuilder: RequestBuilder = if (body.isPresent()) { + if (request.isMultipartFileUpload()) { + val multipart = MimeMultipart(ByteArrayDataSource(body.unwrap(), + request.asHttpPart().contentTypeHeader())) + val multipartRequest = MockMvcRequestBuilders.multipart(requestUriString(request)) + var i = 0 + while (i < multipart.count) { + val bodyPart = multipart.getBodyPart(i) + val contentDisposition = ContentDisposition(bodyPart.getHeader("Content-Disposition").first()) + val name = Objects.toString(contentDisposition.getParameter("name"), "file") + val filename = contentDisposition.getParameter("filename").orEmpty() + if (filename.isEmpty()) { + multipartRequest.part(MockPart(name, FileCopyUtils.copyToByteArray(bodyPart.inputStream))) + } else { + multipartRequest.file(MockMultipartFile(name, filename, bodyPart.contentType, bodyPart.inputStream)) + } + i++ + } + multipartRequest.headers(mapHeaders(request, true)) + } else { + MockMvcRequestBuilders.request(HttpMethod.valueOf(request.method), requestUriString(request)) + .headers(mapHeaders(request, true)) + .content(body.value!!) + } + } else { + MockMvcRequestBuilders.request(HttpMethod.valueOf(request.method), requestUriString(request)) + .headers(mapHeaders(request, false)) + } + if (cookies.isNotEmpty()) { + when (servletRequestBuilder) { + is MockHttpServletRequestBuilder -> servletRequestBuilder.cookie(*cookies) + is MockMultipartHttpServletRequestBuilder -> servletRequestBuilder.cookie(*cookies) + } + } + return servletRequestBuilder + } + + private fun cookies(request: IRequest): Array { + return request.cookies().map { + val values = it.split('=', limit = 2) + Cookie(values[0], values[1]) + }.toTypedArray() + } + + private fun requestUriString(request: IRequest): URI { + val uriBuilder = UriComponentsBuilder.fromPath(request.path) + + val query = request.query + if (query.isNotEmpty()) { + query.forEach { (key, value) -> + uriBuilder.queryParam(key, *value.toTypedArray()) + } + } + + return URI.create(uriBuilder.toUriString()) + } + + private fun mapHeaders(request: IRequest, hasBody: Boolean): HttpHeaders { + val httpHeaders = HttpHeaders() + + request.headers.forEach { (k, v) -> + httpHeaders.add(k, v.joinToString(", ")) + } + + if (hasBody && !httpHeaders.containsHeader(HttpHeaders.CONTENT_TYPE)) { + httpHeaders.add(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) + } + + return httpHeaders + } + + override fun isHttpTarget() = true + + override fun executeInteraction(client: Any?, request: Any?): ProviderResponse { + val mockMvcClient = client as MockMvc + val requestBuilder = request as MockHttpServletRequestBuilder + val mvcResult = performRequest(mockMvcClient, requestBuilder).andDo { + if (printRequestResponse) { + MockMvcResultHandlers.print().handle(it) + } + }.andReturn() + + return handleResponse(mvcResult.response) + } + + private fun performRequest(mockMvc: MockMvc, requestBuilder: RequestBuilder): ResultActions { + val resultActions = mockMvc.perform(requestBuilder) + return if (resultActions.andReturn().request.isAsyncStarted) { + mockMvc.perform(MockMvcRequestBuilders.asyncDispatch(resultActions + .andExpect(MockMvcResultMatchers.request().asyncResult(IsAnything())) + .andReturn())) + } else { + resultActions + } + } + + private fun handleResponse(httpResponse: MockHttpServletResponse): ProviderResponse { + logger.debug { "Received response: ${httpResponse.status}" } + + val headers = mutableMapOf>() + httpResponse.headerNames.forEach { headerName -> + headers[headerName] = listOfNotNull(httpResponse.getHeader(headerName)) + } + + val contentType = if (httpResponse.contentType.isNullOrEmpty()) { + ContentType.JSON + } else { + ContentType.fromString(httpResponse.contentType) + } + + val response = ProviderResponse(httpResponse.status, headers, contentType, + OptionalBody.body(httpResponse.contentAsString, contentType)) + + logger.debug { "Response: $response" } + + return response + } + + override fun prepareVerifier(verifier: IProviderVerifier, testInstance: Any, pact: Pact) { + /* NO-OP */ + } + + override fun supportsInteraction(interaction: Interaction) = interaction is SynchronousRequestResponse + + companion object : KLogging() +} diff --git a/provider/spring7/src/main/kotlin/au/com/dius/pact/provider/spring/spring7/WebFluxBasedTestTarget.kt b/provider/spring7/src/main/kotlin/au/com/dius/pact/provider/spring/spring7/WebFluxBasedTestTarget.kt new file mode 100644 index 0000000000..1474a33610 --- /dev/null +++ b/provider/spring7/src/main/kotlin/au/com/dius/pact/provider/spring/spring7/WebFluxBasedTestTarget.kt @@ -0,0 +1,120 @@ +package au.com.dius.pact.provider.spring.spring7 + +import au.com.dius.pact.core.model.* +import au.com.dius.pact.provider.IProviderVerifier +import au.com.dius.pact.provider.ProviderInfo +import au.com.dius.pact.provider.ProviderResponse +import au.com.dius.pact.provider.junit5.TestTarget +import org.apache.commons.lang3.StringUtils +import org.springframework.http.HttpHeaders +import org.springframework.http.HttpMethod +import org.springframework.http.MediaType +import org.springframework.http.client.MultipartBodyBuilder +import org.springframework.test.web.reactive.server.WebTestClient +import org.springframework.web.reactive.function.BodyInserters +import org.springframework.web.util.UriComponentsBuilder +import java.util.Objects +import javax.mail.internet.ContentDisposition +import javax.mail.internet.MimeMultipart +import javax.mail.util.ByteArrayDataSource +import kotlin.text.set + +/** + * An interface for a WebFlux based test target. + */ +interface WebFluxBasedTestTarget : TestTarget { + override fun getProviderInfo(serviceName: String, pactSource: PactSource?) = ProviderInfo(serviceName) + + override fun isHttpTarget() = true + + override fun executeInteraction(client: Any?, request: Any?): ProviderResponse { + val requestBuilder = request as WebTestClient.RequestHeadersSpec<*> + val exchangeResult = requestBuilder.exchange().expectBody().returnResult() + + val headers = mutableMapOf>() + + exchangeResult.responseHeaders.forEach( + { key, value -> + headers[key] = value + }) + + val contentTypeHeader = exchangeResult.responseHeaders.contentType + val contentType = if (contentTypeHeader == null) { + ContentType.JSON + } else { + ContentType.fromString(contentTypeHeader.toString()) + } + + return ProviderResponse( + exchangeResult.status.value(), + headers, + contentType, + OptionalBody.body(exchangeResult.responseBody?.let { String(it) }, contentType) + ) + } + + override fun prepareVerifier(verifier: IProviderVerifier, testInstance: Any, pact: Pact) { + /* NO-OP */ + } + + fun toWebFluxRequestBuilder(webClient: WebTestClient, request: IRequest): WebTestClient.RequestHeadersSpec<*> { + return if (request.body.isPresent()) { + if (request.isMultipartFileUpload()) { + val multipart = MimeMultipart(ByteArrayDataSource(request.body.unwrap(), request.contentTypeHeader())) + + val bodyBuilder = MultipartBodyBuilder() + var i = 0 + while (i < multipart.count) { + val bodyPart = multipart.getBodyPart(i) + val contentDisposition = ContentDisposition(bodyPart.getHeader("Content-Disposition").first()) + val name = Objects.toString(contentDisposition.getParameter("name"), "file") + val filename = contentDisposition.getParameter("filename").orEmpty() + + bodyBuilder + .part(name, bodyPart.content) + .filename(filename) + .contentType(MediaType.valueOf(bodyPart.contentType)) + .header("Content-Disposition", "form-data; name=$name; filename=$filename") + + i++ + } + + webClient + .method(HttpMethod.POST) + .uri(requestUriString(request)) + .body(BodyInserters.fromMultipartData(bodyBuilder.build())) + .headers { request.headers.forEach { (k, v) -> it.addAll(k, v) } } + } else { + webClient + .method(HttpMethod.valueOf(request.method)) + .uri(requestUriString(request)) + .bodyValue(request.body.value!!) + .headers { + request.headers.forEach { (k, v) -> it.addAll(k, v) } + if (!request.headers.containsKey(HttpHeaders.CONTENT_TYPE)) { + it.set(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) + } + } + } + } else { + webClient + .method(HttpMethod.valueOf(request.method)) + .uri(requestUriString(request)) + .headers { + request.headers.forEach { (k, v) -> it.addAll(k, v) } + } + } + } + + fun requestUriString(request: IRequest): String { + val uriBuilder = UriComponentsBuilder.fromPath(request.path) + + request.query.forEach { (key, value) -> + uriBuilder.queryParam(key, value) + } + + return uriBuilder.toUriString() + } + + override fun supportsInteraction(interaction: Interaction) = interaction is SynchronousRequestResponse +} diff --git a/provider/spring7/src/main/kotlin/au/com/dius/pact/provider/spring/spring7/WebFluxSpring7Target.kt b/provider/spring7/src/main/kotlin/au/com/dius/pact/provider/spring/spring7/WebFluxSpring7Target.kt new file mode 100644 index 0000000000..9e4bc6252f --- /dev/null +++ b/provider/spring7/src/main/kotlin/au/com/dius/pact/provider/spring/spring7/WebFluxSpring7Target.kt @@ -0,0 +1,21 @@ +package au.com.dius.pact.provider.spring.spring7 + +import au.com.dius.pact.core.model.Interaction +import au.com.dius.pact.core.model.Pact +import au.com.dius.pact.core.model.SynchronousRequestResponse +import au.com.dius.pact.core.model.generators.GeneratorTestMode +import org.springframework.test.web.reactive.server.WebTestClient +import org.springframework.web.reactive.function.server.RouterFunction + +class WebFluxSpring7Target(private val routerFunction: RouterFunction<*>) : WebFluxBasedTestTarget { + override val userConfig: Map = emptyMap() + + override fun prepareRequest(pact: Pact, interaction: Interaction, context: MutableMap): Pair? { + if (interaction is SynchronousRequestResponse) { + val request = interaction.request.generatedRequest(context, GeneratorTestMode.Provider) + val webClient = WebTestClient.bindToRouterFunction(routerFunction).build() + return toWebFluxRequestBuilder(webClient, request) to webClient + } + throw UnsupportedOperationException("Only request/response interactions can be used with a WebFlux test target") + } +} diff --git a/provider/spring7/src/main/kotlin/au/com/dius/pact/provider/spring/spring7/WebTestClientSpring7Target.kt b/provider/spring7/src/main/kotlin/au/com/dius/pact/provider/spring/spring7/WebTestClientSpring7Target.kt new file mode 100644 index 0000000000..0344b0a010 --- /dev/null +++ b/provider/spring7/src/main/kotlin/au/com/dius/pact/provider/spring/spring7/WebTestClientSpring7Target.kt @@ -0,0 +1,23 @@ +package au.com.dius.pact.provider.spring.spring7 + +import au.com.dius.pact.core.model.Interaction +import au.com.dius.pact.core.model.Pact +import au.com.dius.pact.core.model.SynchronousRequestResponse +import au.com.dius.pact.core.model.generators.GeneratorTestMode +import org.springframework.test.web.reactive.server.WebTestClient + +class WebTestClientSpring7Target(private val webTestClient: WebTestClient) : WebFluxBasedTestTarget { + override val userConfig: Map = emptyMap() + + override fun prepareRequest( + pact: Pact, + interaction: Interaction, + context: MutableMap + ): Pair? { + if (interaction is SynchronousRequestResponse) { + val request = interaction.request.generatedRequest(context, GeneratorTestMode.Provider) + return toWebFluxRequestBuilder(webTestClient, request) to webTestClient + } + throw UnsupportedOperationException("Only request/response interactions can be used with a WebFlux test target") + } +} diff --git a/provider/spring7/src/test/groovy/au/com/dius/pact/provider/spring/spring7/MockMvcTestTargetSpec.groovy b/provider/spring7/src/test/groovy/au/com/dius/pact/provider/spring/spring7/MockMvcTestTargetSpec.groovy new file mode 100644 index 0000000000..6f5afbc4f9 --- /dev/null +++ b/provider/spring7/src/test/groovy/au/com/dius/pact/provider/spring/spring7/MockMvcTestTargetSpec.groovy @@ -0,0 +1,147 @@ +package au.com.dius.pact.provider.spring.spring7 + +import au.com.dius.pact.core.model.OptionalBody +import au.com.dius.pact.core.model.Pact +import au.com.dius.pact.core.model.Request +import au.com.dius.pact.core.model.RequestResponseInteraction +import org.springframework.http.HttpStatus +import org.springframework.test.web.servlet.MockMvc +import org.springframework.test.web.servlet.setup.MockMvcBuilders +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.RequestParam +import org.springframework.web.bind.annotation.ResponseStatus +import org.springframework.web.bind.annotation.RestController +import spock.lang.Issue +import spock.lang.Specification + +import java.nio.charset.StandardCharsets + +class MockMvcTestTargetSpec extends Specification { + + Spring7MockMvcTestTarget mockMvcTestTarget + + def setup() { + mockMvcTestTarget = new Spring7MockMvcTestTarget(null, [new TestResource()]) + } + + def 'should prepare get request'() { + given: + def request = new Request('GET', '/data', [id: ['1234']]) + def interaction = new RequestResponseInteraction('some description', [], request) + def pact = Mock(Pact) + + when: + def requestAndClient = mockMvcTestTarget.prepareRequest(pact, interaction, [:]) + def requestBuilder = requestAndClient.first + def client = requestAndClient.second + + then: + client instanceof MockMvc + def builtRequest = requestBuilder.buildRequest(null) + builtRequest.requestURI == '/data' + builtRequest.method == 'GET' + builtRequest.parameterMap.id[0] == '1234' + } + + def 'should prepare get request with custom mockMvc'() { + given: + def mockMvc = MockMvcBuilders.standaloneSetup(new TestResource()).build() + def mockMvcTestTarget = new Spring7MockMvcTestTarget(mockMvc) + def request = new Request('GET', '/data', [id: ['1234']]) + def interaction = new RequestResponseInteraction('some description', [], request) + def pact = Mock(Pact) + + when: + def requestAndClient = mockMvcTestTarget.prepareRequest(pact, interaction, [:]) + def requestBuilder = requestAndClient.first + def client = requestAndClient.second + + then: + client === mockMvc + def builtRequest = requestBuilder.buildRequest(null) + builtRequest.requestURI == '/data' + builtRequest.method == 'GET' + builtRequest.parameterMap.id[0] == '1234' + } + + def 'should prepare post request'() { + given: + def request = new Request('POST', '/data', [id: ['1234']], [:], + OptionalBody.body('{"foo":"bar"}'.getBytes(StandardCharsets.UTF_8))) + def interaction = new RequestResponseInteraction('some description', [], request) + def pact = Mock(Pact) + + when: + def requestAndClient = mockMvcTestTarget.prepareRequest(pact, interaction, [:]) + def requestBuilder = requestAndClient.first + def client = requestAndClient.second + + then: + client instanceof MockMvc + def builtRequest = requestBuilder.characterEncoding('UTF-8').buildRequest(null) + builtRequest.requestURI == '/data' + builtRequest.contentAsString == '{"foo":"bar"}' + builtRequest.method == 'POST' + builtRequest.parameterMap.id[0] == '1234' + } + + def 'should execute interaction'() { + given: + def request = new Request('GET', '/data', [id: ['1234']]) + def interaction = new RequestResponseInteraction('some description', [], request) + def pact = Mock(Pact) + def requestAndClient = mockMvcTestTarget.prepareRequest(pact, interaction, [:]) + def requestBuilder = requestAndClient.first + def client = requestAndClient.second + + when: + def response = mockMvcTestTarget.executeInteraction(client, requestBuilder) + + then: + response.statusCode == 200 + response.contentType.toString() == 'application/json' + response.body.valueAsString() == 'Hello 1234' + } + + def 'should execute interaction with custom mockMvc'() { + given: + def mockMvc = MockMvcBuilders.standaloneSetup(new TestResource()).build() + def mockMvcTestTarget = new Spring7MockMvcTestTarget(mockMvc) + + def request = new Request('GET', '/data', [id: ['1234']]) + def interaction = new RequestResponseInteraction('some description', [], request) + def pact = Mock(Pact) + def requestAndClient = mockMvcTestTarget.prepareRequest(pact, interaction, [:]) + def requestBuilder = requestAndClient.first + def client = requestAndClient.second + + when: + def responseMap = mockMvcTestTarget.executeInteraction(client, requestBuilder) + + then: + responseMap.statusCode == 200 + responseMap.contentType.toString() == 'application/json' + responseMap.body.valueAsString() == 'Hello 1234' + } + + @Issue('#1788') + def 'query parameters with null and empty values'() { + given: + def pactRequest = new Request('GET', '/', ['A': ['', ''], 'B': [null, null]]) + + when: + def request = mockMvcTestTarget.requestUriString(pactRequest) + + then: + request.query == 'A=&A=&B&B' + } + + @RestController + static class TestResource { + @GetMapping(value = '/data', produces = 'application/json') + @ResponseStatus(HttpStatus.OK) + String getData(@RequestParam('id') String id) { + "Hello $id" + } + } +} diff --git a/provider/spring7/src/test/groovy/au/com/dius/pact/provider/spring/spring7/MockMvcTestWithCookieSpec.groovy b/provider/spring7/src/test/groovy/au/com/dius/pact/provider/spring/spring7/MockMvcTestWithCookieSpec.groovy new file mode 100644 index 0000000000..0b00c2dd8a --- /dev/null +++ b/provider/spring7/src/test/groovy/au/com/dius/pact/provider/spring/spring7/MockMvcTestWithCookieSpec.groovy @@ -0,0 +1,44 @@ +package au.com.dius.pact.provider.spring.spring7 + +import au.com.dius.pact.provider.junit5.PactVerificationContext +import au.com.dius.pact.provider.junitsupport.Provider +import au.com.dius.pact.provider.junitsupport.loader.PactFolder +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.TestTemplate +import org.junit.jupiter.api.extension.ExtendWith +import org.springframework.boot.webmvc.test.autoconfigure.WebMvcTest +import org.springframework.http.HttpStatus +import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder +import org.springframework.web.bind.annotation.CookieValue +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.RequestParam +import org.springframework.web.bind.annotation.ResponseStatus +import org.springframework.web.bind.annotation.RestController + +@WebMvcTest(controllers = [ CookieResource ]) +@Provider('CookieService') +@PactFolder('pacts') +class MockMvcTestWithCookieSpec { + + @BeforeEach + void before(PactVerificationContext context) { + context?.target = new Spring7MockMvcTestTarget(null, [new CookieResource() ], [], [], true) + } + + @TestTemplate + @ExtendWith(PactVerificationSpring7Provider) + void pactVerificationTestTemplate(PactVerificationContext context, MockHttpServletRequestBuilder request) { + request.header('test', 'test') + context?.verifyInteraction() + } + + @RestController + static class CookieResource { + @GetMapping(value = '/cookie', produces = 'text/plain') + @ResponseStatus(HttpStatus.OK) + String getData(@RequestParam('id') String id, @CookieValue('token') String token) { + assert token != null && !token.empty + "Hello $id $token" + } + } +} diff --git a/provider/spring7/src/test/groovy/au/com/dius/pact/provider/spring/spring7/WebFluxTargetSpec.groovy b/provider/spring7/src/test/groovy/au/com/dius/pact/provider/spring/spring7/WebFluxTargetSpec.groovy new file mode 100644 index 0000000000..dde1bb1342 --- /dev/null +++ b/provider/spring7/src/test/groovy/au/com/dius/pact/provider/spring/spring7/WebFluxTargetSpec.groovy @@ -0,0 +1,100 @@ +package au.com.dius.pact.provider.spring.spring7 + +import au.com.dius.pact.core.model.OptionalBody +import au.com.dius.pact.core.model.Pact +import au.com.dius.pact.core.model.Request +import au.com.dius.pact.core.model.RequestResponseInteraction +import au.com.dius.pact.core.model.V4Interaction +import au.com.dius.pact.core.model.messaging.Message +import org.springframework.http.MediaType +import org.springframework.test.web.reactive.server.WebTestClient +import org.springframework.web.reactive.function.BodyInserters +import org.springframework.web.reactive.function.server.RequestPredicates +import org.springframework.web.reactive.function.server.RouterFunction +import org.springframework.web.reactive.function.server.RouterFunctions +import org.springframework.web.reactive.function.server.ServerResponse +import spock.lang.Specification + +import java.nio.charset.StandardCharsets + +@SuppressWarnings('ClosureAsLastMethodParameter') +class WebFluxTargetSpec extends Specification { + RouterFunction routerFunction = RouterFunctions.route(RequestPredicates.GET('/data'), { req -> + ServerResponse.ok().contentType(MediaType.APPLICATION_JSON) + .body(BodyInserters.fromValue('{"id":1234}')) + }) + + def 'should prepare get request'() { + given: + WebFluxSpring7Target webFluxTarget = new WebFluxSpring7Target(routerFunction) + def request = new Request('GET', '/data', [id: ['1234']]) + def interaction = new RequestResponseInteraction('some description', [], request) + def pact = Mock(Pact) + + when: + def requestAndClient = webFluxTarget.prepareRequest(pact, interaction, [:]) + def requestBuilder = requestAndClient.first + def builtRequest = requestBuilder.exchange().expectBody().returnResult() + + then: + requestBuilder instanceof WebTestClient.RequestHeadersSpec + builtRequest.url.path == '/data' + builtRequest.method.toString() == 'GET' + new String(builtRequest.responseBody) == '{"id":1234}' + } + + def 'should prepare post request'() { + given: + RouterFunction postRouterFunction = RouterFunctions.route(RequestPredicates.POST('/data'), { req -> + assert req.queryParams() == [id: ['1234']] + def reqBody = req.bodyToMono(String).doOnNext({ s -> assert s == '{"foo":"bar"}' }) + ServerResponse.ok().build(reqBody) + }) + WebFluxSpring7Target webFluxTarget = new WebFluxSpring7Target(postRouterFunction) + def request = new Request('POST', '/data', [id: ['1234']], [:], + OptionalBody.body('{"foo":"bar"}'.getBytes(StandardCharsets.UTF_8))) + def interaction = new RequestResponseInteraction('some description', [], request) + def pact = Mock(Pact) + + when: + def requestAndClient = webFluxTarget.prepareRequest(pact, interaction, [:]) + def requestBuilder = requestAndClient.first + + then: + requestBuilder instanceof WebTestClient.RequestHeadersSpec + def builtRequest = requestBuilder.exchangeSuccessfully().expectBody().returnResult() + builtRequest.url.path == '/data' + builtRequest.method.toString() == 'POST' + } + + def 'should execute interaction'() { + given: + def request = new Request('GET', '/data', [id: ['1234']]) + def interaction = new RequestResponseInteraction('some description', [], request) + def pact = Mock(Pact) + WebFluxSpring7Target webFluxTarget = new WebFluxSpring7Target(routerFunction) + def requestAndClient = webFluxTarget.prepareRequest(pact, interaction, [:]) + def requestBuilder = requestAndClient.first + + when: + def response = webFluxTarget.executeInteraction(requestAndClient.second, requestBuilder) + + then: + response.statusCode == 200 + response.contentType.toString() == 'application/json' + response.body.valueAsString() == '{"id":1234}' + } + + def 'supports any HTTP interaction'() { + expect: + new WebFluxSpring7Target(routerFunction).supportsInteraction(interaction) == result + + where: + interaction | result + new RequestResponseInteraction('test') | true + new Message('test') | false + new V4Interaction.AsynchronousMessage('test') | false + new V4Interaction.SynchronousMessages('test') | false + new V4Interaction.SynchronousHttp('test') | true + } +} diff --git a/provider/spring7/src/test/groovy/au/com/dius/pact/provider/spring/spring7/WebTestClientTargetSpec.groovy b/provider/spring7/src/test/groovy/au/com/dius/pact/provider/spring/spring7/WebTestClientTargetSpec.groovy new file mode 100644 index 0000000000..df9693671e --- /dev/null +++ b/provider/spring7/src/test/groovy/au/com/dius/pact/provider/spring/spring7/WebTestClientTargetSpec.groovy @@ -0,0 +1,106 @@ +package au.com.dius.pact.provider.spring.spring7 + +import au.com.dius.pact.core.model.OptionalBody +import au.com.dius.pact.core.model.Pact +import au.com.dius.pact.core.model.Request +import au.com.dius.pact.core.model.RequestResponseInteraction +import au.com.dius.pact.core.model.V4Interaction +import au.com.dius.pact.core.model.messaging.Message +import org.springframework.http.MediaType +import org.springframework.test.web.reactive.server.WebTestClient +import org.springframework.web.reactive.function.BodyInserters +import org.springframework.web.reactive.function.server.RequestPredicates +import org.springframework.web.reactive.function.server.RouterFunction +import org.springframework.web.reactive.function.server.RouterFunctions +import org.springframework.web.reactive.function.server.ServerResponse +import spock.lang.Specification + +import java.nio.charset.StandardCharsets + +import static org.springframework.test.web.reactive.server.WebTestClient.bindToRouterFunction + +@SuppressWarnings('ClosureAsLastMethodParameter') +class WebTestClientTargetSpec extends Specification { + RouterFunction routerFunction = RouterFunctions.route(RequestPredicates.GET('/data'), { req -> + ServerResponse.ok().contentType(MediaType.APPLICATION_JSON) + .body(BodyInserters.fromValue('{"id":1234}')) + }) + + def 'should prepare get request'() { + given: + WebTestClientSpring7Target webTestClientTarget = new WebTestClientSpring7Target( + WebTestClient.bindToRouterFunction(routerFunction).build()) + def request = new Request('GET', '/data', [id: ['1234']]) + def interaction = new RequestResponseInteraction('some description', [], request) + def pact = Mock(Pact) + + when: + def requestAndClient = webTestClientTarget.prepareRequest(pact, interaction, [:]) + def requestBuilder = requestAndClient.first + def builtRequest = requestBuilder.exchange().expectBody().returnResult() + + then: + requestBuilder instanceof WebTestClient.RequestHeadersSpec + builtRequest.url.path == '/data' + builtRequest.method.toString() == 'GET' + new String(builtRequest.responseBody) == '{"id":1234}' + } + + def 'should prepare post request'() { + given: + RouterFunction postRouterFunction = RouterFunctions.route(RequestPredicates.POST('/data'), { req -> + assert req.queryParams() == [id: ['1234']] + def reqBody = req.bodyToMono(String).doOnNext({ s -> assert s == '{"foo":"bar"}' }) + ServerResponse.ok().build(reqBody) + }) + WebTestClientSpring7Target webTestClientTarget = new WebTestClientSpring7Target( + WebTestClient.bindToRouterFunction(postRouterFunction).build()) + def request = new Request('POST', '/data', [id: ['1234']], [:], + OptionalBody.body('{"foo":"bar"}'.getBytes(StandardCharsets.UTF_8))) + def interaction = new RequestResponseInteraction('some description', [], request) + def pact = Mock(Pact) + + when: + def requestAndClient = webTestClientTarget.prepareRequest(pact, interaction, [:]) + def requestBuilder = requestAndClient.first + + then: + requestBuilder instanceof WebTestClient.RequestHeadersSpec + def builtRequest = requestBuilder.exchangeSuccessfully().expectBody().returnResult() + builtRequest.url.path == '/data' + builtRequest.method.toString() == 'POST' + } + + def 'should execute interaction'() { + given: + def request = new Request('GET', '/data', [id: ['1234']]) + def interaction = new RequestResponseInteraction('some description', [], request) + def pact = Mock(Pact) + WebTestClientSpring7Target webTestClientTarget = new WebTestClientSpring7Target( + WebTestClient.bindToRouterFunction(routerFunction).build()) + def requestAndClient = webTestClientTarget.prepareRequest(pact, interaction, [:]) + def requestBuilder = requestAndClient.first + + when: + def response = webTestClientTarget.executeInteraction(requestAndClient.second, requestBuilder) + + then: + response.statusCode == 200 + response.contentType.toString() == 'application/json' + response.body.valueAsString() == '{"id":1234}' + } + + def 'supports any HTTP interaction'() { + expect: + new WebTestClientSpring7Target(WebTestClient.bindToRouterFunction(routerFunction).build()) + .supportsInteraction(interaction) == result + + where: + interaction | result + new RequestResponseInteraction('test') | true + new Message('test') | false + new V4Interaction.AsynchronousMessage('test') | false + new V4Interaction.SynchronousMessages('test') | false + new V4Interaction.SynchronousHttp('test') | true + } +} diff --git a/provider/spring7/src/test/java/au/com/dius/pact/provider/spring/spring7/ConsumerVersionSelectorJavaTest.java b/provider/spring7/src/test/java/au/com/dius/pact/provider/spring/spring7/ConsumerVersionSelectorJavaTest.java new file mode 100644 index 0000000000..4bb865de1d --- /dev/null +++ b/provider/spring7/src/test/java/au/com/dius/pact/provider/spring/spring7/ConsumerVersionSelectorJavaTest.java @@ -0,0 +1,44 @@ +package au.com.dius.pact.provider.spring.spring7; + +import au.com.dius.pact.provider.junit5.PactVerificationContext; +import au.com.dius.pact.provider.junitsupport.IgnoreNoPactsToVerify; +import au.com.dius.pact.provider.junitsupport.Provider; +import au.com.dius.pact.provider.junitsupport.loader.PactBroker; +import au.com.dius.pact.provider.junitsupport.loader.PactBrokerConsumerVersionSelectors; +import au.com.dius.pact.provider.junitsupport.loader.SelectorBuilder; +import org.junit.jupiter.api.AfterAll; +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.junit.jupiter.SpringExtension; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; + +@ExtendWith(SpringExtension.class) +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@Provider("Animal Profile Service") +@PactBroker +@IgnoreNoPactsToVerify(ignoreIoErrors = "true") +class ConsumerVersionSelectorJavaTest { + static boolean called = false; + + @PactBrokerConsumerVersionSelectors + public static SelectorBuilder consumerVersionSelectors() { + called = true; + return new SelectorBuilder().branch("current"); + } + + @TestTemplate + @ExtendWith(PactVerificationSpring7Provider.class) + void pactVerificationTestTemplate(PactVerificationContext context) { + if (context != null) { + context.verifyInteraction(); + } + } + + @AfterAll + static void after() { + assertThat("consumerVersionSelectors() was not called", called, is(true)); + } +} diff --git a/provider/spring7/src/test/java/au/com/dius/pact/provider/spring/spring7/MockMvcTestTargetStandaloneMockMvcTestJava.java b/provider/spring7/src/test/java/au/com/dius/pact/provider/spring/spring7/MockMvcTestTargetStandaloneMockMvcTestJava.java new file mode 100644 index 0000000000..d2971eb368 --- /dev/null +++ b/provider/spring7/src/test/java/au/com/dius/pact/provider/spring/spring7/MockMvcTestTargetStandaloneMockMvcTestJava.java @@ -0,0 +1,53 @@ +package au.com.dius.pact.provider.spring.spring7; + +import au.com.dius.pact.provider.junitsupport.Provider; +import au.com.dius.pact.provider.junitsupport.loader.PactFolder; +import au.com.dius.pact.provider.junit5.PactVerificationContext; +import au.com.dius.pact.provider.junit5.PactVerificationInvocationContextProvider; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.TestTemplate; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.context.request.async.DeferredResult; + +import java.util.concurrent.CompletableFuture; + +@Provider("myAwesomeService") +@PactFolder("pacts") +class MockMvcTestTargetStandaloneMockMvcTestJava { + + @TestTemplate + @ExtendWith(PactVerificationInvocationContextProvider.class) + void pactVerificationTestTemplate(PactVerificationContext context) { + context.verifyInteraction(); + } + + @BeforeEach + void before(PactVerificationContext context) { + Spring7MockMvcTestTarget testTarget = new Spring7MockMvcTestTarget(); + testTarget.setControllers(new DataResource()); + context.setTarget(testTarget); + } + + @RestController + static class DataResource { + @GetMapping("/data") + @ResponseStatus(HttpStatus.NO_CONTENT) + void getData(@RequestParam("ticketId") String ticketId) { + } + + @GetMapping("/async-data") + DeferredResult> getAsyncData(@RequestParam("ticketId") String ticketId) { + DeferredResult> result = new DeferredResult<>(); + CompletableFuture.runAsync(() -> result.setResult(ResponseEntity + .noContent() + .build())); + return result; + } + } +} diff --git a/provider/spring7/src/test/java/au/com/dius/pact/provider/spring/spring7/MockMvcTestTargetWebMvcTestJava.java b/provider/spring7/src/test/java/au/com/dius/pact/provider/spring/spring7/MockMvcTestTargetWebMvcTestJava.java new file mode 100644 index 0000000000..aa70fb1eee --- /dev/null +++ b/provider/spring7/src/test/java/au/com/dius/pact/provider/spring/spring7/MockMvcTestTargetWebMvcTestJava.java @@ -0,0 +1,60 @@ +package au.com.dius.pact.provider.spring.spring7; + +import au.com.dius.pact.provider.junit5.PactVerificationContext; +import au.com.dius.pact.provider.junit5.PactVerificationInvocationContextProvider; +import au.com.dius.pact.provider.junitsupport.Provider; +import au.com.dius.pact.provider.junitsupport.loader.PactFolder; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.TestTemplate; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.webmvc.test.autoconfigure.WebMvcTest; +import org.springframework.context.annotation.Import; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.context.request.async.DeferredResult; + +import java.util.concurrent.CompletableFuture; + +@WebMvcTest +@Import(SecurityTestConfiguration.class) +@Provider("myAwesomeService") +@PactFolder("pacts") +class MockMvcTestTargetWebMvcTestJava { + + @Autowired + private MockMvc mockMvc; + + @TestTemplate + @ExtendWith(PactVerificationInvocationContextProvider.class) + void pactVerificationTestTemplate(PactVerificationContext context) { + context.verifyInteraction(); + } + + @BeforeEach + void before(PactVerificationContext context) { + context.setTarget(new Spring7MockMvcTestTarget(mockMvc)); + } + + @RestController + static class DataResource { + @GetMapping("/data") + @ResponseStatus(HttpStatus.NO_CONTENT) + void getData(@RequestParam("ticketId") String ticketId) { + } + + @GetMapping("/async-data") + DeferredResult> getAsyncData(@RequestParam("ticketId") String ticketId) { + DeferredResult> result = new DeferredResult<>(); + CompletableFuture.runAsync(() -> result.setResult(ResponseEntity + .noContent() + .build())); + return result; + } + } +} diff --git a/provider/spring7/src/test/java/au/com/dius/pact/provider/spring/spring7/SecurityTestConfiguration.java b/provider/spring7/src/test/java/au/com/dius/pact/provider/spring/spring7/SecurityTestConfiguration.java new file mode 100644 index 0000000000..ec22aeb889 --- /dev/null +++ b/provider/spring7/src/test/java/au/com/dius/pact/provider/spring/spring7/SecurityTestConfiguration.java @@ -0,0 +1,19 @@ +package au.com.dius.pact.provider.spring.spring7; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.web.SecurityFilterChain; + +@Configuration +@EnableWebSecurity +public class SecurityTestConfiguration { + @Bean + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + http + .authorizeHttpRequests(authz -> authz.anyRequest().permitAll()) + .csrf(csrf -> csrf.disable()); + return http.build(); + } +} diff --git a/provider/spring7/src/test/java/au/com/dius/pact/provider/spring/spring7/WebTestClientPactTest.java b/provider/spring7/src/test/java/au/com/dius/pact/provider/spring/spring7/WebTestClientPactTest.java new file mode 100644 index 0000000000..7e20f35854 --- /dev/null +++ b/provider/spring7/src/test/java/au/com/dius/pact/provider/spring/spring7/WebTestClientPactTest.java @@ -0,0 +1,50 @@ +package au.com.dius.pact.provider.spring.spring7; + +import au.com.dius.pact.provider.junit5.PactVerificationContext; +import au.com.dius.pact.provider.junitsupport.Provider; +import au.com.dius.pact.provider.junitsupport.loader.PactFolder; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.TestTemplate; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.MediaType; +import org.springframework.test.context.junit.jupiter.SpringExtension; +import org.springframework.test.web.reactive.server.WebTestClient; +import org.springframework.web.reactive.function.server.*; +import reactor.core.publisher.Mono; + +@ExtendWith(SpringExtension.class) +@SpringBootTest +@Provider("myAwesomeService") +@PactFolder("pacts") +class WebTestClientPactTest { + + public static class Handler { + public Mono handleRequest(ServerRequest request) { + return ServerResponse.noContent().build(); + } + } + + static class Router { + public RouterFunction route(Handler handler) { + return RouterFunctions + .route(RequestPredicates.GET("/data").and(RequestPredicates.accept(MediaType.APPLICATION_JSON)), + handler::handleRequest) + .andRoute(RequestPredicates.GET("/async-data").and(RequestPredicates.accept(MediaType.APPLICATION_JSON)), + handler::handleRequest); + } + } + + @BeforeEach + void setup(PactVerificationContext context) { + Handler handler = new Handler(); + WebTestClient webTestClient = WebTestClient.bindToRouterFunction(new Router().route(handler)).build(); + context.setTarget(new WebTestClientSpring7Target(webTestClient)); + } + + @TestTemplate + @ExtendWith(PactVerificationSpring7Provider.class) + void pactVerificationTestTemplate(PactVerificationContext context) { + context.verifyInteraction(); + } +} diff --git a/provider/spring7/src/test/java/au/com/dius/pact/provider/spring/spring7/WebfluxPactTest.java b/provider/spring7/src/test/java/au/com/dius/pact/provider/spring/spring7/WebfluxPactTest.java new file mode 100644 index 0000000000..456d24904a --- /dev/null +++ b/provider/spring7/src/test/java/au/com/dius/pact/provider/spring/spring7/WebfluxPactTest.java @@ -0,0 +1,48 @@ +package au.com.dius.pact.provider.spring.spring7; + +import au.com.dius.pact.provider.junit5.PactVerificationContext; +import au.com.dius.pact.provider.junitsupport.Provider; +import au.com.dius.pact.provider.junitsupport.loader.PactFolder; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.TestTemplate; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.MediaType; +import org.springframework.test.context.junit.jupiter.SpringExtension; +import org.springframework.web.reactive.function.server.*; +import reactor.core.publisher.Mono; + +@ExtendWith(SpringExtension.class) +@SpringBootTest +@Provider("myAwesomeService") +@PactFolder("pacts") +public class WebfluxPactTest { + + public static class Handler { + public Mono handleRequest(ServerRequest request) { + return ServerResponse.noContent().build(); + } + } + + static class Router { + public RouterFunction route(Handler handler) { + return RouterFunctions + .route(RequestPredicates.GET("/data").and(RequestPredicates.accept(MediaType.APPLICATION_JSON)), + handler::handleRequest) + .andRoute(RequestPredicates.GET("/async-data").and(RequestPredicates.accept(MediaType.APPLICATION_JSON)), + handler::handleRequest); + } + } + + @BeforeEach + void setup(PactVerificationContext context) { + Handler handler = new Handler(); + context.setTarget(new WebFluxSpring7Target(new Router().route(handler))); + } + + @TestTemplate + @ExtendWith(PactVerificationSpring7Provider.class) + void pactVerificationTestTemplate(PactVerificationContext context) { + context.verifyInteraction(); + } +} diff --git a/provider/spring7/src/test/kotlin/au/com/dius/pact/provider/spring/spring7/ConsumerVersionSelectorKotlinTest.kt b/provider/spring7/src/test/kotlin/au/com/dius/pact/provider/spring/spring7/ConsumerVersionSelectorKotlinTest.kt new file mode 100644 index 0000000000..8b72045c06 --- /dev/null +++ b/provider/spring7/src/test/kotlin/au/com/dius/pact/provider/spring/spring7/ConsumerVersionSelectorKotlinTest.kt @@ -0,0 +1,41 @@ +package au.com.dius.pact.provider.spring.spring7 + +import au.com.dius.pact.provider.junit5.PactVerificationContext +import au.com.dius.pact.provider.junitsupport.IgnoreNoPactsToVerify +import au.com.dius.pact.provider.junitsupport.Provider +import au.com.dius.pact.provider.junitsupport.loader.PactBroker +import au.com.dius.pact.provider.junitsupport.loader.PactBrokerConsumerVersionSelectors +import au.com.dius.pact.provider.junitsupport.loader.SelectorBuilder +import org.hamcrest.MatcherAssert +import org.hamcrest.Matchers +import org.junit.jupiter.api.AfterAll +import org.junit.jupiter.api.TestTemplate +import org.junit.jupiter.api.extension.ExtendWith +import org.springframework.boot.test.context.SpringBootTest + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@Provider("Animal Profile Service") +@PactBroker +@IgnoreNoPactsToVerify(ignoreIoErrors = "true") +open class ConsumerVersionSelectorKotlinTest { + @PactBrokerConsumerVersionSelectors + fun consumerVersionSelectors(): SelectorBuilder { + called = true + return SelectorBuilder().branch("current") + } + + @TestTemplate + @ExtendWith(PactVerificationSpring7Provider::class) + fun pactVerificationTestTemplate(context: PactVerificationContext?) { + context?.verifyInteraction() + } + + companion object { + private var called: Boolean = false + + @AfterAll + fun after() { + MatcherAssert.assertThat("consumerVersionSelectors() was not called", called, Matchers.`is`(true)) + } + } +} diff --git a/provider/spring7/src/test/kotlin/au/com/dius/pact/provider/spring/spring7/MockMvcTestTargetNoCustomMockMvcTest.kt b/provider/spring7/src/test/kotlin/au/com/dius/pact/provider/spring/spring7/MockMvcTestTargetNoCustomMockMvcTest.kt new file mode 100644 index 0000000000..491ea23f1f --- /dev/null +++ b/provider/spring7/src/test/kotlin/au/com/dius/pact/provider/spring/spring7/MockMvcTestTargetNoCustomMockMvcTest.kt @@ -0,0 +1,54 @@ +package au.com.dius.pact.provider.spring.spring7 + +import au.com.dius.pact.provider.junit5.PactVerificationContext +import au.com.dius.pact.provider.junit5.PactVerificationInvocationContextProvider +import au.com.dius.pact.provider.junitsupport.IgnoreNoPactsToVerify +import au.com.dius.pact.provider.junitsupport.Provider +import au.com.dius.pact.provider.junitsupport.loader.PactFolder +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.TestTemplate +import org.junit.jupiter.api.extension.ExtendWith +import org.springframework.http.HttpStatus +import org.springframework.http.ResponseEntity +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.RequestParam +import org.springframework.web.bind.annotation.ResponseStatus +import org.springframework.web.bind.annotation.RestController +import org.springframework.web.context.request.async.DeferredResult +import java.util.concurrent.CompletableFuture + +@Provider("myAwesomeService") +@IgnoreNoPactsToVerify +@PactFolder("pacts") +internal class MockMvcTestTargetNoCustomMockMvcTest { + + @TestTemplate + @ExtendWith(PactVerificationInvocationContextProvider::class) + fun pactVerificationTestTemplate(context: PactVerificationContext?) { + context?.verifyInteraction() + } + + @BeforeEach + fun before(context: PactVerificationContext?) { + context?.target = Spring7MockMvcTestTarget(controllers = listOf(DataResource())) + } + + @RestController + internal class DataResource { + @GetMapping("/data") + @ResponseStatus(HttpStatus.NO_CONTENT) + fun getData(@RequestParam("ticketId") ticketId: String) { + } + + @GetMapping("/async-data") + fun getAsyncData(@RequestParam("ticketId") ticketId: String): DeferredResult> { + val result = DeferredResult>() + CompletableFuture.runAsync { + result.setResult(ResponseEntity + .noContent() + .build()) + } + return result + } + } +} diff --git a/provider/spring7/src/test/kotlin/au/com/dius/pact/provider/spring/spring7/MockMvcTestTargetStandaloneMockMvcTest.kt b/provider/spring7/src/test/kotlin/au/com/dius/pact/provider/spring/spring7/MockMvcTestTargetStandaloneMockMvcTest.kt new file mode 100644 index 0000000000..33da99241e --- /dev/null +++ b/provider/spring7/src/test/kotlin/au/com/dius/pact/provider/spring/spring7/MockMvcTestTargetStandaloneMockMvcTest.kt @@ -0,0 +1,57 @@ +package au.com.dius.pact.provider.spring.spring7 + +import au.com.dius.pact.provider.junit5.PactVerificationContext +import au.com.dius.pact.provider.junit5.PactVerificationInvocationContextProvider +import au.com.dius.pact.provider.junitsupport.IgnoreNoPactsToVerify +import au.com.dius.pact.provider.junitsupport.Provider +import au.com.dius.pact.provider.junitsupport.loader.PactFolder +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.TestTemplate +import org.junit.jupiter.api.extension.ExtendWith +import org.springframework.http.HttpStatus +import org.springframework.http.ResponseEntity +import org.springframework.test.web.servlet.setup.MockMvcBuilders +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.RequestParam +import org.springframework.web.bind.annotation.ResponseStatus +import org.springframework.web.bind.annotation.RestController +import org.springframework.web.context.request.async.DeferredResult +import java.util.concurrent.CompletableFuture + +@Provider("myAwesomeService") +@IgnoreNoPactsToVerify +@PactFolder("pacts") +internal class MockMvcTestTargetStandaloneMockMvcTest { + + val mockMvc = MockMvcBuilders.standaloneSetup(DataResource()).build() + + @TestTemplate + @ExtendWith(PactVerificationInvocationContextProvider::class) + fun pactVerificationTestTemplate(context: PactVerificationContext?) { + context?.verifyInteraction() + } + + @BeforeEach + fun before(context: PactVerificationContext?) { + context?.target = Spring7MockMvcTestTarget(mockMvc) + } + + @RestController + internal class DataResource { + @GetMapping("/data") + @ResponseStatus(HttpStatus.NO_CONTENT) + fun getData(@RequestParam("ticketId") ticketId: String) { + } + + @GetMapping("/async-data") + fun getAsyncData(@RequestParam("ticketId") ticketId: String): DeferredResult> { + val result = DeferredResult>() + CompletableFuture.runAsync { + result.setResult(ResponseEntity + .noContent() + .build()) + } + return result + } + } +} diff --git a/provider/spring7/src/test/kotlin/au/com/dius/pact/provider/spring/spring7/MockMvcTestTargetWebMvcTest.kt b/provider/spring7/src/test/kotlin/au/com/dius/pact/provider/spring/spring7/MockMvcTestTargetWebMvcTest.kt new file mode 100644 index 0000000000..42b0fb0fba --- /dev/null +++ b/provider/spring7/src/test/kotlin/au/com/dius/pact/provider/spring/spring7/MockMvcTestTargetWebMvcTest.kt @@ -0,0 +1,63 @@ +package au.com.dius.pact.provider.spring.spring7 + +import au.com.dius.pact.provider.junit5.PactVerificationContext +import au.com.dius.pact.provider.junit5.PactVerificationInvocationContextProvider +import au.com.dius.pact.provider.junitsupport.IgnoreNoPactsToVerify +import au.com.dius.pact.provider.junitsupport.Provider +import au.com.dius.pact.provider.junitsupport.loader.PactFolder +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.TestTemplate +import org.junit.jupiter.api.extension.ExtendWith +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.webmvc.test.autoconfigure.WebMvcTest +import org.springframework.context.annotation.Import +import org.springframework.http.HttpStatus +import org.springframework.http.ResponseEntity +import org.springframework.test.web.servlet.MockMvc +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.RequestParam +import org.springframework.web.bind.annotation.ResponseStatus +import org.springframework.web.bind.annotation.RestController +import org.springframework.web.context.request.async.DeferredResult +import java.util.concurrent.CompletableFuture + +@WebMvcTest +@Import(SecurityTestConfiguration::class) +@Provider("myAwesomeService") +@IgnoreNoPactsToVerify +@PactFolder("pacts") +internal class MockMvcTestTargetWebMvcTest { + + @Autowired + lateinit var mockMvc: MockMvc + + @TestTemplate + @ExtendWith(PactVerificationInvocationContextProvider::class) + fun pactVerificationTestTemplate(context: PactVerificationContext?) { + context?.verifyInteraction() + } + + @BeforeEach + fun before(context: PactVerificationContext?) { + context?.target = Spring7MockMvcTestTarget(mockMvc) + } +} + +@RestController +internal class DataResource { + @GetMapping("/data") + @ResponseStatus(HttpStatus.NO_CONTENT) + fun getData(@RequestParam("ticketId") ticketId: String) { + } + + @GetMapping("/async-data") + fun getAsyncData(@RequestParam("ticketId") ticketId: String): DeferredResult> { + val result = DeferredResult>() + CompletableFuture.runAsync { + result.setResult(ResponseEntity + .noContent() + .build()) + } + return result + } +} diff --git a/provider/spring7/src/test/kotlin/au/com/dius/pact/provider/spring/spring7/PactVerificationSpringProviderTest.kt b/provider/spring7/src/test/kotlin/au/com/dius/pact/provider/spring/spring7/PactVerificationSpringProviderTest.kt new file mode 100644 index 0000000000..ff928b8472 --- /dev/null +++ b/provider/spring7/src/test/kotlin/au/com/dius/pact/provider/spring/spring7/PactVerificationSpringProviderTest.kt @@ -0,0 +1,27 @@ +package au.com.dius.pact.provider.spring.spring7 + +import au.com.dius.pact.provider.junit5.PactVerificationContext +import au.com.dius.pact.provider.junitsupport.IgnoreNoPactsToVerify +import au.com.dius.pact.provider.junitsupport.Provider +import au.com.dius.pact.provider.junitsupport.loader.PactBroker +import org.junit.jupiter.api.TestTemplate +import org.junit.jupiter.api.extension.ExtendWith +import org.springframework.boot.autoconfigure.SpringBootApplication +import org.springframework.boot.test.context.SpringBootTest +import org.springframework.test.context.junit.jupiter.SpringExtension + +@SpringBootApplication +open class TestApplication + +@ExtendWith(SpringExtension::class) +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@Provider("Animal Profile Service") +@PactBroker +@IgnoreNoPactsToVerify(ignoreIoErrors = "true") +internal class PactVerificationSpringProviderTest { + @TestTemplate + @ExtendWith(PactVerificationSpring7Provider::class) + fun pactVerificationTestTemplate(context: PactVerificationContext?) { + context?.verifyInteraction() + } +} diff --git a/provider/spring7/src/test/resources/application.yml b/provider/spring7/src/test/resources/application.yml new file mode 100644 index 0000000000..3115ba717b --- /dev/null +++ b/provider/spring7/src/test/resources/application.yml @@ -0,0 +1,3 @@ +pactbroker: + host: localhost + port: ${local.server.port} diff --git a/provider/spring7/src/test/resources/pacts/contract.json b/provider/spring7/src/test/resources/pacts/contract.json new file mode 100644 index 0000000000..7b4ec344ae --- /dev/null +++ b/provider/spring7/src/test/resources/pacts/contract.json @@ -0,0 +1,39 @@ +{ + "provider" : { + "name" : "myAwesomeService" + }, + "consumer" : { + "name" : "anotherService" + }, + "interactions" : [ { + "description" : "Get data", + "request" : { + "method" : "GET", + "path" : "/data", + "query": "ticketId=0000" + }, + "response" : { + "status" : 204 + } + }, + { + "description" : "Get async data", + "request" : { + "method" : "GET", + "path" : "/async-data", + "query": "ticketId=0000" + }, + "response" : { + "status" : 204 + } + } + ], + "metadata" : { + "pact-specification" : { + "version" : "2.0.0" + }, + "pact-jvm" : { + "version" : "3.1.1" + } + } +} diff --git a/provider/spring7/src/test/resources/pacts/cookie.json b/provider/spring7/src/test/resources/pacts/cookie.json new file mode 100644 index 0000000000..8b4b172dbf --- /dev/null +++ b/provider/spring7/src/test/resources/pacts/cookie.json @@ -0,0 +1,31 @@ +{ + "provider" : { + "name" : "CookieService" + }, + "consumer" : { + "name" : "CookieConsumer" + }, + "interactions" : [ { + "description" : "Get data", + "request" : { + "method" : "GET", + "path" : "/cookie", + "query" : "id=0000", + "headers" : { + "Cookie" : "token=1234abcd" + } + }, + "response" : { + "status" : 200, + "body" : "Hello 0000 1234abcd" + } + } ], + "metadata" : { + "pact-specification" : { + "version" : "2.0.0" + }, + "pact-jvm" : { + "version" : "3.1.1" + } + } +} diff --git a/settings.gradle b/settings.gradle index e5fdf45e6f..a9ab16a1c9 100644 --- a/settings.gradle +++ b/settings.gradle @@ -19,6 +19,7 @@ include 'provider:junit5' include 'provider:spring' include 'provider:junit5spring' include 'provider:spring6' +include 'provider:spring7' // In Windows, fails with java.lang.NullPointerException if (System.getenv('GITHUB_WORKFLOW') == null) {