From 561f3b2fbfbb6c727d629e34b0e5e31680e1ea6a Mon Sep 17 00:00:00 2001 From: Dave Syer Date: Wed, 21 Jun 2017 09:12:35 +0100 Subject: [PATCH] Add wiremock verification --- pom.xml | 2 +- spring-cloud-netflix-eureka-server/pom.xml | 9 +- .../doc/AbstractDocumentationTests.java | 3 +- .../eureka/server/doc/EmptyAppsTests.java | 18 +- .../server/doc/RequestVerifierFilter.java | 301 ++++++++++++++++++ 5 files changed, 321 insertions(+), 12 deletions(-) create mode 100644 spring-cloud-netflix-eureka-server/src/test/java/org/springframework/cloud/netflix/eureka/server/doc/RequestVerifierFilter.java diff --git a/pom.xml b/pom.xml index 16b684e2..8e05f517 100644 --- a/pom.xml +++ b/pom.xml @@ -10,7 +10,7 @@ org.springframework.cloud spring-cloud-build - 1.3.5.BUILD-SNAPSHOT + 1.3.5.RELEASE diff --git a/spring-cloud-netflix-eureka-server/pom.xml b/spring-cloud-netflix-eureka-server/pom.xml index 7a3ab77d..3e34f9c7 100644 --- a/spring-cloud-netflix-eureka-server/pom.xml +++ b/spring-cloud-netflix-eureka-server/pom.xml @@ -151,7 +151,7 @@ - copy-resources + copy-docs prepare-package copy-resources @@ -220,13 +220,6 @@ - - - org.springframework.restdocs - spring-restdocs-asciidoctor - 1.1.3.RELEASE - - diff --git a/spring-cloud-netflix-eureka-server/src/test/java/org/springframework/cloud/netflix/eureka/server/doc/AbstractDocumentationTests.java b/spring-cloud-netflix-eureka-server/src/test/java/org/springframework/cloud/netflix/eureka/server/doc/AbstractDocumentationTests.java index 597c093d..05beb064 100644 --- a/spring-cloud-netflix-eureka-server/src/test/java/org/springframework/cloud/netflix/eureka/server/doc/AbstractDocumentationTests.java +++ b/spring-cloud-netflix-eureka-server/src/test/java/org/springframework/cloud/netflix/eureka/server/doc/AbstractDocumentationTests.java @@ -85,7 +85,8 @@ public abstract class AbstractDocumentationTests { protected RequestSpecification assure(String name, Object body) { RestDocumentationFilter filter = filter(name); - return RestAssured.given(document(body, filter)).filter(filter); + RequestSpecification assured = RestAssured.given(document(body, filter)); + return assured.filter(filter); } protected RequestSpecification assure(String name) { diff --git a/spring-cloud-netflix-eureka-server/src/test/java/org/springframework/cloud/netflix/eureka/server/doc/EmptyAppsTests.java b/spring-cloud-netflix-eureka-server/src/test/java/org/springframework/cloud/netflix/eureka/server/doc/EmptyAppsTests.java index 2813a041..f3c9de15 100644 --- a/spring-cloud-netflix-eureka-server/src/test/java/org/springframework/cloud/netflix/eureka/server/doc/EmptyAppsTests.java +++ b/spring-cloud-netflix-eureka-server/src/test/java/org/springframework/cloud/netflix/eureka/server/doc/EmptyAppsTests.java @@ -16,6 +16,7 @@ package org.springframework.cloud.netflix.eureka.server.doc; +import com.github.tomakehurst.wiremock.client.WireMock; import com.netflix.appinfo.ApplicationInfoManager; import org.junit.Test; @@ -27,6 +28,7 @@ import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.Matchers.emptyIterable; +import static org.springframework.cloud.netflix.eureka.server.doc.RequestVerifierFilter.verify; @RunWith(SpringJUnit4ClassRunner.class) public class EmptyAppsTests extends AbstractDocumentationTests { @@ -42,8 +44,20 @@ public class EmptyAppsTests extends AbstractDocumentationTests { instanceConfig.setInstanceId("unique-id"); instanceConfig.setHostname("foo.example.com"); applicationInfoManager.initComponent(instanceConfig); - assure("add-app", applicationInfoManager.getInfo()).when() - .post("/eureka/apps/FOO").then().assertThat().statusCode(is(204)); + assure("add-app", applicationInfoManager.getInfo()) + .filter(verify("$.instance.app").json("$.instance.hostName") + .json("$.instance[?(@.status=='STARTING')]") + .json("$.instance.instanceId") + .json("$.instance.dataCenterInfo.name")) + .when().post("/eureka/apps/FOO").then().assertThat().statusCode(is(204)); + assure("up-app") + .filter(verify( + WireMock.put(WireMock.urlPathMatching("/eureka/apps/FOO/.*")) + .withQueryParam("value", WireMock.matching("UP")))) + .when() + .put("/eureka/apps/FOO/{id}/status?value={value}", + applicationInfoManager.getInfo().getInstanceId(), "UP") + .then().assertThat().statusCode(is(200)); assure("delete-app").when() .delete("/eureka/apps/FOO/{id}", applicationInfoManager.getInfo().getInstanceId()) diff --git a/spring-cloud-netflix-eureka-server/src/test/java/org/springframework/cloud/netflix/eureka/server/doc/RequestVerifierFilter.java b/spring-cloud-netflix-eureka-server/src/test/java/org/springframework/cloud/netflix/eureka/server/doc/RequestVerifierFilter.java new file mode 100644 index 00000000..e92c70e1 --- /dev/null +++ b/spring-cloud-netflix-eureka-server/src/test/java/org/springframework/cloud/netflix/eureka/server/doc/RequestVerifierFilter.java @@ -0,0 +1,301 @@ +/* + * Copyright 2016-2017 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.cloud.netflix.eureka.server.doc; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import com.github.tomakehurst.wiremock.client.MappingBuilder; +import com.github.tomakehurst.wiremock.client.ResponseDefinitionBuilder; +import com.github.tomakehurst.wiremock.http.ContentTypeHeader; +import com.github.tomakehurst.wiremock.http.Cookie; +import com.github.tomakehurst.wiremock.http.HttpHeader; +import com.github.tomakehurst.wiremock.http.HttpHeaders; +import com.github.tomakehurst.wiremock.http.QueryParameter; +import com.github.tomakehurst.wiremock.http.Request; +import com.github.tomakehurst.wiremock.http.RequestMethod; +import com.github.tomakehurst.wiremock.matching.MatchResult; +import com.github.tomakehurst.wiremock.stubbing.StubMapping; +import com.jayway.jsonpath.JsonPath; +import com.jayway.restassured.filter.Filter; +import com.jayway.restassured.filter.FilterContext; +import com.jayway.restassured.response.Header; +import com.jayway.restassured.response.Response; +import com.jayway.restassured.specification.FilterableRequestSpecification; +import com.jayway.restassured.specification.FilterableResponseSpecification; + +import org.springframework.util.Base64Utils; +import org.springframework.util.ObjectUtils; +import org.springframework.util.StringUtils; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Dave Syer + * + */ +public class RequestVerifierFilter implements Filter { + + static final String CONTEXT_KEY_CONFIGURATION = "org.springframework.restdocs.configuration"; + private Map jsonPaths = new LinkedHashMap<>(); + private MappingBuilder builder; + + public static RequestVerifierFilter verify(String path) { + return new RequestVerifierFilter(path); + } + + public static RequestVerifierFilter verify(MappingBuilder builder) { + return new RequestVerifierFilter().wiremock(builder); + } + + private RequestVerifierFilter(String expression, Object... args) { + expression = String.format(expression, args); + this.jsonPaths.put(expression, JsonPath.compile(expression)); + } + + private RequestVerifierFilter() { + } + + public RequestVerifierFilter json(String expression, Object... args) { + expression = String.format(expression, args); + this.jsonPaths.put(expression, JsonPath.compile(expression)); + return this; + } + + public RequestVerifierFilter wiremock(MappingBuilder builder) { + this.builder = builder; + return this; + } + + @Override + public Response filter(FilterableRequestSpecification requestSpec, + FilterableResponseSpecification responseSpec, FilterContext context) { + Map configuration = getConfiguration(requestSpec, context); + configuration.put("contract.jsonPaths", this.jsonPaths.keySet()); + Response response = context.next(requestSpec, responseSpec); + if (requestSpec.getBody() != null && !this.jsonPaths.isEmpty()) { + String actual = new String((byte[]) requestSpec.getBody()); + for (JsonPath jsonPath : this.jsonPaths.values()) { + new JsonPathValue(jsonPath, actual).assertHasValue(Object.class, + "an object"); + } + } + if (this.builder != null) { + this.builder.willReturn(getResponseDefinition(response)); + StubMapping stubMapping = this.builder.build(); + MatchResult match = stubMapping.getRequest() + .match(new WireMockRestAssuredRequestAdapter(requestSpec)); + assertThat(match.isExactMatch()).as("wiremock did not match request") + .isTrue(); + configuration.put("contract.stubMapping", stubMapping); + } + return response; + } + + private ResponseDefinitionBuilder getResponseDefinition(Response response) { + ResponseDefinitionBuilder definition = ResponseDefinitionBuilder + .responseDefinition().withBody(response.getBody().asString()) + .withStatus(response.getStatusCode()); + addResponseHeaders(definition, response); + return definition; + } + + private void addResponseHeaders(ResponseDefinitionBuilder definition, + Response input) { + for (Header header : input.getHeaders().asList()) { + String name = header.getName(); + definition.withHeader(name, input.getHeader(name)); + } + } + + protected Map getConfiguration( + FilterableRequestSpecification requestSpec, FilterContext context) { + Map configuration = context + .>getValue(CONTEXT_KEY_CONFIGURATION); + return configuration; + } +} + +class JsonPathValue { + + private final JsonPath jsonPath; + private final String expression; + private final CharSequence actual; + + JsonPathValue(JsonPath jsonPath, CharSequence actual) { + this.jsonPath = jsonPath; + this.actual = actual; + this.expression = jsonPath.getPath(); + } + + public void assertHasValue(Class type, String expectedDescription) { + Object value = getValue(true); + if (value == null || isIndefiniteAndEmpty()) { + throw new AssertionError(getNoValueMessage()); + } + if (type != null && !type.isInstance(value)) { + throw new AssertionError(getExpectedValueMessage(expectedDescription)); + } + } + + private boolean isIndefiniteAndEmpty() { + return !isDefinite() && isEmpty(); + } + + private boolean isDefinite() { + return this.jsonPath.isDefinite(); + } + + private boolean isEmpty() { + return ObjectUtils.isEmpty(getValue(false)); + } + + public Object getValue(boolean required) { + try { + CharSequence json = this.actual; + return this.jsonPath.read(json == null ? null : json.toString()); + } + catch (Exception ex) { + if (!required) { + return null; + } + throw new AssertionError(getNoValueMessage() + ". " + ex.getMessage()); + } + } + + private String getNoValueMessage() { + return "No value at JSON path \"" + this.expression + "\""; + } + + private String getExpectedValueMessage(String expectedDescription) { + return String.format("Expected %s at JSON path \"%s\" but found: %s", + expectedDescription, this.expression, + ObjectUtils.nullSafeToString(StringUtils.quoteIfString(getValue(false)))); + } + +} + +class WireMockRestAssuredRequestAdapter implements Request { + + private FilterableRequestSpecification request; + + public WireMockRestAssuredRequestAdapter(FilterableRequestSpecification request) { + this.request = request; + } + + @Override + public String getUrl() { + return request.getDerivedPath(); + } + + @Override + public String getAbsoluteUrl() { + return request.getURI(); + } + + @Override + public RequestMethod getMethod() { + return RequestMethod.fromString(request.getMethod().name()); + } + + @Override + public String getClientIp() { + return "127.0.0.1"; + } + + @Override + public String getHeader(String key) { + return request.getHeaders().getValue(key); + } + + @Override + public HttpHeader header(String key) { + return new HttpHeader(key, request.getHeaders().getValue(key)); + } + + @Override + public ContentTypeHeader contentTypeHeader() { + return new ContentTypeHeader(request.getContentType()); + } + + @Override + public HttpHeaders getHeaders() { + List headers = new ArrayList<>(); + for (Header header : request.getHeaders()) { + headers.add(new HttpHeader(header.getName(), header.getValue())); + } + return new HttpHeaders(headers); + } + + @Override + public boolean containsHeader(String key) { + return request.getHeaders().hasHeaderWithName(key); + } + + @Override + public Set getAllHeaderKeys() { + Set headers = new LinkedHashSet<>(); + for (Header header : request.getHeaders()) { + headers.add(header.getName()); + } + return headers; + } + + @Override + public Map getCookies() { + Map map = new LinkedHashMap<>(); + for (com.jayway.restassured.response.Cookie cookie : request.getCookies()) { + Cookie value = new Cookie(cookie.getValue()); + map.put(cookie.getName(), value); + } + return map; + } + + @Override + public QueryParameter queryParameter(String key) { + Map params = request.getQueryParams(); + if (params.containsKey(key)) { + return new QueryParameter(key, Arrays.asList(params.get(key))); + } + return null; + } + + @Override + public byte[] getBody() { + return request.getBody(); + } + + @Override + public String getBodyAsString() { + return new String(getBody()); + } + + @Override + public String getBodyAsBase64() { + return Base64Utils.encodeToString(getBody()); + } + + @Override + public boolean isBrowserProxyRequest() { + return false; + } + +}