From 8e6a576d1ee6c6d1f6415d3221d621e44df421b0 Mon Sep 17 00:00:00 2001 From: Marcin Grzejszczak Date: Tue, 9 Mar 2021 10:02:04 +0100 Subject: [PATCH 1/2] Properly escapes query parameters; fixes gh-1441 --- .../verifier/builder/GroovyBodyParser.java | 10 +- .../verifier/builder/JaxRsUrlPathWhen.java | 10 +- .../contract/verifier/builder/JaxRsWhen.java | 3 +- .../JaxRsClientMethodBuilderSpec.groovy | 104 ++++++++++++++---- .../builder/MethodBodyBuilderSpec.groovy | 43 ++++++++ 5 files changed, 143 insertions(+), 27 deletions(-) diff --git a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/GroovyBodyParser.java b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/GroovyBodyParser.java index 8547042824..5333825dcd 100644 --- a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/GroovyBodyParser.java +++ b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/GroovyBodyParser.java @@ -78,7 +78,15 @@ interface GroovyBodyParser extends BodyParser { @Override default String quotedLongText(Object text) { - return "'''" + groovyEscapedString(text) + "'''"; + String escapedString = groovyEscapedString(text); + if (escapedString.startsWith("'")) { + escapedString = "\\'" + escapedString.substring(1); + } + if (escapedString.endsWith("'")) { + escapedString = escapedString.substring(0, escapedString.length() - 1) + + "\\'"; + } + return "'''" + escapedString + "'''"; } default String groovyEscapedString(Object text) { diff --git a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/JaxRsUrlPathWhen.java b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/JaxRsUrlPathWhen.java index 13d1313f72..e0a28ab65d 100644 --- a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/JaxRsUrlPathWhen.java +++ b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/JaxRsUrlPathWhen.java @@ -33,9 +33,13 @@ class JaxRsUrlPathWhen implements When, JaxRsAcceptor, QueryParamsResolver { private final GeneratedClassMetaData generatedClassMetaData; - JaxRsUrlPathWhen(BlockBuilder blockBuilder, GeneratedClassMetaData metaData) { + private final BodyParser bodyParser; + + JaxRsUrlPathWhen(BlockBuilder blockBuilder, GeneratedClassMetaData metaData, + BodyParser bodyParser) { this.blockBuilder = blockBuilder; this.generatedClassMetaData = metaData; + this.bodyParser = bodyParser; } @Override @@ -73,8 +77,8 @@ class JaxRsUrlPathWhen implements When, JaxRsAcceptor, QueryParamsResolver { .filter(this::allowedQueryParameter).iterator(); while (iterator.hasNext()) { QueryParameter param = iterator.next(); - String text = ".queryParam(\"" + param.getName() + "\", \"" - + resolveParamValue(param) + "\")"; + String text = ".queryParam(\"" + param.getName() + "\", " + + this.bodyParser.quotedShortText(resolveParamValue(param)) + ")"; if (iterator.hasNext()) { this.blockBuilder.addLine(text); } diff --git a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/JaxRsWhen.java b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/JaxRsWhen.java index 8311bbd1f4..ab8297c775 100644 --- a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/JaxRsWhen.java +++ b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/JaxRsWhen.java @@ -38,7 +38,8 @@ class JaxRsWhen implements When, BodyMethodVisitor, JaxRsAcceptor { this.generatedClassMetaData = generatedClassMetaData; this.bodyParser = bodyParser; this.whens.addAll(Arrays.asList( - new JaxRsUrlPathWhen(this.blockBuilder, this.generatedClassMetaData), + new JaxRsUrlPathWhen(this.blockBuilder, this.generatedClassMetaData, + bodyParser), new JaxRsRequestWhen(this.blockBuilder, this.generatedClassMetaData), new JaxRsRequestHeadersWhen(this.blockBuilder, bodyParser), new JaxRsRequestCookiesWhen(this.blockBuilder, bodyParser), diff --git a/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/builder/JaxRsClientMethodBuilderSpec.groovy b/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/builder/JaxRsClientMethodBuilderSpec.groovy index 6a40910e59..06b494d459 100644 --- a/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/builder/JaxRsClientMethodBuilderSpec.groovy +++ b/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/builder/JaxRsClientMethodBuilderSpec.groovy @@ -632,7 +632,7 @@ class JaxRsClientMethodBuilderSpec extends Specification implements WireMockStub } | ['entity("\\"\\"", "text/plain")', 'header("Timer", "123")'] } - def "should generate a call with an url path and query parameters with #methodBuilderName"() { + def "should generate a call with an url path and query parameters with jaxrs"() { given: Contract contractDsl = Contract.make { request { @@ -646,6 +646,8 @@ class JaxRsClientMethodBuilderSpec extends Specification implements WireMockStub parameter 'search': $(consumer(notMatching(~/^\/[0-9]{2}$/)), producer("55")) parameter 'age': $(consumer(notMatching("^\\w*\$")), producer("99")) parameter 'name': $(consumer(matching("Denis.*")), producer("Denis.Stepanov")) + parameter 'nameWithDoubleQuote': '"quote"' + parameter 'nameWithQuote': "'quote'" parameter 'email': "bob@email.com" parameter 'hello': $(consumer(matching("Denis.*")), producer(absent())) parameter 'hello': absent() @@ -666,14 +668,16 @@ class JaxRsClientMethodBuilderSpec extends Specification implements WireMockStub when: String test = singleTestGenerator(contractDsl) then: - test.contains(modifyStringIfRequired.call('''queryParam("limit", "10"''')) - test.contains(modifyStringIfRequired.call('''queryParam("offset", "20"''')) - test.contains(modifyStringIfRequired.call('''queryParam("filter", "email"''')) - test.contains(modifyStringIfRequired.call('''queryParam("sort", "name"''')) - test.contains(modifyStringIfRequired.call('''queryParam("search", "55"''')) - test.contains(modifyStringIfRequired.call('''queryParam("age", "99"''')) - test.contains(modifyStringIfRequired.call('''queryParam("name", "Denis.Stepanov"''')) - test.contains(modifyStringIfRequired.call('''queryParam("email", "bob@email.com"''')) + test.contains('''queryParam("limit", "10"''') + test.contains('''queryParam("offset", "20"''') + test.contains('''queryParam("filter", "email"''') + test.contains('''queryParam("sort", "name"''') + test.contains('''queryParam("search", "55"''') + test.contains('''queryParam("age", "99"''') + test.contains('''queryParam("name", "Denis.Stepanov"''') + test.contains('''queryParam("nameWithDoubleQuote", "\\"quote\\""''') + test.contains('''queryParam("nameWithQuote", "'quote'"''') + test.contains('''queryParam("email", "bob@email.com"''') test.contains("""assertThatJson(parsedJson).field("['property1']").isEqualTo("a")""") test.contains("""assertThatJson(parsedJson).field("['property2']").isEqualTo("b")""") and: @@ -681,13 +685,69 @@ class JaxRsClientMethodBuilderSpec extends Specification implements WireMockStub and: SyntaxChecker.tryToCompile(methodBuilderName, test) where: - methodBuilderName | methodBuilder | modifyStringIfRequired - "jaxrs-spock" | { - properties.testFramework = TestFramework.SPOCK; properties.testMode = TestMode.JAXRSCLIENT - } | { String paramString -> paramString } + methodBuilderName | methodBuilder "jaxrs" | { properties.testFramework = TestFramework.JUNIT; properties.testMode = TestMode.JAXRSCLIENT - } | { String paramString -> paramString.replace("'", "\"") } + } + } + + def "should generate a call with an url path and query parameters with jaxrs-spock"() { + given: + Contract contractDsl = Contract.make { + request { + method 'GET' + urlPath('/users') { + queryParameters { + parameter 'limit': $(consumer(equalTo("20")), producer(equalTo("10"))) + parameter 'offset': $(consumer(containing("20")), producer(equalTo("20"))) + parameter 'filter': "email" + parameter 'sort': equalTo("name") + parameter 'search': $(consumer(notMatching(~/^\/[0-9]{2}$/)), producer("55")) + parameter 'age': $(consumer(notMatching("^\\w*\$")), producer("99")) + parameter 'name': $(consumer(matching("Denis.*")), producer("Denis.Stepanov")) + parameter 'nameWithDoubleQuote': '"quote"' + parameter 'nameWithQuote': "'quote'" + parameter 'email': "bob@email.com" + parameter 'hello': $(consumer(matching("Denis.*")), producer(absent())) + parameter 'hello': absent() + } + } + } + response { + status OK() + body """ + { + "property1": "a", + "property2": "b" + } + """ + } + } + methodBuilder() + when: + String test = singleTestGenerator(contractDsl) + then: + test.contains('''queryParam("limit", '10\'''') + test.contains('''queryParam("offset", '20\'''') + test.contains('''queryParam("filter", 'email\'''') + test.contains('''queryParam("sort", 'name\'''') + test.contains('''queryParam("search", '55\'''') + test.contains('''queryParam("age", '99\'''') + test.contains('''queryParam("name", 'Denis.Stepanov\'''') + test.contains("""queryParam("nameWithDoubleQuote", '''"quote"'''""") + test.contains("""queryParam("nameWithQuote", '''\\'quote\\''''""") + test.contains('''queryParam("email", 'bob@email.com\'''') + test.contains("""assertThatJson(parsedJson).field("['property1']").isEqualTo("a")""") + test.contains("""assertThatJson(parsedJson).field("['property2']").isEqualTo("b")""") + and: + stubMappingIsValidWireMockStub(contractDsl) + and: + SyntaxChecker.tryToCompile(methodBuilderName, test) + where: + methodBuilderName | methodBuilder + "jaxrs-spock" | { + properties.testFramework = TestFramework.SPOCK; properties.testMode = TestMode.JAXRSCLIENT + } } @Issue('#169') @@ -725,14 +785,14 @@ class JaxRsClientMethodBuilderSpec extends Specification implements WireMockStub when: String test = singleTestGenerator(contractDsl) then: - test.contains(modifyStringIfRequired.call('''queryParam("limit", "10"''')) - test.contains(modifyStringIfRequired.call('''queryParam("offset", "20"''')) - test.contains(modifyStringIfRequired.call('''queryParam("filter", "email"''')) - test.contains(modifyStringIfRequired.call('''queryParam("sort", "name"''')) - test.contains(modifyStringIfRequired.call('''queryParam("search", "55"''')) - test.contains(modifyStringIfRequired.call('''queryParam("age", "99"''')) - test.contains(modifyStringIfRequired.call('''queryParam("name", "Denis.Stepanov"''')) - test.contains(modifyStringIfRequired.call('''queryParam("email", "bob@email.com"''')) + test.contains(modifyStringIfRequired.call('''queryParam("limit", '10\'''')) + test.contains(modifyStringIfRequired.call('''queryParam("offset", '20\'''')) + test.contains(modifyStringIfRequired.call('''queryParam("filter", 'email\'''')) + test.contains(modifyStringIfRequired.call('''queryParam("sort", 'name\'''')) + test.contains(modifyStringIfRequired.call('''queryParam("search", '55\'''')) + test.contains(modifyStringIfRequired.call('''queryParam("age", '99\'''')) + test.contains(modifyStringIfRequired.call('''queryParam("name", 'Denis.Stepanov\'''')) + test.contains(modifyStringIfRequired.call('''queryParam("email", 'bob@email.com\'''')) test.contains("""assertThatJson(parsedJson).field("['property1']").isEqualTo("a")""") test.contains("""assertThatJson(parsedJson).field("['property2']").isEqualTo("b")""") and: diff --git a/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/builder/MethodBodyBuilderSpec.groovy b/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/builder/MethodBodyBuilderSpec.groovy index 9be94f1cdd..2aa3737748 100644 --- a/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/builder/MethodBodyBuilderSpec.groovy +++ b/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/builder/MethodBodyBuilderSpec.groovy @@ -1814,6 +1814,49 @@ response: } } + @Issue("#1441") + def "should work with query parameters that need to be escaped [#methodBuilderName]"() { + given: + Contract contractDsl = Contract.make { + request { + method 'POST' + urlPath("/rest/something") { + queryParameters { + parameter 'quote': equalTo("\"") + } + } + } + response { + status OK() + } + } + methodBuilder() + when: + String test = singleTestGenerator(contractDsl) + then: + SyntaxChecker.tryToCompileWithoutCompileStatic(methodBuilderName, test) + !test.contains('''.queryParam("quote",""")''') + and: + stubMappingIsValidWireMockStub(contractDsl) + where: + methodBuilderName | methodBuilder + "spock" | { + properties.testFramework = TestFramework.SPOCK + } + "mockmvc" | { + properties.testMode = TestMode.MOCKMVC + } + "jaxrs-spock" | { + properties.testFramework = TestFramework.SPOCK; properties.testMode = TestMode.JAXRSCLIENT + } + "jaxrs" | { + properties.testFramework = TestFramework.JUNIT; properties.testMode = TestMode.JAXRSCLIENT + } + "testNG" | { + properties.testFramework = TestFramework.TESTNG + } + } + @Issue("#1262") def "should work with the timeout flag for groovy [#methodBuilderName]"() { given: From 0f9b1aec078c3018d2a8644942a193347def2819 Mon Sep 17 00:00:00 2001 From: Marcin Grzejszczak Date: Tue, 9 Mar 2021 10:13:49 +0100 Subject: [PATCH 2/2] Fixed the schema link; fixes gh-1593 --- README.adoc | 34 +++++++++- docs/src/main/asciidoc/_configprops.adoc | 62 +++++++++---------- .../asciidoc/_project-features-contract.adoc | 2 +- 3 files changed, 65 insertions(+), 33 deletions(-) diff --git a/README.adoc b/README.adoc index 84644f2332..c236be76fc 100644 --- a/README.adoc +++ b/README.adoc @@ -19,7 +19,6 @@ If you prefer to learn about the project by doing some tutorials, you can check workshops under https://cloud-samples.spring.io/spring-cloud-contract-samples/workshops.html[this link]. - == Project page You can read more about Spring Cloud Contract by going to https://spring.io/projects/spring-cloud-contract[the project page] @@ -208,6 +207,39 @@ IMPORTANT: Remember to set the `Scan Scope` to `All sources` since we apply chec == How to Build Spring Cloud Contract +=== Cloning the repository on Windows + +While cloning this project on Windows, some files in the git repository may exceed the Windows maximum file path limit of 255 characters, which may +result in an incorrectly (probably partially) checked out repository. + +To resolve this issue, you can set the `core.longPaths` attribute to `true` or clone the Spring Cloud Contract repository. + +To set the `core.longPaths` attribute to `true`, you have three options: + +- Change it for all users of the machine (doing so requires administrator privileges): + + +[source,bash] +---- +git config --system core.longPaths true +git clone https://github.com/spring-cloud/spring-cloud-contract.git +---- + +- Change it for the current user (no administrative privileges required): + +[source,bash] +---- +git config --global core.longPaths true +git clone https://github.com/spring-cloud/spring-cloud-contract.git +---- + +- Change for just this repository (administrative privileges depend on where the repository is being cloned to): + +[source,bash] +---- +git clone -c core.longPaths true https://github.com/spring-cloud/spring-cloud-contract.git +---- + IMPORTANT: You need to have all the necessary Groovy plugins installed for your IDE to properly resolve the sources. For example, in Intellij IDEA, having both the Eclipse Groovy Compiler Plugin and the GMavenPlus Intellij diff --git a/docs/src/main/asciidoc/_configprops.adoc b/docs/src/main/asciidoc/_configprops.adoc index 96f6b92343..be24854e0a 100644 --- a/docs/src/main/asciidoc/_configprops.adoc +++ b/docs/src/main/asciidoc/_configprops.adoc @@ -1,46 +1,46 @@ |=== |Name | Default | Description -|stubrunner.amqp.enabled | false | Whether to enable support for Stub Runner and AMQP. -|stubrunner.amqp.mockCOnnection | true | Whether to enable support for Stub Runner and AMQP mocked connection factory. -|stubrunner.classifier | stubs | The classifier to use by default in ivy co-ordinates for a stub. -|stubrunner.cloud.consul.enabled | true | Whether to enable stubs registration in Consul. -|stubrunner.cloud.delegate.enabled | true | Whether to enable DiscoveryClient's Stub Runner implementation. -|stubrunner.cloud.enabled | true | Whether to enable Spring Cloud support for Stub Runner. -|stubrunner.cloud.eureka.enabled | true | Whether to enable stubs registration in Eureka. -|stubrunner.cloud.loadbalancer.enabled | true | Whether to enable Stub Runner's Spring Cloud Load Balancer integration. -|stubrunner.cloud.stubbed.discovery.enabled | true | Whether Service Discovery should be stubbed for Stub Runner. If set to false, stubs will get registered in real service discovery. -|stubrunner.cloud.zookeeper.enabled | true | Whether to enable stubs registration in Zookeeper. +|stubrunner.amqp.enabled | `false` | Whether to enable support for Stub Runner and AMQP. +|stubrunner.amqp.mockCOnnection | `true` | Whether to enable support for Stub Runner and AMQP mocked connection factory. +|stubrunner.classifier | `stubs` | The classifier to use by default in ivy co-ordinates for a stub. +|stubrunner.cloud.consul.enabled | `true` | Whether to enable stubs registration in Consul. +|stubrunner.cloud.delegate.enabled | `true` | Whether to enable DiscoveryClient's Stub Runner implementation. +|stubrunner.cloud.enabled | `true` | Whether to enable Spring Cloud support for Stub Runner. +|stubrunner.cloud.eureka.enabled | `true` | Whether to enable stubs registration in Eureka. +|stubrunner.cloud.loadbalancer.enabled | `true` | Whether to enable Stub Runner's Spring Cloud Load Balancer integration. +|stubrunner.cloud.stubbed.discovery.enabled | `true` | Whether Service Discovery should be stubbed for Stub Runner. If set to false, stubs will get registered in real service discovery. +|stubrunner.cloud.zookeeper.enabled | `true` | Whether to enable stubs registration in Zookeeper. |stubrunner.consumer-name | | You can override the default {@code spring.application.name} of this field by setting a value to this parameter. -|stubrunner.delete-stubs-after-test | true | If set to {@code false} will NOT delete stubs from a temporary folder after running tests. -|stubrunner.fail-on-no-stubs | true | When enabled, this flag will tell stub runner to throw an exception when no stubs / contracts were found. -|stubrunner.generate-stubs | false | When enabled, this flag will tell stub runner to not load the generated stubs, but convert the found contracts at runtime to a stub format and run those stubs. +|stubrunner.delete-stubs-after-test | `true` | If set to {@code false} will NOT delete stubs from a temporary folder after running tests. +|stubrunner.fail-on-no-stubs | `true` | When enabled, this flag will tell stub runner to throw an exception when no stubs / contracts were found. +|stubrunner.generate-stubs | `false` | When enabled, this flag will tell stub runner to not load the generated stubs, but convert the found contracts at runtime to a stub format and run those stubs. |stubrunner.http-server-stub-configurer | | Configuration for an HTTP server stub. -|stubrunner.ids | [] | The ids of the stubs to run in "ivy" notation ([groupId]:artifactId:[version]:[classifier][:port]). {@code groupId}, {@code classifier}, {@code version} and {@code port} can be optional. +|stubrunner.ids | `[]` | The ids of the stubs to run in "ivy" notation ([groupId]:artifactId:[version]:[classifier][:port]). {@code groupId}, {@code classifier}, {@code version} and {@code port} can be optional. |stubrunner.ids-to-service-ids | | Mapping of Ivy notation based ids to serviceIds inside your application. Example "a:b" -> "myService" "artifactId" -> "myOtherService" -|stubrunner.integration.enabled | true | Whether to enable Stub Runner integration with Spring Integration. -|stubrunner.jms.enabled | true | Whether to enable Stub Runner integration with Spring JMS. -|stubrunner.kafka.enabled | true | Whether to enable Stub Runner integration with Spring Kafka. -|stubrunner.kafka.initializer.enabled | true | Whether to allow Stub Runner to take care of polling for messages instead of the KafkaStubMessages component. The latter should be used only on the producer side. +|stubrunner.integration.enabled | `true` | Whether to enable Stub Runner integration with Spring Integration. +|stubrunner.jms.enabled | `true` | Whether to enable Stub Runner integration with Spring JMS. +|stubrunner.kafka.enabled | `true` | Whether to enable Stub Runner integration with Spring Kafka. +|stubrunner.kafka.initializer.enabled | `true` | Whether to allow Stub Runner to take care of polling for messages instead of the KafkaStubMessages component. The latter should be used only on the producer side. |stubrunner.mappings-output-folder | | Dumps the mappings of each HTTP server to the selected folder. -|stubrunner.max-port | 15000 | Max value of a port for the automatically started WireMock server. -|stubrunner.min-port | 10000 | Min value of a port for the automatically started WireMock server. +|stubrunner.max-port | `15000` | Max value of a port for the automatically started WireMock server. +|stubrunner.min-port | `10000` | Min value of a port for the automatically started WireMock server. |stubrunner.password | | Repository password. |stubrunner.properties | | Map of properties that can be passed to custom {@link org.springframework.cloud.contract.stubrunner.StubDownloaderBuilder}. |stubrunner.proxy-host | | Repository proxy host. |stubrunner.proxy-port | | Repository proxy port. -|stubrunner.stream.enabled | true | Whether to enable Stub Runner integration with Spring Cloud Stream. +|stubrunner.stream.enabled | `true` | Whether to enable Stub Runner integration with Spring Cloud Stream. |stubrunner.stubs-mode | | Pick where the stubs should come from. -|stubrunner.stubs-per-consumer | false | Should only stubs for this particular consumer get registered in HTTP server stub. +|stubrunner.stubs-per-consumer | `false` | Should only stubs for this particular consumer get registered in HTTP server stub. |stubrunner.username | | Repository username. -|wiremock.placeholders.enabled | true | Flag to indicate that http URLs in generated wiremock stubs should be filtered to add or resolve a placeholder for a dynamic port. -|wiremock.reset-mappings-after-each-test | false | -|wiremock.rest-template-ssl-enabled | false | -|wiremock.server.files | [] | -|wiremock.server.https-port | -1 | -|wiremock.server.https-port-dynamic | false | -|wiremock.server.port | 8080 | -|wiremock.server.port-dynamic | false | -|wiremock.server.stubs | [] | +|wiremock.placeholders.enabled | `true` | Flag to indicate that http URLs in generated wiremock stubs should be filtered to add or resolve a placeholder for a dynamic port. +|wiremock.reset-mappings-after-each-test | `false` | +|wiremock.rest-template-ssl-enabled | `false` | +|wiremock.server.files | `[]` | +|wiremock.server.https-port | `-1` | +|wiremock.server.https-port-dynamic | `false` | +|wiremock.server.port | `8080` | +|wiremock.server.port-dynamic | `false` | +|wiremock.server.stubs | `[]` | |=== \ No newline at end of file diff --git a/docs/src/main/asciidoc/_project-features-contract.adoc b/docs/src/main/asciidoc/_project-features-contract.adoc index 85daf731d5..ca787ffb00 100644 --- a/docs/src/main/asciidoc/_project-features-contract.adoc +++ b/docs/src/main/asciidoc/_project-features-contract.adoc @@ -167,7 +167,7 @@ You can also provide an import to the `contract` function (`import org.springfra [[contract-yml]] === Contract DSL in YML -In order to see a schema of a YAML contract, you can check out the link:yml-schema.html[YML Schema] page. +In order to see a schema of a YAML contract, you can check out the {docs-url}/reference/html/yml-schema.html[YML Schema] page. [[contract-limitations]] === Limitations