From bb0d920b2b1ca413c1c77d6af61c1f1e53bde833 Mon Sep 17 00:00:00 2001 From: goutamadwant Date: Thu, 18 Jun 2026 22:59:27 -0700 Subject: [PATCH] Include REST Docs request cookies in generated stubs Fixes gh-2410 Signed-off-by: goutamadwant --- .../wiremock/restdocs/ContractDslSnippet.java | 2 + .../wiremock/restdocs/WireMockSnippet.java | 10 ++- .../default-dsl-contract-only.snippet | 7 ++ ...ServerRestDocsMatcherApplicationTests.java | 33 +++++++ .../restdocs/WireMockSnippetTests.java | 85 ++++++++++++++++++- 5 files changed, 134 insertions(+), 3 deletions(-) diff --git a/spring-cloud-contract-wiremock/src/main/java/org/springframework/cloud/contract/wiremock/restdocs/ContractDslSnippet.java b/spring-cloud-contract-wiremock/src/main/java/org/springframework/cloud/contract/wiremock/restdocs/ContractDslSnippet.java index fb414eac6a..dd7982003c 100644 --- a/spring-cloud-contract-wiremock/src/main/java/org/springframework/cloud/contract/wiremock/restdocs/ContractDslSnippet.java +++ b/spring-cloud-contract-wiremock/src/main/java/org/springframework/cloud/contract/wiremock/restdocs/ContractDslSnippet.java @@ -134,6 +134,8 @@ private void insertRequestModel(Operation operation, Map model) filterHeaders(headers); model.put("request_headers_present", !headers.isEmpty()); model.put("request_headers", headers.entrySet()); + model.put("request_cookies_present", !request.getCookies().isEmpty()); + model.put("request_cookies", request.getCookies()); @SuppressWarnings("unchecked") Set jsonPaths = (Set) operation.getAttributes().get("contract.jsonPaths"); model.put("request_json_paths_present", jsonPaths != null && !jsonPaths.isEmpty()); diff --git a/spring-cloud-contract-wiremock/src/main/java/org/springframework/cloud/contract/wiremock/restdocs/WireMockSnippet.java b/spring-cloud-contract-wiremock/src/main/java/org/springframework/cloud/contract/wiremock/restdocs/WireMockSnippet.java index 49fccfdbae..2775911e35 100644 --- a/spring-cloud-contract-wiremock/src/main/java/org/springframework/cloud/contract/wiremock/restdocs/WireMockSnippet.java +++ b/spring-cloud-contract-wiremock/src/main/java/org/springframework/cloud/contract/wiremock/restdocs/WireMockSnippet.java @@ -38,6 +38,7 @@ import org.springframework.http.MediaType; import org.springframework.restdocs.RestDocumentationContext; import org.springframework.restdocs.operation.Operation; +import org.springframework.restdocs.operation.RequestCookie; import org.springframework.restdocs.snippet.RestDocumentationContextPlaceholderResolverFactory; import org.springframework.restdocs.snippet.Snippet; import org.springframework.restdocs.snippet.StandardWriterResolver; @@ -150,7 +151,7 @@ private ResponseDefinitionBuilder response(Operation operation) { } private MappingBuilder request(Operation operation) { - return queryParams(requestHeaders(requestBuilder(operation), operation), operation); + return queryParams(requestCookies(requestHeaders(requestBuilder(operation), operation), operation), operation); } private MappingBuilder queryParams(MappingBuilder request, Operation operation) { @@ -166,6 +167,13 @@ private MappingBuilder queryParams(MappingBuilder request, Operation operation) return request; } + private MappingBuilder requestCookies(MappingBuilder request, Operation operation) { + for (RequestCookie cookie : operation.getRequest().getCookies()) { + request = request.withCookie(cookie.getName(), equalTo(cookie.getValue())); + } + return request; + } + private MappingBuilder requestHeaders(MappingBuilder request, Operation operation) { org.springframework.http.HttpHeaders headers = operation.getRequest().getHeaders(); // TODO: whitelist headers diff --git a/spring-cloud-contract-wiremock/src/main/resources/org/springframework/restdocs/templates/default-dsl-contract-only.snippet b/spring-cloud-contract-wiremock/src/main/resources/org/springframework/restdocs/templates/default-dsl-contract-only.snippet index e5cada93ae..738e88567c 100644 --- a/spring-cloud-contract-wiremock/src/main/resources/org/springframework/restdocs/templates/default-dsl-contract-only.snippet +++ b/spring-cloud-contract-wiremock/src/main/resources/org/springframework/restdocs/templates/default-dsl-contract-only.snippet @@ -20,6 +20,13 @@ Contract.make { {{/request_headers}} } {{/request_headers_present}} + {{#request_cookies_present}} + cookies { + {{#request_cookies}} + cookie('''{{name}}''', '''{{value}}''') + {{/request_cookies}} + } + {{/request_cookies_present}} {{#request_json_paths_present}} bodyMatchers { {{#request_json_paths}} diff --git a/spring-cloud-contract-wiremock/src/test/java/org/springframework/cloud/contract/wiremock/WiremockServerRestDocsMatcherApplicationTests.java b/spring-cloud-contract-wiremock/src/test/java/org/springframework/cloud/contract/wiremock/WiremockServerRestDocsMatcherApplicationTests.java index 3bc1f0ad45..7ef862426f 100644 --- a/spring-cloud-contract-wiremock/src/test/java/org/springframework/cloud/contract/wiremock/WiremockServerRestDocsMatcherApplicationTests.java +++ b/spring-cloud-contract-wiremock/src/test/java/org/springframework/cloud/contract/wiremock/WiremockServerRestDocsMatcherApplicationTests.java @@ -17,8 +17,12 @@ package org.springframework.cloud.contract.wiremock; import java.io.File; +import java.nio.file.Files; import com.github.tomakehurst.wiremock.client.WireMock; +import com.github.tomakehurst.wiremock.stubbing.StubMapping; +import jakarta.servlet.http.Cookie; +import org.assertj.core.api.Assertions; import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; @@ -29,6 +33,7 @@ import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.webmvc.test.autoconfigure.AutoConfigureMockMvc; import org.springframework.cloud.contract.wiremock.WiremockServerRestDocsMatcherApplicationTests.TestConfiguration; +import org.springframework.cloud.contract.wiremock.restdocs.SpringCloudContractRestDocs; import org.springframework.cloud.contract.wiremock.restdocs.WireMockRestDocs; import org.springframework.context.annotation.Configuration; import org.springframework.http.MediaType; @@ -52,6 +57,9 @@ @AutoConfigureMockMvc public class WiremockServerRestDocsMatcherApplicationTests { + /** + * Expected exception rule for WireMock mismatch assertions. + */ @Rule public ExpectedException expected = ExpectedException.none(); @@ -71,6 +79,25 @@ public void matchesRequest() throws Exception { assertThat(new File("target/snippets/stubs/posted.json")).exists(); } + @Test + public void stubsRenderRequestCookies() throws Exception { + File stub = new File("target/snippets/stubs/cookie.json"); + File contract = new File("target/snippets/contracts/cookie.groovy"); + FileSystemUtils.deleteRecursively(stub); + FileSystemUtils.deleteRecursively(contract); + this.mockMvc.perform(MockMvcRequestBuilders.get("/cookie").cookie(new Cookie("test_user", "free"))) + .andExpect(MockMvcResultMatchers.content().string("Hello Cookie")) + .andDo(WireMockRestDocs.verify()) + .andDo(document("cookie", SpringCloudContractRestDocs.dslContract())); + + assertThat(stub).exists(); + StubMapping stubMapping = WireMockStubMapping.buildFrom(Files.readString(stub.toPath())); + assertThat(stubMapping.getRequest().getCookies()) + .containsOnly(Assertions.entry("test_user", WireMock.equalTo("free"))); + assertThat(contract).exists(); + assertThat(Files.readString(contract.toPath())).contains("cookie('''test_user''', '''free''')"); + } + @Test public void doesNotMatch() throws Exception { this.expected.expect(AssertionError.class); @@ -94,6 +121,12 @@ public String resource(@RequestBody String body) { return "Hello World"; } + @ResponseBody + @RequestMapping(value = "/cookie", method = RequestMethod.GET) + public String cookie() { + return "Hello Cookie"; + } + } } diff --git a/spring-cloud-contract-wiremock/src/test/java/org/springframework/cloud/contract/wiremock/restdocs/WireMockSnippetTests.java b/spring-cloud-contract-wiremock/src/test/java/org/springframework/cloud/contract/wiremock/restdocs/WireMockSnippetTests.java index aac265a9ee..89d676a430 100644 --- a/spring-cloud-contract-wiremock/src/test/java/org/springframework/cloud/contract/wiremock/restdocs/WireMockSnippetTests.java +++ b/spring-cloud-contract-wiremock/src/test/java/org/springframework/cloud/contract/wiremock/restdocs/WireMockSnippetTests.java @@ -49,6 +49,13 @@ import org.springframework.restdocs.operation.OperationResponse; import org.springframework.restdocs.operation.RequestCookie; import org.springframework.restdocs.operation.ResponseCookie; +import org.springframework.restdocs.snippet.RestDocumentationContextPlaceholderResolverFactory; +import org.springframework.restdocs.snippet.StandardWriterResolver; +import org.springframework.restdocs.snippet.WriterResolver; +import org.springframework.restdocs.templates.StandardTemplateResourceResolver; +import org.springframework.restdocs.templates.TemplateEngine; +import org.springframework.restdocs.templates.TemplateFormats; +import org.springframework.restdocs.templates.mustache.MustacheTemplateEngine; import static com.github.tomakehurst.wiremock.client.WireMock.equalTo; import static org.assertj.core.api.Assertions.assertThat; @@ -58,12 +65,15 @@ */ public class WireMockSnippetTests { + /** + * Temporary output folder for generated snippets. + */ @Rule public TemporaryFolder tmp = new TemporaryFolder(); - Operation operation; + private Operation operation; - RestDocumentationContext context; + private RestDocumentationContext context; private File outputFolder; @@ -172,6 +182,38 @@ public void should_accept_query_params() throws IOException { .containsOnly(Assertions.entry("myParam", MultiValuePattern.of(equalTo(("myValue"))))); } + @Test + public void should_accept_request_cookies() throws IOException { + this.operation = operation(requestGetWithCookie(), response(), this.context); + WireMockSnippet snippet = new WireMockSnippet(); + + snippet.document(this.operation); + + File stub = new File(this.outputFolder, "stubs/foo.json"); + assertThat(stub).exists(); + StubMapping stubMapping = WireMockStubMapping.buildFrom(new String(Files.readAllBytes(stub.toPath()))); + assertThat(stubMapping.getRequest().getCookies()).containsOnly(Assertions.entry("test_user", equalTo("free"))); + } + + @Test + public void should_include_request_cookies_in_contract() throws IOException { + this.operation = operation(requestGetWithCookie(), response(), this.context); + this.operation.getAttributes() + .put(TemplateEngine.class.getName(), + new MustacheTemplateEngine(new StandardTemplateResourceResolver(TemplateFormats.asciidoctor()))); + this.operation.getAttributes() + .put(WriterResolver.class.getName(), new StandardWriterResolver( + new RestDocumentationContextPlaceholderResolverFactory(), "UTF-8", TemplateFormats.asciidoctor())); + ContractDslSnippet snippet = new ContractDslSnippet(); + + snippet.document(this.operation); + + File contract = new File(this.outputFolder, "contracts/foo.groovy"); + assertThat(contract).exists(); + String contractText = new String(Files.readAllBytes(contract.toPath())); + assertThat(contractText).contains("cookies {").contains("cookie('''test_user''', '''free''')"); + } + private Operation operation(OperationRequest request, OperationResponse response, RestDocumentationContext context) { return operation("foo", request, response, context); @@ -486,4 +528,43 @@ public Collection getCookies() { }; } + private OperationRequest requestGetWithCookie() { + return new OperationRequest() { + @Override + public byte[] getContent() { + return new byte[0]; + } + + @Override + public String getContentAsString() { + return ""; + } + + @Override + public HttpHeaders getHeaders() { + return new HttpHeaders(); + } + + @Override + public HttpMethod getMethod() { + return HttpMethod.GET; + } + + @Override + public Collection getParts() { + return null; + } + + @Override + public URI getUri() { + return URI.create("https://foo/bar"); + } + + @Override + public Collection getCookies() { + return Collections.singleton(new RequestCookie("test_user", "free")); + } + }; + } + }