From da7efa3e3a7362101a50912ed0b9c25e21532444 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 28 Apr 2015 16:26:01 +0100 Subject: [PATCH] Improve testing of the generated documentation snippets --- .../restdocs/RestDocumentationException.java | 37 +++ .../restdocs/http/HttpDocumentation.java | 5 +- .../hypermedia/LinkSnippetResultHandler.java | 8 +- .../restdocs/payload/FieldProcessor.java | 1 - .../restdocs/payload/FieldValidator.java | 17 +- .../restdocs/curl/CurlDocumentationTests.java | 214 +++++++----------- .../restdocs/http/HttpDocumentationTests.java | 146 +++++------- .../HypermediaDocumentationTests.java | 117 ++++++++++ .../restdocs/payload/FieldValidatorTests.java | 15 +- .../payload/PayloadDocumentationTests.java | 157 ++++++------- .../restdocs/test/ExpectedSnippet.java | 193 ++++++++++++++++ .../restdocs/test/SnippetMatchers.java | 177 +++++++++++++++ .../restdocs/{ => test}/StubMvcResult.java | 38 +++- 13 files changed, 780 insertions(+), 345 deletions(-) create mode 100644 spring-restdocs/src/main/java/org/springframework/restdocs/RestDocumentationException.java create mode 100644 spring-restdocs/src/test/java/org/springframework/restdocs/hypermedia/HypermediaDocumentationTests.java create mode 100644 spring-restdocs/src/test/java/org/springframework/restdocs/test/ExpectedSnippet.java create mode 100644 spring-restdocs/src/test/java/org/springframework/restdocs/test/SnippetMatchers.java rename spring-restdocs/src/test/java/org/springframework/restdocs/{ => test}/StubMvcResult.java (63%) diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/RestDocumentationException.java b/spring-restdocs/src/main/java/org/springframework/restdocs/RestDocumentationException.java new file mode 100644 index 00000000..f63c1f22 --- /dev/null +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/RestDocumentationException.java @@ -0,0 +1,37 @@ +/* + * Copyright 2014-2015 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.restdocs; + +/** + * A {@link RuntimeException} thrown to indicate a problem with a RESTful resource's + * documentation. + * + * @author Andy Wilkinson + */ +@SuppressWarnings("serial") +public class RestDocumentationException extends RuntimeException { + + /** + * Creates a new {@code RestDocumentationException} described by the given + * {@code message} + * @param message the message that describes the documentation problem + */ + public RestDocumentationException(String message) { + super(message); + } + +} diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/http/HttpDocumentation.java b/spring-restdocs/src/main/java/org/springframework/restdocs/http/HttpDocumentation.java index d840f925..6726da47 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/http/HttpDocumentation.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/http/HttpDocumentation.java @@ -148,7 +148,10 @@ public abstract class HttpDocumentation { } } this.writer.println(); - this.writer.println(this.result.getResponse().getContentAsString()); + String content = this.result.getResponse().getContentAsString(); + if (StringUtils.hasText(content)) { + this.writer.println(content); + } } } diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/hypermedia/LinkSnippetResultHandler.java b/spring-restdocs/src/main/java/org/springframework/restdocs/hypermedia/LinkSnippetResultHandler.java index d2c1f2c0..0940ccfb 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/hypermedia/LinkSnippetResultHandler.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/hypermedia/LinkSnippetResultHandler.java @@ -16,8 +16,6 @@ package org.springframework.restdocs.hypermedia; -import static org.junit.Assert.fail; - import java.io.IOException; import java.util.HashMap; import java.util.HashSet; @@ -26,6 +24,7 @@ import java.util.Map; import java.util.Map.Entry; import java.util.Set; +import org.springframework.restdocs.RestDocumentationException; import org.springframework.restdocs.snippet.DocumentationWriter; import org.springframework.restdocs.snippet.DocumentationWriter.TableAction; import org.springframework.restdocs.snippet.DocumentationWriter.TableWriter; @@ -94,10 +93,13 @@ public class LinkSnippetResultHandler extends SnippetWritingResultHandler { + undocumentedRels; } if (!missingRels.isEmpty()) { + if (message.length() > 0) { + message += ". "; + } message += "Links with the following relations were not found in the response: " + missingRels; } - fail(message); + throw new RestDocumentationException(message); } Assert.isTrue(actualRels.equals(expectedRels)); diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/payload/FieldProcessor.java b/spring-restdocs/src/main/java/org/springframework/restdocs/payload/FieldProcessor.java index 70a4f1d0..eb9e0ca8 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/payload/FieldProcessor.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/payload/FieldProcessor.java @@ -121,7 +121,6 @@ class FieldProcessor { .getParentMatch()))) { return false; } - ; } return true; } diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/payload/FieldValidator.java b/spring-restdocs/src/main/java/org/springframework/restdocs/payload/FieldValidator.java index 07583049..e8de809f 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/payload/FieldValidator.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/payload/FieldValidator.java @@ -22,6 +22,8 @@ import java.util.ArrayList; import java.util.List; import java.util.Map; +import org.springframework.restdocs.RestDocumentationException; + import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.SerializationFeature; @@ -51,14 +53,17 @@ class FieldValidator { String message = ""; if (!undocumentedPayload.isEmpty()) { message += String.format( - "Portions of the payload were not documented:%n%s", + "The following parts of the payload were not documented:%n%s", this.objectMapper.writeValueAsString(undocumentedPayload)); } if (!missingFields.isEmpty()) { + if (message.length() > 0) { + message += String.format("%n"); + } message += "Fields with the following paths were not found in the payload: " + missingFields; } - throw new FieldValidationException(message); + throw new RestDocumentationException(message); } } @@ -86,12 +91,4 @@ class FieldValidator { return payload; } - @SuppressWarnings("serial") - static class FieldValidationException extends RuntimeException { - - FieldValidationException(String message) { - super(message); - } - } - } diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/curl/CurlDocumentationTests.java b/spring-restdocs/src/test/java/org/springframework/restdocs/curl/CurlDocumentationTests.java index 296408c5..e984e830 100644 --- a/spring-restdocs/src/test/java/org/springframework/restdocs/curl/CurlDocumentationTests.java +++ b/spring-restdocs/src/test/java/org/springframework/restdocs/curl/CurlDocumentationTests.java @@ -16,23 +16,19 @@ package org.springframework.restdocs.curl; -import static org.hamcrest.CoreMatchers.hasItem; -import static org.junit.Assert.assertThat; import static org.springframework.restdocs.curl.CurlDocumentation.documentCurlRequest; +import static org.springframework.restdocs.test.SnippetMatchers.codeBlock; +import static org.springframework.restdocs.test.StubMvcResult.result; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; -import java.io.BufferedReader; -import java.io.File; -import java.io.FileReader; import java.io.IOException; -import java.util.ArrayList; -import java.util.List; -import org.junit.After; -import org.junit.Before; +import org.junit.Rule; import org.junit.Test; import org.springframework.http.MediaType; import org.springframework.mock.web.MockHttpServletRequest; -import org.springframework.restdocs.StubMvcResult; +import org.springframework.restdocs.test.ExpectedSnippet; /** * Tests for {@link CurlDocumentation} @@ -43,224 +39,174 @@ import org.springframework.restdocs.StubMvcResult; */ public class CurlDocumentationTests { - private final File outputDir = new File("build/curl-documentation-tests"); - - @Before - public void setup() { - System.setProperty("org.springframework.restdocs.outputDir", - this.outputDir.getAbsolutePath()); - } - - @After - public void cleanup() { - System.clearProperty("org.springframework.restdocs.outputDir"); - } + @Rule + public ExpectedSnippet snippet = new ExpectedSnippet(); @Test public void getRequest() throws IOException { - documentCurlRequest("get-request").handle( - new StubMvcResult(new MockHttpServletRequest("GET", "/foo"), null)); - assertThat(requestSnippetLines("get-request"), - hasItem("$ curl 'http://localhost/foo' -i")); + this.snippet.expectCurlRequest("get-request").withContents( + codeBlock("bash").content("$ curl 'http://localhost/foo' -i")); + documentCurlRequest("get-request").handle(result(get("/foo"))); } @Test public void nonGetRequest() throws IOException { - documentCurlRequest("non-get-request").handle( - new StubMvcResult(new MockHttpServletRequest("POST", "/foo"), null)); - assertThat(requestSnippetLines("non-get-request"), - hasItem("$ curl 'http://localhost/foo' -i -X POST")); + this.snippet.expectCurlRequest("non-get-request").withContents( + codeBlock("bash").content("$ curl 'http://localhost/foo' -i -X POST")); + documentCurlRequest("non-get-request").handle(result(post("/foo"))); } @Test public void requestWithContent() throws IOException { - MockHttpServletRequest request = new MockHttpServletRequest("GET", "/foo"); - request.setContent("content".getBytes()); + this.snippet.expectCurlRequest("request-with-content").withContents( + codeBlock("bash") + .content("$ curl 'http://localhost/foo' -i -d 'content'")); documentCurlRequest("request-with-content").handle( - new StubMvcResult(request, null)); - assertThat(requestSnippetLines("request-with-content"), - hasItem("$ curl 'http://localhost/foo' -i -d 'content'")); - } - - @Test - public void requestWitUriQueryString() throws IOException { - documentCurlRequest("request-with-uri-query-string").handle( - new StubMvcResult(new MockHttpServletRequest("GET", "/foo?param=value"), - null)); - assertThat(requestSnippetLines("request-with-uri-query-string"), - hasItem("$ curl 'http://localhost/foo?param=value' -i")); + result(get("/foo").content("content"))); } @Test public void requestWithQueryString() throws IOException { - MockHttpServletRequest request = new MockHttpServletRequest("GET", "/foo"); - request.setQueryString("param=value"); + this.snippet.expectCurlRequest("request-with-query-string") + .withContents( + codeBlock("bash").content( + "$ curl 'http://localhost/foo?param=value' -i")); documentCurlRequest("request-with-query-string").handle( - new StubMvcResult(request, null)); - assertThat(requestSnippetLines("request-with-query-string"), - hasItem("$ curl 'http://localhost/foo?param=value' -i")); + result(get("/foo?param=value"))); } @Test public void requestWithOneParameter() throws IOException { - MockHttpServletRequest request = new MockHttpServletRequest("GET", "/foo"); - request.addParameter("k1", "v1"); + this.snippet.expectCurlRequest("request-with-one-parameter").withContents( + codeBlock("bash").content("$ curl 'http://localhost/foo?k1=v1' -i")); documentCurlRequest("request-with-one-parameter").handle( - new StubMvcResult(request, null)); - assertThat(requestSnippetLines("request-with-one-parameter"), - hasItem("$ curl 'http://localhost/foo?k1=v1' -i")); + result(get("/foo").param("k1", "v1"))); } @Test public void requestWithMultipleParameters() throws IOException { - MockHttpServletRequest request = new MockHttpServletRequest("GET", "/foo"); - request.addParameter("k1", "v1"); - request.addParameter("k2", "v2"); - request.addParameter("k1", "v1-bis"); + this.snippet.expectCurlRequest("request-with-multiple-parameters").withContents( + codeBlock("bash").content( + "$ curl 'http://localhost/foo?k1=v1&k1=v1-bis&k2=v2' -i")); documentCurlRequest("request-with-multiple-parameters").handle( - new StubMvcResult(request, null)); - assertThat(requestSnippetLines("request-with-multiple-parameters"), - hasItem("$ curl 'http://localhost/foo?k1=v1&k1=v1-bis&k2=v2' -i")); + result(get("/foo").param("k1", "v1").param("k2", "v2") + .param("k1", "v1-bis"))); } @Test public void requestWithUrlEncodedParameter() throws IOException { - MockHttpServletRequest request = new MockHttpServletRequest("GET", "/foo"); - request.addParameter("k1", "foo bar&"); + this.snippet.expectCurlRequest("request-with-url-encoded-parameter") + .withContents( + codeBlock("bash").content( + "$ curl 'http://localhost/foo?k1=foo+bar%26' -i")); documentCurlRequest("request-with-url-encoded-parameter").handle( - new StubMvcResult(request, null)); - assertThat(requestSnippetLines("request-with-url-encoded-parameter"), - hasItem("$ curl 'http://localhost/foo?k1=foo+bar%26' -i")); + result(get("/foo").param("k1", "foo bar&"))); } @Test public void postRequestWithOneParameter() throws IOException { - MockHttpServletRequest request = new MockHttpServletRequest("POST", "/foo"); - request.addParameter("k1", "v1"); + this.snippet.expectCurlRequest("post-request-with-one-parameter").withContents( + codeBlock("bash").content( + "$ curl 'http://localhost/foo' -i -X POST -d 'k1=v1'")); documentCurlRequest("post-request-with-one-parameter").handle( - new StubMvcResult(request, null)); - assertThat(requestSnippetLines("post-request-with-one-parameter"), - hasItem("$ curl 'http://localhost/foo' -i -X POST -d 'k1=v1'")); + result(post("/foo").param("k1", "v1"))); } @Test public void postRequestWithMultipleParameters() throws IOException { - MockHttpServletRequest request = new MockHttpServletRequest("POST", "/foo"); - request.addParameter("k1", "v1"); - request.addParameter("k2", "v2"); - request.addParameter("k1", "v1-bis"); + this.snippet.expectCurlRequest("post-request-with-multiple-parameters") + .withContents( + codeBlock("bash").content( + "$ curl 'http://localhost/foo' -i -X POST" + + " -d 'k1=v1&k1=v1-bis&k2=v2'")); documentCurlRequest("post-request-with-multiple-parameters").handle( - new StubMvcResult(request, null)); - assertThat( - requestSnippetLines("post-request-with-multiple-parameters"), - hasItem("$ curl 'http://localhost/foo' -i -X POST -d 'k1=v1&k1=v1-bis&k2=v2'")); + result(post("/foo").param("k1", "v1", "v1-bis").param("k2", "v2"))); } @Test public void postRequestWithUrlEncodedParameter() throws IOException { - MockHttpServletRequest request = new MockHttpServletRequest("POST", "/foo"); - request.addParameter("k1", "a&b"); + this.snippet + .expectCurlRequest("post-request-with-url-encoded-parameter") + .withContents( + codeBlock("bash").content( + "$ curl 'http://localhost/foo' -i -X POST -d 'k1=a%26b'")); documentCurlRequest("post-request-with-url-encoded-parameter").handle( - new StubMvcResult(request, null)); - assertThat(requestSnippetLines("post-request-with-url-encoded-parameter"), - hasItem("$ curl 'http://localhost/foo' -i -X POST -d 'k1=a%26b'")); + result(post("/foo").param("k1", "a&b"))); } @Test public void requestWithHeaders() throws IOException { - MockHttpServletRequest request = new MockHttpServletRequest("GET", "/foo"); - request.setContentType(MediaType.APPLICATION_JSON_VALUE); - request.addHeader("a", "alpha"); + this.snippet.expectCurlRequest("request-with-headers").withContents( + codeBlock("bash").content( + "$ curl 'http://localhost/foo' -i" + + " -H 'Content-Type: application/json' -H 'a: alpha'")); documentCurlRequest("request-with-headers").handle( - new StubMvcResult(request, null)); - assertThat( - requestSnippetLines("request-with-headers"), - hasItem("$ curl 'http://localhost/foo' -i -H 'Content-Type: application/json' -H 'a: alpha'")); + result(get("/foo").contentType(MediaType.APPLICATION_JSON).header("a", + "alpha"))); } @Test public void httpWithNonStandardPort() throws IOException { + this.snippet.expectCurlRequest("http-with-non-standard-port").withContents( + codeBlock("bash").content("$ curl 'http://localhost:8080/foo' -i")); MockHttpServletRequest request = new MockHttpServletRequest("GET", "/foo"); request.setServerPort(8080); - documentCurlRequest("http-with-non-standard-port").handle( - new StubMvcResult(request, null)); - assertThat(requestSnippetLines("http-with-non-standard-port"), - hasItem("$ curl 'http://localhost:8080/foo' -i")); + documentCurlRequest("http-with-non-standard-port").handle(result(request)); } @Test public void httpsWithStandardPort() throws IOException { + this.snippet.expectCurlRequest("https-with-standard-port").withContents( + codeBlock("bash").content("$ curl 'https://localhost/foo' -i")); MockHttpServletRequest request = new MockHttpServletRequest("GET", "/foo"); request.setServerPort(443); request.setScheme("https"); - documentCurlRequest("https-with-standard-port").handle( - new StubMvcResult(request, null)); - assertThat(requestSnippetLines("https-with-standard-port"), - hasItem("$ curl 'https://localhost/foo' -i")); + documentCurlRequest("https-with-standard-port").handle(result(request)); } @Test public void httpsWithNonStandardPort() throws IOException { + this.snippet.expectCurlRequest("https-with-non-standard-port").withContents( + codeBlock("bash").content("$ curl 'https://localhost:8443/foo' -i")); MockHttpServletRequest request = new MockHttpServletRequest("GET", "/foo"); request.setServerPort(8443); request.setScheme("https"); - documentCurlRequest("https-with-non-standard-port").handle( - new StubMvcResult(request, null)); - assertThat(requestSnippetLines("https-with-non-standard-port"), - hasItem("$ curl 'https://localhost:8443/foo' -i")); + documentCurlRequest("https-with-non-standard-port").handle(result(request)); } @Test public void requestWithCustomHost() throws IOException { + this.snippet.expectCurlRequest("request-with-custom-host").withContents( + codeBlock("bash").content("$ curl 'http://api.example.com/foo' -i")); MockHttpServletRequest request = new MockHttpServletRequest("GET", "/foo"); request.setServerName("api.example.com"); - documentCurlRequest("request-with-custom-host").handle( - new StubMvcResult(request, null)); - assertThat(requestSnippetLines("request-with-custom-host"), - hasItem("$ curl 'http://api.example.com/foo' -i")); + documentCurlRequest("request-with-custom-host").handle(result(request)); } @Test public void requestWithContextPathWithSlash() throws IOException { + this.snippet.expectCurlRequest("request-with-custom-context-with-slash") + .withContents( + codeBlock("bash").content( + "$ curl 'http://api.example.com/v3/foo' -i")); MockHttpServletRequest request = new MockHttpServletRequest("GET", "/foo"); request.setServerName("api.example.com"); request.setContextPath("/v3"); documentCurlRequest("request-with-custom-context-with-slash").handle( - new StubMvcResult(request, null)); - assertThat(requestSnippetLines("request-with-custom-context-with-slash"), - hasItem("$ curl 'http://api.example.com/v3/foo' -i")); + result(request)); } @Test public void requestWithContextPathWithoutSlash() throws IOException { + this.snippet.expectCurlRequest("request-with-custom-context-without-slash") + .withContents( + codeBlock("bash").content( + "$ curl 'http://api.example.com/v3/foo' -i")); MockHttpServletRequest request = new MockHttpServletRequest("GET", "/foo"); request.setServerName("api.example.com"); request.setContextPath("v3"); documentCurlRequest("request-with-custom-context-without-slash").handle( - new StubMvcResult(request, null)); - assertThat(requestSnippetLines("request-with-custom-context-without-slash"), - hasItem("$ curl 'http://api.example.com/v3/foo' -i")); + result(request)); } - private List requestSnippetLines(String snippetName) throws IOException { - return snippetLines(snippetName, "curl-request"); - } - - private List snippetLines(String snippetName, String snippetType) - throws IOException { - File snippetDir = new File(this.outputDir, snippetName); - File snippetFile = new File(snippetDir, snippetType + ".adoc"); - String line = null; - List lines = new ArrayList(); - BufferedReader reader = new BufferedReader(new FileReader(snippetFile)); - try { - while ((line = reader.readLine()) != null) { - lines.add(line); - } - } - finally { - reader.close(); - } - return lines; - } } diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/http/HttpDocumentationTests.java b/spring-restdocs/src/test/java/org/springframework/restdocs/http/HttpDocumentationTests.java index d0fc8ec2..4211c909 100644 --- a/spring-restdocs/src/test/java/org/springframework/restdocs/http/HttpDocumentationTests.java +++ b/spring-restdocs/src/test/java/org/springframework/restdocs/http/HttpDocumentationTests.java @@ -16,27 +16,25 @@ package org.springframework.restdocs.http; -import static org.hamcrest.CoreMatchers.hasItem; -import static org.hamcrest.CoreMatchers.hasItems; -import static org.junit.Assert.assertThat; +import static org.springframework.http.HttpStatus.BAD_REQUEST; +import static org.springframework.http.HttpStatus.OK; import static org.springframework.restdocs.http.HttpDocumentation.documentHttpRequest; import static org.springframework.restdocs.http.HttpDocumentation.documentHttpResponse; +import static org.springframework.restdocs.test.SnippetMatchers.httpRequest; +import static org.springframework.restdocs.test.SnippetMatchers.httpResponse; +import static org.springframework.restdocs.test.StubMvcResult.result; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.web.bind.annotation.RequestMethod.GET; +import static org.springframework.web.bind.annotation.RequestMethod.POST; -import java.io.BufferedReader; -import java.io.File; -import java.io.FileReader; import java.io.IOException; -import java.util.ArrayList; -import java.util.List; -import org.junit.After; -import org.junit.Before; +import org.junit.Rule; import org.junit.Test; -import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; -import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.mock.web.MockHttpServletResponse; -import org.springframework.restdocs.StubMvcResult; +import org.springframework.restdocs.test.ExpectedSnippet; /** * Tests for {@link HttpDocumentation} @@ -45,131 +43,93 @@ import org.springframework.restdocs.StubMvcResult; */ public class HttpDocumentationTests { - private final File outputDir = new File("build/http-documentation-tests"); - - @Before - public void setup() { - System.setProperty("org.springframework.restdocs.outputDir", - this.outputDir.getAbsolutePath()); - } - - @After - public void cleanup() { - System.clearProperty("org.springframework.restdocs.outputDir"); - } + @Rule + public final ExpectedSnippet snippet = new ExpectedSnippet(); @Test public void getRequest() throws IOException { - MockHttpServletRequest request = new MockHttpServletRequest("GET", "/foo"); - request.addHeader("Alpha", "a"); - documentHttpRequest("get-request").handle(new StubMvcResult(request, null)); - assertThat(requestSnippetLines("get-request"), - hasItems("GET /foo HTTP/1.1", "Alpha: a")); + this.snippet.expectHttpRequest("get-request").withContents( + httpRequest(GET, "/foo").header("Alpha", "a")); + + documentHttpRequest("get-request").handle( + result(get("/foo").header("Alpha", "a"))); } @Test public void getRequestWithQueryString() throws IOException { - MockHttpServletRequest request = new MockHttpServletRequest("GET", "/foo?bar=baz"); + this.snippet.expectHttpRequest("get-request-with-query-string").withContents( + httpRequest(GET, "/foo?bar=baz")); + documentHttpRequest("get-request-with-query-string").handle( - new StubMvcResult(request, null)); - assertThat(requestSnippetLines("get-request-with-query-string"), - hasItems("GET /foo?bar=baz HTTP/1.1")); + result(get("/foo?bar=baz"))); } @Test public void getRequestWithParameter() throws IOException { - MockHttpServletRequest request = new MockHttpServletRequest("GET", "/foo"); - request.addParameter("b&r", "baz"); + this.snippet.expectHttpRequest("get-request-with-parameter").withContents( + httpRequest(GET, "/foo?b%26r=baz")); + documentHttpRequest("get-request-with-parameter").handle( - new StubMvcResult(request, null)); - assertThat(requestSnippetLines("get-request-with-parameter"), - hasItems("GET /foo?b%26r=baz HTTP/1.1")); + result(get("/foo").param("b&r", "baz"))); } @Test public void postRequestWithContent() throws IOException { - MockHttpServletRequest request = new MockHttpServletRequest("POST", "/foo"); - byte[] content = "Hello, world".getBytes(); - request.setContent(content); + this.snippet.expectHttpRequest("post-request-with-content").withContents( + httpRequest(POST, "/foo") // + .content("Hello, world")); + documentHttpRequest("post-request-with-content").handle( - new StubMvcResult(request, null)); - assertThat(requestSnippetLines("post-request-with-content"), - hasItems("POST /foo HTTP/1.1", "Hello, world")); + result(post("/foo").content("Hello, world"))); } @Test public void postRequestWithParameter() throws IOException { - MockHttpServletRequest request = new MockHttpServletRequest("POST", "/foo"); - request.addParameter("b&r", "baz"); - request.addParameter("a", "alpha"); + this.snippet.expectHttpRequest("post-request-with-parameter").withContents( + httpRequest(POST, "/foo") // + .header("Content-Type", "application/x-www-form-urlencoded") // + .content("b%26r=baz&a=alpha")); + documentHttpRequest("post-request-with-parameter").handle( - new StubMvcResult(request, null)); - assertThat( - requestSnippetLines("post-request-with-parameter"), - hasItems("POST /foo HTTP/1.1", - "Content-Type: application/x-www-form-urlencoded", - "b%26r=baz&a=alpha")); + result(post("/foo").param("b&r", "baz").param("a", "alpha"))); } @Test public void basicResponse() throws IOException { - documentHttpResponse("basic-response").handle( - new StubMvcResult(null, new MockHttpServletResponse())); - assertThat(responseSnippetLines("basic-response"), hasItem("HTTP/1.1 200 OK")); + this.snippet.expectHttpResponse("basic-response").withContents(httpResponse(OK)); + documentHttpResponse("basic-response").handle(result()); } @Test public void nonOkResponse() throws IOException { + this.snippet.expectHttpResponse("non-ok-response").withContents( + httpResponse(BAD_REQUEST)); + MockHttpServletResponse response = new MockHttpServletResponse(); - response.setStatus(HttpStatus.BAD_REQUEST.value()); - documentHttpResponse("non-ok-response").handle(new StubMvcResult(null, response)); - assertThat(responseSnippetLines("non-ok-response"), - hasItem("HTTP/1.1 400 Bad Request")); + response.setStatus(BAD_REQUEST.value()); + documentHttpResponse("non-ok-response").handle(result(response)); } @Test public void responseWithHeaders() throws IOException { + this.snippet.expectHttpResponse("response-with-headers").withContents( + httpResponse(OK) // + .header("Content-Type", "application/json") // + .header("a", "alpha")); + MockHttpServletResponse response = new MockHttpServletResponse(); response.setContentType(MediaType.APPLICATION_JSON_VALUE); response.setHeader("a", "alpha"); - documentHttpResponse("non-ok-response").handle(new StubMvcResult(null, response)); - assertThat(responseSnippetLines("non-ok-response"), - hasItems("HTTP/1.1 200 OK", "Content-Type: application/json", "a: alpha")); + documentHttpResponse("response-with-headers").handle(result(response)); } @Test public void responseWithContent() throws IOException { + this.snippet.expectHttpResponse("response-with-content").withContents( + httpResponse(OK).content("content")); MockHttpServletResponse response = new MockHttpServletResponse(); response.getWriter().append("content"); - documentHttpResponse("response-with-content").handle( - new StubMvcResult(null, response)); - assertThat(responseSnippetLines("response-with-content"), - hasItems("HTTP/1.1 200 OK", "content")); + documentHttpResponse("response-with-content").handle(result(response)); } - private List requestSnippetLines(String snippetName) throws IOException { - return snippetLines(snippetName, "http-request"); - } - - private List responseSnippetLines(String snippetName) throws IOException { - return snippetLines(snippetName, "http-response"); - } - - private List snippetLines(String snippetName, String snippetType) - throws IOException { - File snippetDir = new File(this.outputDir, snippetName); - File snippetFile = new File(snippetDir, snippetType + ".adoc"); - String line = null; - List lines = new ArrayList(); - BufferedReader reader = new BufferedReader(new FileReader(snippetFile)); - try { - while ((line = reader.readLine()) != null) { - lines.add(line); - } - } - finally { - reader.close(); - } - return lines; - } } diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/hypermedia/HypermediaDocumentationTests.java b/spring-restdocs/src/test/java/org/springframework/restdocs/hypermedia/HypermediaDocumentationTests.java new file mode 100644 index 00000000..15073949 --- /dev/null +++ b/spring-restdocs/src/test/java/org/springframework/restdocs/hypermedia/HypermediaDocumentationTests.java @@ -0,0 +1,117 @@ +/* + * Copyright 2014-2015 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.restdocs.hypermedia; + +import static org.hamcrest.CoreMatchers.equalTo; +import static org.springframework.restdocs.hypermedia.HypermediaDocumentation.documentLinks; +import static org.springframework.restdocs.test.SnippetMatchers.tableWithHeader; +import static org.springframework.restdocs.test.StubMvcResult.result; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.restdocs.RestDocumentationException; +import org.springframework.restdocs.test.ExpectedSnippet; + +/** + * Tests for {@link HypermediaDocumentation} + * + * @author Andy Wilkinson + */ +public class HypermediaDocumentationTests { + + @Rule + public ExpectedSnippet snippet = new ExpectedSnippet(); + + @Rule + public ExpectedException thrown = ExpectedException.none(); + + @Test + public void undocumentedLink() throws IOException { + this.thrown.expect(RestDocumentationException.class); + this.thrown.expectMessage(equalTo("Links with the following relations were not" + + " documented: [foo]")); + documentLinks("undocumented-link", + new StubLinkExtractor().withLinks(new Link("foo", "bar"))).handle( + result()); + } + + @Test + public void missingLink() throws IOException { + this.thrown.expect(RestDocumentationException.class); + this.thrown.expectMessage(equalTo("Links with the following relations were not" + + " found in the response: [foo]")); + documentLinks("undocumented-link", new StubLinkExtractor(), + new LinkDescriptor("foo").description("bar")).handle(result()); + } + + @Test + public void undocumentedLinkAndMissingLink() throws IOException { + this.thrown.expect(RestDocumentationException.class); + this.thrown.expectMessage(equalTo("Links with the following relations were not" + + " documented: [a]. Links with the following relations were not" + + " found in the response: [foo]")); + documentLinks("undocumented-link-and-missing-link", + new StubLinkExtractor().withLinks(new Link("a", "alpha")), + new LinkDescriptor("foo").description("bar")).handle(result()); + } + + @Test + public void documentedLinks() throws IOException { + this.snippet.expectLinks("documented-links").withContents( // + tableWithHeader("Relation", "Description") // + .row("a", "one") // + .row("b", "two")); + documentLinks( + "documented-links", + new StubLinkExtractor().withLinks(new Link("a", "alpha"), new Link("b", + "bravo")), new LinkDescriptor("a").description("one"), + new LinkDescriptor("b").description("two")).handle(result()); + } + + private static class StubLinkExtractor implements LinkExtractor { + + private Map> linksByRel = new HashMap>(); + + @Override + public Map> extractLinks(MockHttpServletResponse response) + throws IOException { + return this.linksByRel; + } + + private StubLinkExtractor withLinks(Link... links) { + for (Link link : links) { + List linksWithRel = this.linksByRel.get(link.getRel()); + if (linksWithRel == null) { + linksWithRel = new ArrayList(); + this.linksByRel.put(link.getRel(), linksWithRel); + } + linksWithRel.add(link); + } + return this; + } + + } + +} diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/payload/FieldValidatorTests.java b/spring-restdocs/src/test/java/org/springframework/restdocs/payload/FieldValidatorTests.java index e6f2f43f..2706a832 100644 --- a/spring-restdocs/src/test/java/org/springframework/restdocs/payload/FieldValidatorTests.java +++ b/spring-restdocs/src/test/java/org/springframework/restdocs/payload/FieldValidatorTests.java @@ -24,7 +24,7 @@ import java.util.Arrays; import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; -import org.springframework.restdocs.payload.FieldValidator.FieldValidationException; +import org.springframework.restdocs.RestDocumentationException; /** * Tests for {@link FieldValidator} @@ -70,9 +70,10 @@ public class FieldValidatorTests { @Test public void missingField() throws IOException { - this.thrownException.expect(FieldValidationException.class); + this.thrownException.expect(RestDocumentationException.class); this.thrownException - .expectMessage(equalTo("Fields with the following paths were not found in the payload: [y, z]")); + .expectMessage(equalTo("Fields with the following paths were not found" + + " in the payload: [y, z]")); this.fieldValidator.validate(this.payload, Arrays.asList( new FieldDescriptor("a"), new FieldDescriptor("a.b"), new FieldDescriptor("y"), new FieldDescriptor("z"))); @@ -80,10 +81,10 @@ public class FieldValidatorTests { @Test public void undocumentedField() throws IOException { - this.thrownException.expect(FieldValidationException.class); - this.thrownException - .expectMessage(equalTo(String - .format("Portions of the payload were not documented:%n{%n \"a\" : {%n \"c\" : true%n }%n}"))); + this.thrownException.expect(RestDocumentationException.class); + this.thrownException.expectMessage(equalTo(String + .format("The following parts of the payload were not" + + " documented:%n{%n \"a\" : {%n \"c\" : true%n }%n}"))); this.fieldValidator.validate(this.payload, Arrays.asList(new FieldDescriptor("a.b"), new FieldDescriptor("a.d"))); } diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/payload/PayloadDocumentationTests.java b/spring-restdocs/src/test/java/org/springframework/restdocs/payload/PayloadDocumentationTests.java index 6e45e20f..fd3be61f 100644 --- a/spring-restdocs/src/test/java/org/springframework/restdocs/payload/PayloadDocumentationTests.java +++ b/spring-restdocs/src/test/java/org/springframework/restdocs/payload/PayloadDocumentationTests.java @@ -15,31 +15,24 @@ */ package org.springframework.restdocs.payload; -import static org.hamcrest.Matchers.is; -import static org.hamcrest.core.IsEqual.equalTo; -import static org.junit.Assert.assertThat; +import static org.hamcrest.CoreMatchers.endsWith; +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.startsWith; import static org.springframework.restdocs.payload.PayloadDocumentation.documentRequestFields; import static org.springframework.restdocs.payload.PayloadDocumentation.documentResponseFields; import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; +import static org.springframework.restdocs.test.SnippetMatchers.tableWithHeader; +import static org.springframework.restdocs.test.StubMvcResult.result; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; -import java.io.BufferedReader; -import java.io.File; -import java.io.FileReader; import java.io.IOException; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.List; -import org.hamcrest.Matcher; -import org.hamcrest.collection.IsIterableContainingInAnyOrder; -import org.junit.After; -import org.junit.Before; +import org.junit.Rule; import org.junit.Test; -import org.springframework.mock.web.MockHttpServletRequest; +import org.junit.rules.ExpectedException; import org.springframework.mock.web.MockHttpServletResponse; -import org.springframework.restdocs.StubMvcResult; -import org.springframework.util.StringUtils; +import org.springframework.restdocs.RestDocumentationException; +import org.springframework.restdocs.test.ExpectedSnippet; /** * Tests for {@link PayloadDocumentation} @@ -48,40 +41,42 @@ import org.springframework.util.StringUtils; */ public class PayloadDocumentationTests { - private final File outputDir = new File("build/payload-documentation-tests"); + @Rule + public final ExpectedException thrown = ExpectedException.none(); - @Before - public void setup() { - System.setProperty("org.springframework.restdocs.outputDir", - this.outputDir.getAbsolutePath()); - } - - @After - public void cleanup() { - System.clearProperty("org.springframework.restdocs.outputDir"); - } + @Rule + public final ExpectedSnippet snippet = new ExpectedSnippet(); @Test public void requestWithFields() throws IOException { - MockHttpServletRequest request = new MockHttpServletRequest("GET", "/foo"); - request.setContent("{\"a\": {\"b\": 5, \"c\": \"charlie\"}}".getBytes()); + this.snippet.expectRequestFields("request-with-fields").withContents( // + tableWithHeader("Path", "Type", "Description") // + .row("a.b", "Number", "one") // + .row("a.c", "String", "two") // + .row("a", "Object", "three")); + documentRequestFields("request-with-fields", fieldWithPath("a.b").description("one"), fieldWithPath("a.c").description("two"), fieldWithPath("a").description("three")).handle( - new StubMvcResult(request, null)); - assertThat( - snippet("request-with-fields", "request-fields"), - is(asciidoctorTableWith(header("Path", "Type", "Description"), - row("a.b", "Number", "one"), row("a.c", "String", "two"), - row("a", "Object", "three")))); + result(get("/foo").content("{\"a\": {\"b\": 5, \"c\": \"charlie\"}}"))); } @Test public void responseWithFields() throws IOException { + this.snippet.expectResponseFields("response-with-fields").withContents(// + tableWithHeader("Path", "Type", "Description") // + .row("id", "Number", "one") // + .row("date", "String", "two") // + .row("assets", "Array", "three") // + .row("assets[]", "Object", "four") // + .row("assets[].id", "Number", "five") // + .row("assets[].name", "String", "six")); + MockHttpServletResponse response = new MockHttpServletResponse(); - response.getWriter() - .append("{\"id\": 67,\"date\": \"2015-01-20\",\"assets\": [{\"id\":356,\"name\": \"sample\"}]}"); + response.getWriter().append( + "{\"id\": 67,\"date\": \"2015-01-20\",\"assets\":" + + " [{\"id\":356,\"name\": \"sample\"}]}"); documentResponseFields("response-with-fields", fieldWithPath("id").description("one"), fieldWithPath("date").description("two"), @@ -89,67 +84,41 @@ public class PayloadDocumentationTests { fieldWithPath("assets[]").description("four"), fieldWithPath("assets[].id").description("five"), fieldWithPath("assets[].name").description("six")).handle( - new StubMvcResult(new MockHttpServletRequest("GET", "/"), response)); - assertThat( - snippet("response-with-fields", "response-fields"), - is(asciidoctorTableWith(header("Path", "Type", "Description"), - row("id", "Number", "one"), row("date", "String", "two"), - row("assets", "Array", "three"), - row("assets[]", "Object", "four"), - row("assets[].id", "Number", "five"), - row("assets[].name", "String", "six")))); + result(response)); } - private Matcher> asciidoctorTableWith(String[] header, - String[]... rows) { - Collection> matchers = new ArrayList>(); - for (String headerItem : header) { - matchers.add(equalTo(headerItem)); - } - - for (String[] row : rows) { - for (String rowItem : row) { - matchers.add(equalTo(rowItem)); - } - } - - matchers.add(equalTo("|===")); - matchers.add(equalTo("")); - - return new IsIterableContainingInAnyOrder(matchers); + @Test + public void undocumentedRequestField() throws IOException { + this.thrown.expect(RestDocumentationException.class); + this.thrown + .expectMessage(startsWith("The following parts of the payload were not" + + " documented:")); + documentRequestFields("undocumented-request-fields").handle( + result(get("/foo").content("{\"a\": 5}"))); } - private String[] header(String... columns) { - String header = "|" - + StringUtils.collectionToDelimitedString(Arrays.asList(columns), "|"); - return new String[] { "", "|===", header, "" }; + @Test + public void missingRequestField() throws IOException { + this.thrown.expect(RestDocumentationException.class); + this.thrown + .expectMessage(equalTo("Fields with the following paths were not found" + + " in the payload: [a.b]")); + documentRequestFields("missing-request-fields", + fieldWithPath("a.b").description("one")).handle( + result(get("/foo").content("{}"))); } - private String[] row(String... entries) { - List lines = new ArrayList(); - for (String entry : entries) { - lines.add("|" + entry); - } - lines.add(""); - return lines.toArray(new String[lines.size()]); + @Test + public void undocumentedRequestFieldAndMissingRequestField() throws IOException { + this.thrown.expect(RestDocumentationException.class); + this.thrown + .expectMessage(startsWith("The following parts of the payload were not" + + " documented:")); + this.thrown + .expectMessage(endsWith("Fields with the following paths were not found" + + " in the payload: [a.b]")); + documentRequestFields("undocumented-request-field-and-missing-request-field", + fieldWithPath("a.b").description("one")).handle( + result(get("/foo").content("{ \"a\": { \"c\": 5 }}"))); } - - private List snippet(String snippetName, String snippetType) - throws IOException { - File snippetDir = new File(this.outputDir, snippetName); - File snippetFile = new File(snippetDir, snippetType + ".adoc"); - String line = null; - List lines = new ArrayList(); - BufferedReader reader = new BufferedReader(new FileReader(snippetFile)); - try { - while ((line = reader.readLine()) != null) { - lines.add(line); - } - } - finally { - reader.close(); - } - return lines; - } - } diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/test/ExpectedSnippet.java b/spring-restdocs/src/test/java/org/springframework/restdocs/test/ExpectedSnippet.java new file mode 100644 index 00000000..5c7f9b52 --- /dev/null +++ b/spring-restdocs/src/test/java/org/springframework/restdocs/test/ExpectedSnippet.java @@ -0,0 +1,193 @@ +/* + * Copyright 2014-2015 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.restdocs.test; + +import static org.hamcrest.core.IsEqual.equalTo; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertTrue; + +import java.io.File; +import java.io.FileReader; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; + +import org.hamcrest.Matcher; +import org.hamcrest.collection.IsIterableContainingInAnyOrder; +import org.junit.rules.TestRule; +import org.junit.runner.Description; +import org.junit.runners.model.Statement; +import org.springframework.restdocs.snippet.SnippetWritingResultHandler; +import org.springframework.util.FileCopyUtils; +import org.springframework.util.StringUtils; + +/** + * The {@code ExpectedSnippet} rule is used to verify that a + * {@link SnippetWritingResultHandler} has generated the expected snippet. + * + * @author Andy Wilkinson + */ +public class ExpectedSnippet implements TestRule { + + private String expectedName; + + private String expectedType; + + private Matcher expectedContents; + + private File outputDir; + + @Override + public Statement apply(final Statement base, Description description) { + this.outputDir = new File("build/" + description.getTestClass().getSimpleName()); + return new OutputDirectoryStatement(new ExpectedSnippetStatement(base), + this.outputDir); + } + + private static final class OutputDirectoryStatement extends Statement { + + private final Statement delegate; + + private final File outputDir; + + public OutputDirectoryStatement(Statement delegate, File outputDir) { + this.delegate = delegate; + this.outputDir = outputDir; + } + + @Override + public void evaluate() throws Throwable { + System.setProperty("org.springframework.restdocs.outputDir", + this.outputDir.getAbsolutePath()); + try { + this.delegate.evaluate(); + } + finally { + System.clearProperty("org.springframework.restdocs.outputDir"); + } + } + } + + private final class ExpectedSnippetStatement extends Statement { + + private final Statement delegate; + + public ExpectedSnippetStatement(Statement delegate) { + this.delegate = delegate; + } + + @Override + public void evaluate() throws Throwable { + this.delegate.evaluate(); + verifySnippet(); + } + + } + + private void verifySnippet() throws IOException { + if (this.outputDir != null && this.expectedName != null) { + File snippetDir = new File(this.outputDir, this.expectedName); + File snippetFile = new File(snippetDir, this.expectedType + ".adoc"); + assertTrue("The file " + snippetFile + " does not exist or is not a file", + snippetFile.isFile()); + if (this.expectedContents != null) { + assertThat(read(snippetFile), this.expectedContents); + } + } + } + + public ExpectedSnippet expectCurlRequest(String name) { + expect(name, "curl-request"); + return this; + } + + public ExpectedSnippet expectRequestFields(String name) { + expect(name, "request-fields"); + return this; + } + + public ExpectedSnippet expectResponseFields(String name) { + expect(name, "response-fields"); + return this; + } + + public ExpectedSnippet expectLinks(String name) { + expect(name, "links"); + return this; + } + + public ExpectedSnippet expectHttpRequest(String name) { + expect(name, "http-request"); + return this; + } + + public ExpectedSnippet expectHttpResponse(String name) { + expect(name, "http-response"); + return this; + } + + private ExpectedSnippet expect(String name, String type) { + this.expectedName = name; + this.expectedType = type; + return this; + } + + public void withContents(Matcher matcher) { + this.expectedContents = matcher; + } + + private String read(File snippetFile) throws IOException { + return FileCopyUtils.copyToString(new FileReader(snippetFile)); + } + + public Matcher> asciidoctorTableWith(String[] header, + String[]... rows) { + Collection> matchers = new ArrayList>(); + for (String headerItem : header) { + matchers.add(equalTo(headerItem)); + } + + for (String[] row : rows) { + for (String rowItem : row) { + matchers.add(equalTo(rowItem)); + } + } + + matchers.add(equalTo("|===")); + matchers.add(equalTo("")); + + return new IsIterableContainingInAnyOrder(matchers); + } + + public String[] header(String... columns) { + String header = "|" + + StringUtils.collectionToDelimitedString(Arrays.asList(columns), "|"); + return new String[] { "", "|===", header, "" }; + } + + public String[] row(String... entries) { + List lines = new ArrayList(); + for (String entry : entries) { + lines.add("|" + entry); + } + lines.add(""); + return lines.toArray(new String[lines.size()]); + } + +} diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/test/SnippetMatchers.java b/spring-restdocs/src/test/java/org/springframework/restdocs/test/SnippetMatchers.java new file mode 100644 index 00000000..801ad07f --- /dev/null +++ b/spring-restdocs/src/test/java/org/springframework/restdocs/test/SnippetMatchers.java @@ -0,0 +1,177 @@ +/* + * Copyright 2014-2015 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.restdocs.test; + +import java.io.StringWriter; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import org.hamcrest.BaseMatcher; +import org.hamcrest.Description; +import org.hamcrest.Matcher; +import org.springframework.http.HttpStatus; +import org.springframework.util.StringUtils; +import org.springframework.web.bind.annotation.RequestMethod; + +/** + * {@link Matcher Matchers} for verify the contents of generated documentation snippets. + * + * @author Andy Wilkinson + */ +public class SnippetMatchers { + + public static AsciidoctorTableMatcher tableWithHeader(String... headers) { + return new AsciidoctorTableMatcher(headers); + } + + public static HttpRequestMatcher httpRequest(RequestMethod method, String uri) { + return new HttpRequestMatcher(method, uri); + } + + public static HttpResponseMatcher httpResponse(HttpStatus status) { + return new HttpResponseMatcher(status); + } + + @SuppressWarnings({ "rawtypes" }) + public static AsciidoctorCodeBlockMatcher codeBlock(String language) { + return new AsciidoctorCodeBlockMatcher(language); + } + + private static abstract class AbstractSnippetMatcher extends BaseMatcher { + + private List lines = new ArrayList(); + + protected void addLine(String line) { + this.lines.add(line); + } + + protected void addLine(int index, String line) { + if (index < 0) { + index = index + this.lines.size(); + } + this.lines.add(index, line); + } + + @Override + public boolean matches(Object item) { + return getLinesAsString().equals(item); + } + + @Override + public void describeTo(Description description) { + description.appendText("Asciidoctor snippet"); + description.appendText(getLinesAsString()); + } + + @Override + public void describeMismatch(Object item, Description description) { + description.appendText("was:"); + if (item instanceof String) { + description.appendText((String) item); + } + else { + description.appendValue(item); + } + } + + private String getLinesAsString() { + StringWriter writer = new StringWriter(); + for (String line : this.lines) { + writer.append(String.format("%s%n", line)); + } + return writer.toString(); + } + } + + public static class AsciidoctorCodeBlockMatcher> + extends AbstractSnippetMatcher { + + protected AsciidoctorCodeBlockMatcher(String language) { + this.addLine(""); + this.addLine("[source," + language + "]"); + this.addLine("----"); + this.addLine("----"); + this.addLine(""); + } + + @SuppressWarnings("unchecked") + public T content(String content) { + this.addLine(-2, content); + return (T) this; + } + + } + + public static abstract class HttpMatcher> extends + AsciidoctorCodeBlockMatcher> { + + private int headerOffset = 4; + + protected HttpMatcher() { + super("http"); + } + + @SuppressWarnings("unchecked") + public T header(String name, String value) { + this.addLine(this.headerOffset++, name + ": " + value); + return (T) this; + } + + } + + public static class HttpResponseMatcher extends HttpMatcher { + + public HttpResponseMatcher(HttpStatus status) { + this.content("HTTP/1.1 " + status.value() + " " + status.getReasonPhrase()); + this.content(""); + } + + } + + public static class HttpRequestMatcher extends HttpMatcher { + + public HttpRequestMatcher(RequestMethod requestMethod, String uri) { + this.content(requestMethod.name() + " " + uri + " HTTP/1.1"); + this.content(""); + } + + } + + public static class AsciidoctorTableMatcher extends AbstractSnippetMatcher { + + private AsciidoctorTableMatcher(String... columns) { + this.addLine(""); + this.addLine("|==="); + String header = "|" + + StringUtils + .collectionToDelimitedString(Arrays.asList(columns), "|"); + this.addLine(header); + this.addLine(""); + this.addLine("|==="); + this.addLine(""); + } + + public AsciidoctorTableMatcher row(String... entries) { + for (String entry : entries) { + this.addLine(-2, "|" + entry); + } + this.addLine(-2, ""); + return this; + } + } +} diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/StubMvcResult.java b/spring-restdocs/src/test/java/org/springframework/restdocs/test/StubMvcResult.java similarity index 63% rename from spring-restdocs/src/test/java/org/springframework/restdocs/StubMvcResult.java rename to spring-restdocs/src/test/java/org/springframework/restdocs/test/StubMvcResult.java index 1afea51e..ab11f19d 100644 --- a/spring-restdocs/src/test/java/org/springframework/restdocs/StubMvcResult.java +++ b/spring-restdocs/src/test/java/org/springframework/restdocs/test/StubMvcResult.java @@ -14,11 +14,13 @@ * limitations under the License. */ -package org.springframework.restdocs; +package org.springframework.restdocs.test; import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.mock.web.MockServletContext; import org.springframework.test.web.servlet.MvcResult; +import org.springframework.test.web.servlet.RequestBuilder; import org.springframework.web.servlet.FlashMap; import org.springframework.web.servlet.HandlerInterceptor; import org.springframework.web.servlet.ModelAndView; @@ -35,11 +37,43 @@ public class StubMvcResult implements MvcResult { private final MockHttpServletResponse response; - public StubMvcResult(MockHttpServletRequest request, MockHttpServletResponse response) { + public static StubMvcResult result() { + return new StubMvcResult(); + } + + public static StubMvcResult result(RequestBuilder requestBuilder) { + return new StubMvcResult(requestBuilder); + } + + public static StubMvcResult result(MockHttpServletRequest request) { + return new StubMvcResult(request); + } + + public static StubMvcResult result(MockHttpServletResponse response) { + return new StubMvcResult(response); + } + + private StubMvcResult() { + this(new MockHttpServletRequest(), new MockHttpServletResponse()); + } + + private StubMvcResult(MockHttpServletRequest request) { + this(request, new MockHttpServletResponse()); + } + + private StubMvcResult(MockHttpServletResponse response) { + this(new MockHttpServletRequest(), response); + } + + private StubMvcResult(MockHttpServletRequest request, MockHttpServletResponse response) { this.request = request; this.response = response; } + private StubMvcResult(RequestBuilder requestBuilder) { + this(requestBuilder.buildRequest(new MockServletContext())); + } + @Override public MockHttpServletRequest getRequest() { return this.request;