Initial support for documenting fields in requests and responses

This commit introduces support for documenting the fields in a request
and response, modelled on the existing support for documenting links.
A test with field documentation enabled will fail if a documented
field is missing from the request or response, or if a field present
in the request or response has not been documented.

See gh-24
Closes gh-25
This commit is contained in:
Andreas Evers
2015-02-22 23:48:29 +01:00
committed by Andy Wilkinson
parent cc54e5d7b0
commit ecfc7abd92
17 changed files with 998 additions and 1 deletions

View File

@@ -20,6 +20,8 @@ import static org.springframework.restdocs.curl.CurlDocumentation.documentCurlRe
import static org.springframework.restdocs.curl.CurlDocumentation.documentCurlRequestAndResponse;
import static org.springframework.restdocs.curl.CurlDocumentation.documentCurlResponse;
import static org.springframework.restdocs.hypermedia.HypermediaDocumentation.documentLinks;
import static org.springframework.restdocs.state.StateDocumentation.documentRequestFields;
import static org.springframework.restdocs.state.StateDocumentation.documentResponseFields;
import java.util.ArrayList;
import java.util.List;
@@ -28,6 +30,9 @@ import org.springframework.restdocs.hypermedia.HypermediaDocumentation;
import org.springframework.restdocs.hypermedia.LinkDescriptor;
import org.springframework.restdocs.hypermedia.LinkExtractor;
import org.springframework.restdocs.hypermedia.LinkExtractors;
import org.springframework.restdocs.state.FieldDescriptor;
import org.springframework.restdocs.state.Path;
import org.springframework.restdocs.state.StateDocumentation;
import org.springframework.test.web.servlet.MvcResult;
import org.springframework.test.web.servlet.ResultHandler;
@@ -35,6 +40,7 @@ import org.springframework.test.web.servlet.ResultHandler;
* A Spring MVC Test {@code ResultHandler} for documenting RESTful APIs.
*
* @author Andy Wilkinson
* @author Andreas Evers
* @see RestDocumentation#document(String)
*/
public class RestDocumentationResultHandler implements ResultHandler {
@@ -97,4 +103,42 @@ public class RestDocumentationResultHandler implements ResultHandler {
return this;
}
/**
* Document the fields in the response using the given {@code descriptors}. The fields
* are extracted from the response based on its content type.
* <p>
* If a field is present in the response but is not described by one of the
* descriptors a failure will occur when this handler is invoked. Similarly, if a
* field is described but is not present in the response a failure will also occur
* when this handler is invoked.
*
* @param descriptors the link descriptors
* @return {@code this}
* @see StateDocumentation#fieldWithPath(Path)
*/
public RestDocumentationResultHandler withRequestFields(
FieldDescriptor... descriptors) {
this.delegates.add(documentRequestFields(this.outputDir, descriptors));
return this;
}
/**
* Document the fields in the response using the given {@code descriptors}. The fields
* are extracted from the response based on its content type.
* <p>
* If a field is present in the response but is not described by one of the
* descriptors a failure will occur when this handler is invoked. Similarly, if a
* field is described but is not present in the response a failure will also occur
* when this handler is invoked.
*
* @param descriptors the link descriptors
* @return {@code this}
* @see StateDocumentation#fieldWithPath(Path)
*/
public RestDocumentationResultHandler withResponseFields(
FieldDescriptor... descriptors) {
this.delegates.add(documentResponseFields(this.outputDir, descriptors));
return this;
}
}

View File

@@ -59,5 +59,4 @@ public abstract class HypermediaDocumentation {
return new LinkSnippetResultHandler(outputDir, linkExtractor,
Arrays.asList(descriptors));
}
}

View File

@@ -0,0 +1,95 @@
/*
* 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.state;
import org.springframework.core.style.ToStringCreator;
/**
* Representation of a field used in a Hypermedia-based API
*
* @author Andreas Evers
*/
public class Field {
private final Path path;
private final Object value;
/**
* Creates a new {@code Field} with the given {@code path} and {@code value}
*
* @param path The field's path
* @param value The field's value
*/
public Field(Path path, Object value) {
this.path = path;
this.value = value;
}
/**
* Returns the field's {@code path}
* @return the field's {@code path}
*/
public Path getPath() {
return this.path;
}
/**
* Returns the field's {@code value}
* @return the field's {@code value}
*/
public Object getValue() {
return this.value;
}
@Override
public int hashCode() {
int prime = 31;
int result = 1;
result = prime * result + this.path.hashCode();
result = prime * result + this.value.hashCode();
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
Field other = (Field) obj;
if (!this.path.equals(other.path)) {
return false;
}
if (!this.value.equals(other.value)) {
return false;
}
return true;
}
@Override
public String toString() {
return new ToStringCreator(this).append("path", this.path)
.append("value", this.value).toString();
}
}

View File

@@ -0,0 +1,105 @@
/*
* 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.state;
/**
* A description of a field found in a hypermedia API
*
* @see StateDocumentation#fieldWithPath(Path)
*
* @author Andreas Evers
*/
public class FieldDescriptor {
private final Path path;
private String type;
private boolean required;
private String constraints;
private String description;
FieldDescriptor(Path path) {
this.path = path;
}
/**
* Specifies the type of the field
*
* @param type The field's type (could be number, string, boolean, array, object, ...)
* @return {@code this}
*/
public FieldDescriptor type(String type) {
this.type = type;
return this;
}
/**
* Specifies necessity of the field
*
* @param required The field's necessity
* @return {@code this}
*/
public FieldDescriptor required(boolean required) {
this.required = required;
return this;
}
/**
* Specifies the constraints of the field
*
* @param constraints The field's constraints
* @return {@code this}
*/
public FieldDescriptor constraints(String constraints) {
this.constraints = constraints;
return this;
}
/**
* Specifies the description of the field
*
* @param description The field's description
* @return {@code this}
*/
public FieldDescriptor description(String description) {
this.description = description;
return this;
}
Path getPath() {
return this.path;
}
String getType() {
return this.type;
}
boolean isRequired() {
return this.required;
}
String getConstraints() {
return this.constraints;
}
String getDescription() {
return this.description;
}
}

View File

@@ -0,0 +1,86 @@
/*
* 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.state;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.util.Assert;
import com.fasterxml.jackson.databind.ObjectMapper;
/**
* A {@code FieldExtractor} is used to extract {@link Field fields} from a JSON response.
* The expected format of the links in the response is determined by the implementation.
*
* @author Andy Wilkinson
*
*/
public class FieldExtractor {
private final ObjectMapper objectMapper = new ObjectMapper();
private Map<Path, Field> extractedFields = new HashMap<>();
@SuppressWarnings("unchecked")
public Map<Path, Field> extractFields(MockHttpServletRequest request)
throws IOException {
Map<String, Object> jsonContent = this.objectMapper.readValue(
request.getInputStream(), Map.class);
extractFieldsRecursively(jsonContent);
return this.extractedFields;
}
@SuppressWarnings("unchecked")
public Map<Path, Field> extractFields(MockHttpServletResponse response)
throws IOException {
String responseBody = response.getContentAsString();
Assert.hasText(responseBody,
"The response doesn't contain a body to extract fields from");
Map<String, Object> jsonContent = this.objectMapper.readValue(responseBody,
Map.class);
extractFieldsRecursively(jsonContent);
return this.extractedFields;
}
private void extractFieldsRecursively(Map<String, Object> jsonContent) {
extractFieldsRecursively(null, jsonContent);
}
@SuppressWarnings("unchecked")
private void extractFieldsRecursively(Path previousSteps,
Map<String, Object> jsonContent) {
for (Entry<String, Object> entry : jsonContent.entrySet()) {
Path path;
if (previousSteps == null) {
path = new Path(entry.getKey());
}
else {
path = new Path(previousSteps, entry.getKey());
}
this.extractedFields.put(path, new Field(path, entry.getValue()));
if (entry.getValue() instanceof Map) {
Map<String, Object> value = (Map<String, Object>) entry.getValue();
extractFieldsRecursively(path, value);
}
}
}
}

View File

@@ -0,0 +1,98 @@
/*
* 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.state;
import static org.springframework.restdocs.state.FieldSnippetResultHandler.Type.REQUEST;
import java.io.IOException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.SortedSet;
import java.util.TreeSet;
import org.springframework.restdocs.snippet.DocumentationWriter;
import org.springframework.restdocs.snippet.SnippetWritingResultHandler;
import org.springframework.test.web.servlet.MvcResult;
import org.springframework.util.Assert;
/**
* A {@link SnippetWritingResultHandler} that produces a snippet documenting a RESTful
* resource's request or response fields.
*
* @author Andreas Evers
*/
public class FieldSnippetResultHandler extends SnippetWritingResultHandler {
private final Map<Path, FieldDescriptor> descriptorsByName = new HashMap<Path, FieldDescriptor>();
private final Type type;
private FieldExtractor extractor = new FieldExtractor();
private StateDocumentationValidator validator;
enum Type {
REQUEST, RESPONSE;
}
FieldSnippetResultHandler(String outputDir, FieldSnippetResultHandler.Type type,
List<FieldDescriptor> descriptors) {
super(outputDir, type.toString().toLowerCase() + "fields");
this.type = type;
for (FieldDescriptor descriptor : descriptors) {
Assert.notNull(descriptor.getPath());
Assert.hasText(descriptor.getDescription());
this.descriptorsByName.put(descriptor.getPath(), descriptor);
}
this.validator = new StateDocumentationValidator(type);
}
@Override
protected void handle(MvcResult result, DocumentationWriter writer)
throws IOException {
Map<Path, Field> fields;
if (this.type == REQUEST) {
fields = this.extractor.extractFields(result.getRequest());
}
else {
fields = this.extractor.extractFields(result.getResponse());
}
SortedSet<Path> actualFields = new TreeSet<Path>(fields.keySet());
SortedSet<Path> expectedFields = new TreeSet<Path>(
this.descriptorsByName.keySet());
this.validator.validateFields(actualFields, expectedFields);
writer.println("|===");
writer.println("| Path | Description | Type | Required | Constraints");
for (Entry<Path, FieldDescriptor> entry : this.descriptorsByName.entrySet()) {
writer.println();
writer.println("| " + entry.getKey());
writer.println("| " + entry.getValue().getDescription());
writer.println("| " + entry.getValue().getType());
writer.println("| " + entry.getValue().isRequired());
writer.println("| " + entry.getValue().getConstraints());
}
writer.println("|===");
}
}

View File

@@ -0,0 +1,124 @@
/*
* 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.state;
import static java.lang.Math.min;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import org.springframework.core.style.ToStringCreator;
/**
* Representation of a path for a field. In case the field is a nested field, there will
* be multiple steps. Each step is the name of a field, albeit parents of the field in
* question. In case the field is not nested, there is only one step, which is the name of the field.
*
* @author Andreas Evers
*/
public class Path implements Comparable<Path> {
private List<String> steps = new ArrayList<>();
public Path(Path path) {
this.steps = new ArrayList<String>(path.getSteps());
}
public Path(List<String> steps) {
this.steps = steps;
}
public Path(Path previousSteps, String newStep) {
this.steps.addAll(previousSteps.getSteps());
this.steps.add(newStep);
}
public Path(String... steps) {
this.steps = Arrays.asList(steps);
}
public static Path path(List<String> steps) {
return new Path(steps);
}
public static Path path(String... steps) {
return new Path(steps);
}
public static Path path(Path previousSteps, String newStep) {
return new Path(previousSteps, newStep);
}
public List<String> getSteps() {
return this.steps;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((this.steps == null) ? 0 : this.steps.hashCode());
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
Path other = (Path) obj;
if (this.steps == null) {
if (other.steps != null) {
return false;
}
}
else if (!this.steps.equals(other.steps)) {
return false;
}
return true;
}
@Override
public String toString() {
return new ToStringCreator(this).append("steps", this.steps).toString();
}
@Override
public int compareTo(Path o) {
int comparison = 0;
int size = min(this.steps.size(), o.getSteps().size());
for (int i = 0; i < size; i++) {
String thisStep = this.steps.get(i);
String thatStep = o.getSteps().get(i);
comparison = thisStep.compareTo(thatStep);
if (comparison != 0) {
break;
}
}
if (comparison == 0) {
comparison = ((Integer) this.steps.size()).compareTo(o.getSteps().size());
}
return comparison;
}
}

View File

@@ -0,0 +1,94 @@
/*
* 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.state;
import static org.springframework.restdocs.state.FieldSnippetResultHandler.Type.REQUEST;
import static org.springframework.restdocs.state.FieldSnippetResultHandler.Type.RESPONSE;
import static org.springframework.restdocs.state.Path.path;
import java.util.Arrays;
import org.springframework.restdocs.RestDocumentationResultHandler;
/**
* Static factory methods for documenting a RESTful API's state.
*
* @author Andreas Evers
*/
public abstract class StateDocumentation {
private StateDocumentation() {
}
/**
* Creates a {@code FieldDescriptor} that describes a field with the given
* {@code path}.
*
* @param path The path of the field
* @return a {@code FieldDescriptor} ready for further configuration
* @see RestDocumentationResultHandler#withRequestFields(FieldDescriptor...)
* @see RestDocumentationResultHandler#withResponseFields(FieldDescriptor...)
*/
public static FieldDescriptor fieldWithPath(Path path) {
return new FieldDescriptor(path);
}
/**
* Creates a {@code FieldDescriptor} that describes a field with the given
* {@code path}, in case the field is at the root of the request or response body
*
* @param name The name of the field being at the root of the request or response body
* @return a {@code FieldDescriptor} ready for further configuration
* @see RestDocumentationResultHandler#withRequestFields(FieldDescriptor...)
* @see RestDocumentationResultHandler#withResponseFields(FieldDescriptor...)
*/
public static FieldDescriptor fieldWithPath(String name) {
return new FieldDescriptor(path(name));
}
/**
* Creates a {@code RequestFieldsSnippetResultHandler} that will produce a
* documentation snippet for a request's fields.
*
* @param outputDir The directory to which the snippet should be written
* @param descriptors The descriptions of the request's fields
* @return the handler
* @see RestDocumentationResultHandler#withRequestFields(FieldDescriptor...)
*/
public static FieldSnippetResultHandler documentRequestFields(String outputDir,
FieldDescriptor... descriptors) {
return new FieldSnippetResultHandler(outputDir, REQUEST,
Arrays.asList(descriptors));
}
/**
* Creates a {@code ResponseFieldsSnippetResultHandler} that will produce a
* documentation snippet for a response's fields.
*
* @param outputDir The directory to which the snippet should be written
* @param descriptors The descriptions of the response's fields
* @return the handler
* @see RestDocumentationResultHandler#withResponseFields(FieldDescriptor...)
*/
public static FieldSnippetResultHandler documentResponseFields(String outputDir,
FieldDescriptor... descriptors) {
return new FieldSnippetResultHandler(outputDir, RESPONSE,
Arrays.asList(descriptors));
}
}

View File

@@ -0,0 +1,75 @@
/*
* 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.state;
import static org.junit.Assert.fail;
import java.util.HashSet;
import java.util.Set;
import java.util.SortedSet;
import org.springframework.restdocs.state.FieldSnippetResultHandler.Type;
/**
* Validator which verifies if fields are documented correctly. All fields need to be
* documented except nested fields. For those fields it is sufficient that only the parent
* is documented. In case there are fields documented that don't appear in the actual
* request or response, the validation will fail.
*
* @author Andreas Evers
*/
public class StateDocumentationValidator {
private final Type type;
public StateDocumentationValidator(Type type) {
this.type = type;
}
public void validateFields(SortedSet<Path> actualFields,
SortedSet<Path> expectedFields) {
Set<Path> undocumentedFields = new HashSet<Path>(actualFields);
Set<Path> ignoredFields = new HashSet<Path>();
undocumentedFields.removeAll(expectedFields);
for (Path path : undocumentedFields) {
if (path.getSteps().size() > 1) {
Path wrappingPath = new Path(path);
wrappingPath.getSteps().remove(wrappingPath.getSteps().size() - 1);
if (actualFields.contains(wrappingPath)) {
ignoredFields.add(path);
}
}
}
undocumentedFields.removeAll(ignoredFields);
Set<Path> missingFields = new HashSet<Path>(expectedFields);
missingFields.removeAll(actualFields);
if (!undocumentedFields.isEmpty() || !missingFields.isEmpty()) {
String message = "";
if (!undocumentedFields.isEmpty()) {
message += "Fields with the following paths were not documented: "
+ undocumentedFields;
}
if (!missingFields.isEmpty()) {
message += "Fields with the following paths were not found in the "
+ this.type.toString().toLowerCase() + ": " + missingFields;
}
fail(message);
}
}
}

View File

@@ -0,0 +1,139 @@
/*
* 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.state;
import static org.junit.Assert.assertEquals;
import static org.springframework.restdocs.state.Path.path;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.junit.Test;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.util.FileCopyUtils;
/**
* Tests for {@link FieldExtractor}.
*
* @author Andreas Evers
*/
public class FieldExtractorTests {
private final FieldExtractor fieldExtractor = new FieldExtractor();
@Test
public void singleField() throws IOException {
Map<Path, Field> fields = this.fieldExtractor
.extractFields(createResponse("single-field"));
assertFields(Arrays.asList(new Field(path("alpha"), "alpha-value")), fields);
}
@Test
public void multipleFields() throws IOException {
Map<Path, Field> fields = this.fieldExtractor
.extractFields(createResponse("multiple-fields"));
assertFields(Arrays.asList(new Field(path("alpha"), "alpha-value"), new Field(
path("bravo"), 123), new Field(path("charlie"), createMap()), new Field(
path("delta"), createList()), new Field(path("echo"),
createListWithMaps())), fields);
}
private Map<String, Object> createMap() {
Map<String, Object> hashMap = new HashMap<>();
hashMap.put("one", 456);
hashMap.put("two", "two-value");
return hashMap;
}
private List<String> createList() {
List<String> arrayList = new ArrayList<>();
arrayList.add("delta-value-1");
arrayList.add("delta-value-2");
return arrayList;
}
private List<Map<String, Object>> createListWithMaps() {
List<Map<String, Object>> arrayList = new ArrayList<>();
Map<String, Object> hashMap1 = new HashMap<>();
hashMap1.put("one", 789);
hashMap1.put("two", "two-value");
arrayList.add(hashMap1);
Map<String, Object> hashMap2 = new HashMap<>();
hashMap2.put("one", 987);
hashMap2.put("two", "value-two");
arrayList.add(hashMap2);
return arrayList;
}
@Test
public void multipleFieldsAndLinks() throws IOException {
Map<Path, Field> fields = this.fieldExtractor
.extractFields(createResponse("multiple-fields-and-links"));
assertFields(Arrays.asList(new Field(path("beta"), "beta-value"), new Field(
path("charlie"), "charlie-value")), fields);
}
@Test
public void multipleFieldsAndEmbedded() throws IOException {
Map<Path, Field> fields = this.fieldExtractor
.extractFields(createResponse("multiple-fields-and-embedded"));
assertFields(Arrays.asList(new Field(path("beta"), "beta-value"), new Field(
path("charlie"), "charlie-value")), fields);
}
@Test
public void multipleFieldsAndEmbeddedAndLinks() throws IOException {
Map<Path, Field> fields = this.fieldExtractor
.extractFields(createResponse("multiple-fields-and-embedded-and-links"));
assertFields(Arrays.asList(new Field(path("beta"), "beta-value"), new Field(
path("charlie"), "charlie-value")), fields);
}
@Test
public void noFields() throws IOException {
Map<Path, Field> fields = this.fieldExtractor
.extractFields(createResponse("no-fields"));
assertFields(Collections.<Field> emptyList(), fields);
}
private void assertFields(List<Field> expectedFields, Map<Path, Field> actualFields) {
Map<Path, Field> expectedFieldsByName = new HashMap<>();
for (Field expectedField : expectedFields) {
expectedFieldsByName.put(expectedField.getPath(), expectedField);
}
assertEquals(expectedFieldsByName, actualFields);
}
private MockHttpServletResponse createResponse(String contentName) throws IOException {
MockHttpServletResponse response = new MockHttpServletResponse();
FileCopyUtils.copy(new FileReader(getPayloadFile(contentName)),
response.getWriter());
return response;
}
private File getPayloadFile(String name) {
return new File("src/test/resources/field-payloads/" + name + ".json");
}
}

View File

@@ -0,0 +1,93 @@
package org.springframework.restdocs.state;
import static java.util.Arrays.asList;
import static org.springframework.restdocs.state.FieldSnippetResultHandler.Type.REQUEST;
import static org.springframework.restdocs.state.Path.path;
import java.util.SortedSet;
import java.util.TreeSet;
import org.junit.After;
import org.junit.Test;
public class StateDocumentationValidatorTests {
SortedSet<Path> actualFields = new TreeSet<Path>();
SortedSet<Path> expectedFields = new TreeSet<Path>();
StateDocumentationValidator validator = new StateDocumentationValidator(REQUEST);
@After
public void cleanup() {
this.actualFields = new TreeSet<Path>();
this.expectedFields = new TreeSet<Path>();
}
@Test
public void equalFields() {
this.actualFields = new TreeSet<Path>(asList(path("alpha"), path("bravo"),
path("bravo", "marco"), path("bravo", "polo"), path("charlie"),
path("charlie", "marco"), path("charlie", "marco", "alpha")));
this.expectedFields = new TreeSet<Path>(asList(path("alpha"), path("bravo"),
path("bravo", "marco"), path("bravo", "polo"), path("charlie"),
path("charlie", "marco"), path("charlie", "marco", "alpha")));
this.validator.validateFields(this.actualFields, this.expectedFields);
}
@Test(expected = AssertionError.class)
public void sameLevelButMoreDocumented() {
this.actualFields = new TreeSet<Path>(asList(path("alpha")));
this.expectedFields = new TreeSet<Path>(asList(path("alpha"), path("bravo")));
this.validator.validateFields(this.actualFields, this.expectedFields);
}
@Test(expected = AssertionError.class)
public void sameLevelButMoreActuals() {
this.actualFields = new TreeSet<Path>(asList(path("alpha"), path("bravo")));
this.expectedFields = new TreeSet<Path>(asList(path("alpha")));
this.validator.validateFields(this.actualFields, this.expectedFields);
}
@Test(expected = AssertionError.class)
public void moreDocumentedButParentPresent() {
this.actualFields = new TreeSet<Path>(asList(path("alpha"), path("bravo"),
path("bravo", "marco"), path("bravo", "polo"), path("charlie"),
path("charlie", "marco")));
this.expectedFields = new TreeSet<Path>(asList(path("alpha"), path("bravo"),
path("bravo", "marco"), path("bravo", "polo"), path("charlie"),
path("charlie", "marco"), path("charlie", "marco", "alpha")));
this.validator.validateFields(this.actualFields, this.expectedFields);
}
@Test
public void moreActualsButParentPresent() {
this.actualFields = new TreeSet<Path>(asList(path("alpha"), path("bravo"),
path("bravo", "marco"), path("bravo", "polo"), path("charlie"),
path("charlie", "marco"), path("charlie", "marco", "alpha")));
this.expectedFields = new TreeSet<Path>(asList(path("alpha"), path("bravo"),
path("bravo", "marco"), path("charlie"), path("charlie", "marco")));
this.validator.validateFields(this.actualFields, this.expectedFields);
}
@Test
public void documentationSkippedLevel() {
this.actualFields = new TreeSet<Path>(asList(path("alpha"), path("bravo"),
path("bravo", "marco"), path("bravo", "polo"), path("charlie"),
path("charlie", "marco"), path("charlie", "marco", "alpha")));
this.expectedFields = new TreeSet<Path>(asList(path("alpha"), path("bravo"),
path("bravo", "marco"), path("bravo", "polo"), path("charlie"),
path("charlie", "marco", "alpha")));
this.validator.validateFields(this.actualFields, this.expectedFields);
}
@Test(expected = AssertionError.class)
public void moreActualsWithoutParentPresent() {
this.actualFields = new TreeSet<Path>(asList(path("alpha"), path("bravo"),
path("bravo", "marco"), path("bravo", "polo"), path("charlie")));
this.expectedFields = new TreeSet<Path>(asList(path("alpha"), path("bravo"),
path("bravo", "marco"), path("bravo", "polo"), path("charlie"),
path("charlie", "marco"), path("charlie", "marco", "alpha")));
this.validator.validateFields(this.actualFields, this.expectedFields);
}
}

View File

@@ -0,0 +1,9 @@
{
"_links": {
"alpha": "http://alpha.example.com",
"bravo": "http://bravo.example.com"
},
"_embedded": "embedded-test",
"beta": "beta-value",
"charlie": "charlie-value"
}

View File

@@ -0,0 +1,5 @@
{
"_embedded": "embedded-test",
"beta": "beta-value",
"charlie": "charlie-value"
}

View File

@@ -0,0 +1,8 @@
{
"_links": {
"alpha": "http://alpha.example.com",
"bravo": "http://bravo.example.com"
},
"beta": "beta-value",
"charlie": "charlie-value"
}

View File

@@ -0,0 +1,19 @@
{
"alpha": "alpha-value",
"bravo": 123,
"charlie": {
"one": 456,
"two": "two-value"
},
"delta": [
"delta-value-1",
"delta-value-2"
],
"echo": [{
"one": 789,
"two": "two-value"
},{
"one": 987,
"two": "value-two"
}]
}

View File

@@ -0,0 +1 @@
{ }

View File

@@ -0,0 +1,3 @@
{
"alpha": "alpha-value"
}