From d3f6ad90a8b907bf7eecf807d6cc58984322f8bd Mon Sep 17 00:00:00 2001 From: Dave Syer Date: Fri, 21 Apr 2017 13:26:51 +0100 Subject: [PATCH] Add a fault injector if server is jetty or tomcat Tomcat sends a slightly different reponse and the client sees it differently in the malformed chunk scenario. It would be good to have the same features for Undertow. See gh-270. --- .../WiremockServerApplicationTests.java | 49 ++++++++-- .../WiremockServerApplicationTests.java | 50 ++++++++-- .../wiremock/SpringBootHttpServerFactory.java | 56 ++++++++--- .../wiremock/TomcatFaultInjector.java | 95 +++++++++++++++++++ .../wiremock/TomcatFaultInjectorFactory.java | 37 ++++++++ .../WiremockServerApplicationTests.java | 15 +-- 6 files changed, 269 insertions(+), 33 deletions(-) create mode 100644 spring-cloud-contract-wiremock/src/main/java/org/springframework/cloud/contract/wiremock/TomcatFaultInjector.java create mode 100644 spring-cloud-contract-wiremock/src/main/java/org/springframework/cloud/contract/wiremock/TomcatFaultInjectorFactory.java diff --git a/samples/wiremock-jetty/src/test/java/com/example/WiremockServerApplicationTests.java b/samples/wiremock-jetty/src/test/java/com/example/WiremockServerApplicationTests.java index 6c6f1e6da9..9bbb1671d6 100644 --- a/samples/wiremock-jetty/src/test/java/com/example/WiremockServerApplicationTests.java +++ b/samples/wiremock-jetty/src/test/java/com/example/WiremockServerApplicationTests.java @@ -1,14 +1,17 @@ package com.example; -import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; -import static com.github.tomakehurst.wiremock.client.WireMock.get; -import static com.github.tomakehurst.wiremock.client.WireMock.stubFor; -import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo; -import static org.assertj.core.api.Assertions.assertThat; +import com.github.tomakehurst.wiremock.http.Fault; +import com.github.tomakehurst.wiremock.junit.WireMockClassRule; +import org.apache.http.MalformedChunkCodingException; +import org.apache.http.NoHttpResponseException; +import org.apache.http.client.ClientProtocolException; import org.junit.ClassRule; +import org.junit.Rule; import org.junit.Test; +import org.junit.rules.ExpectedException; import org.junit.runner.RunWith; + import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; @@ -16,7 +19,12 @@ import org.springframework.cloud.contract.wiremock.WireMockSpring; import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.context.junit4.SpringRunner; -import com.github.tomakehurst.wiremock.junit.WireMockClassRule; +import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; +import static com.github.tomakehurst.wiremock.client.WireMock.get; +import static com.github.tomakehurst.wiremock.client.WireMock.stubFor; +import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo; +import static org.assertj.core.api.Assertions.assertThat; +import static org.hamcrest.CoreMatchers.instanceOf; @RunWith(SpringRunner.class) @SpringBootTest(properties="app.baseUrl=http://localhost:6061", webEnvironment=WebEnvironment.NONE) @@ -26,14 +34,41 @@ public class WiremockServerApplicationTests { @ClassRule public static WireMockClassRule wiremock = new WireMockClassRule(WireMockSpring.options().port(6061)); + @Rule + public ExpectedException expected = ExpectedException.none(); + @Autowired private Service service; @Test - public void contextLoads() throws Exception { + public void hello() throws Exception { stubFor(get(urlEqualTo("/resource")) .willReturn(aResponse().withHeader("Content-Type", "text/plain").withBody("Hello World!"))); assertThat(this.service.go()).isEqualTo("Hello World!"); } + @Test + public void randomData() throws Exception { + stubFor(get(urlEqualTo("/resource")) + .willReturn(aResponse().withFault(Fault.RANDOM_DATA_THEN_CLOSE))); + expected.expectCause(instanceOf(ClientProtocolException.class)); + assertThat(this.service.go()).isEqualTo("Oops!"); + } + + @Test + public void emptyResponse() throws Exception { + stubFor(get(urlEqualTo("/resource")) + .willReturn(aResponse().withFault(Fault.EMPTY_RESPONSE))); + expected.expectCause(instanceOf(NoHttpResponseException.class)); + assertThat(this.service.go()).isEqualTo("Oops!"); + } + + @Test + public void malformed() throws Exception { + stubFor(get(urlEqualTo("/resource")) + .willReturn(aResponse().withFault(Fault.MALFORMED_RESPONSE_CHUNK))); + expected.expectCause(instanceOf(MalformedChunkCodingException.class)); + assertThat(this.service.go()).isEqualTo("Oops!"); + } + } diff --git a/samples/wiremock-tomcat/src/test/java/com/example/WiremockServerApplicationTests.java b/samples/wiremock-tomcat/src/test/java/com/example/WiremockServerApplicationTests.java index d7f2980b6f..d4c5656527 100644 --- a/samples/wiremock-tomcat/src/test/java/com/example/WiremockServerApplicationTests.java +++ b/samples/wiremock-tomcat/src/test/java/com/example/WiremockServerApplicationTests.java @@ -1,14 +1,18 @@ package com.example; -import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; -import static com.github.tomakehurst.wiremock.client.WireMock.get; -import static com.github.tomakehurst.wiremock.client.WireMock.stubFor; -import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo; -import static org.assertj.core.api.Assertions.assertThat; +import java.io.IOException; +import com.github.tomakehurst.wiremock.http.Fault; +import com.github.tomakehurst.wiremock.junit.WireMockClassRule; + +import org.apache.http.NoHttpResponseException; +import org.apache.http.client.ClientProtocolException; import org.junit.ClassRule; +import org.junit.Rule; import org.junit.Test; +import org.junit.rules.ExpectedException; import org.junit.runner.RunWith; + import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; @@ -16,7 +20,12 @@ import org.springframework.cloud.contract.wiremock.WireMockSpring; import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.context.junit4.SpringRunner; -import com.github.tomakehurst.wiremock.junit.WireMockClassRule; +import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; +import static com.github.tomakehurst.wiremock.client.WireMock.get; +import static com.github.tomakehurst.wiremock.client.WireMock.stubFor; +import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo; +import static org.assertj.core.api.Assertions.assertThat; +import static org.hamcrest.CoreMatchers.instanceOf; @RunWith(SpringRunner.class) @SpringBootTest(properties="app.baseUrl=http://localhost:6067", webEnvironment=WebEnvironment.NONE) @@ -26,6 +35,9 @@ public class WiremockServerApplicationTests { @ClassRule public static WireMockClassRule wiremock = new WireMockClassRule(WireMockSpring.options().port(6067)); + @Rule + public ExpectedException expected = ExpectedException.none(); + @Autowired private Service service; @@ -36,4 +48,30 @@ public class WiremockServerApplicationTests { assertThat(this.service.go()).isEqualTo("Hello World!"); } + @Test + public void randomData() throws Exception { + stubFor(get(urlEqualTo("/resource")) + .willReturn(aResponse().withFault(Fault.RANDOM_DATA_THEN_CLOSE))); + expected.expectCause(instanceOf(ClientProtocolException.class)); + assertThat(this.service.go()).isEqualTo("Oops!"); + } + + @Test + public void emptyResponse() throws Exception { + stubFor(get(urlEqualTo("/resource")) + .willReturn(aResponse().withFault(Fault.EMPTY_RESPONSE))); + expected.expectCause(instanceOf(NoHttpResponseException.class)); + assertThat(this.service.go()).isEqualTo("Oops!"); + } + + @Test + public void malformed() throws Exception { + stubFor(get(urlEqualTo("/resource")) + .willReturn(aResponse().withFault(Fault.MALFORMED_RESPONSE_CHUNK))); + // It's a different exception type than Jetty, but it's in the right ballpark + expected.expectCause(instanceOf(IOException.class)); + expected.expectMessage("chunk"); + assertThat(this.service.go()).isEqualTo("Oops!"); + } + } diff --git a/spring-cloud-contract-wiremock/src/main/java/org/springframework/cloud/contract/wiremock/SpringBootHttpServerFactory.java b/spring-cloud-contract-wiremock/src/main/java/org/springframework/cloud/contract/wiremock/SpringBootHttpServerFactory.java index ecf36be8cb..3c4fc2f84c 100644 --- a/spring-cloud-contract-wiremock/src/main/java/org/springframework/cloud/contract/wiremock/SpringBootHttpServerFactory.java +++ b/spring-cloud-contract-wiremock/src/main/java/org/springframework/cloud/contract/wiremock/SpringBootHttpServerFactory.java @@ -18,11 +18,28 @@ package org.springframework.cloud.contract.wiremock; import javax.servlet.ServletContext; +import io.undertow.Undertow.Builder; + +import com.github.tomakehurst.wiremock.common.HttpsSettings; +import com.github.tomakehurst.wiremock.common.Notifier; +import com.github.tomakehurst.wiremock.core.Options; +import com.github.tomakehurst.wiremock.core.WireMockApp; +import com.github.tomakehurst.wiremock.http.AdminRequestHandler; +import com.github.tomakehurst.wiremock.http.HttpServer; +import com.github.tomakehurst.wiremock.http.HttpServerFactory; +import com.github.tomakehurst.wiremock.http.RequestHandler; +import com.github.tomakehurst.wiremock.http.StubRequestHandler; +import com.github.tomakehurst.wiremock.jetty9.JettyFaultInjectorFactory; +import com.github.tomakehurst.wiremock.servlet.FaultInjectorFactory; +import com.github.tomakehurst.wiremock.servlet.NoFaultInjectorFactory; +import com.github.tomakehurst.wiremock.servlet.WireMockHandlerDispatchingServlet; + import org.apache.catalina.connector.Connector; import org.eclipse.jetty.server.ConnectionFactory; import org.eclipse.jetty.server.HttpConfiguration; import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.ServerConnector; + import org.springframework.beans.BeansException; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.config.BeanPostProcessor; @@ -62,21 +79,9 @@ import org.springframework.context.annotation.Import; import org.springframework.context.event.EventListener; import org.springframework.context.support.GenericApplicationContext; import org.springframework.stereotype.Component; +import org.springframework.util.ClassUtils; import org.springframework.web.context.ServletContextAware; -import com.github.tomakehurst.wiremock.common.HttpsSettings; -import com.github.tomakehurst.wiremock.common.Notifier; -import com.github.tomakehurst.wiremock.core.Options; -import com.github.tomakehurst.wiremock.core.WireMockApp; -import com.github.tomakehurst.wiremock.http.AdminRequestHandler; -import com.github.tomakehurst.wiremock.http.HttpServer; -import com.github.tomakehurst.wiremock.http.HttpServerFactory; -import com.github.tomakehurst.wiremock.http.RequestHandler; -import com.github.tomakehurst.wiremock.http.StubRequestHandler; -import com.github.tomakehurst.wiremock.servlet.WireMockHandlerDispatchingServlet; - -import io.undertow.Undertow.Builder; - /** * @author Dave Syer * @@ -258,6 +263,8 @@ class WiremockServerConfiguration { @Autowired private StubRequestHandler stubRequestHandler; @Autowired + private FaultInjectorFactory faultInjectorFactory; + @Autowired private Options options; @Bean(name = DispatcherServletAutoConfiguration.DEFAULT_DISPATCHER_SERVLET_REGISTRATION_BEAN_NAME) @@ -265,6 +272,10 @@ class WiremockServerConfiguration { ServletRegistrationBean reg = new ServletRegistrationBean(); reg.addInitParameter(RequestHandler.HANDLER_CLASS_KEY, StubRequestHandler.class.getName()); + if (WiremockServerConfiguration.this.faultInjectorFactory != null) { + reg.addInitParameter(FaultInjectorFactory.INJECTOR_CLASS_KEY, + FaultInjectorFactory.class.getName()); + } reg.setServlet(new WireMockHandlerDispatchingServlet()); reg.setName("stub"); reg.addUrlMappings("/"); @@ -293,6 +304,10 @@ class WiremockServerConfiguration { WiremockServerConfiguration.this.stubRequestHandler); servletContext.setAttribute(Notifier.KEY, WiremockServerConfiguration.this.options.notifier()); + if (WiremockServerConfiguration.this.faultInjectorFactory != null) { + servletContext.setAttribute(FaultInjectorFactory.class.getName(), + WiremockServerConfiguration.this.faultInjectorFactory); + } } }; } @@ -367,6 +382,11 @@ class ContainerConfiguration { return tomcat; } + @Bean + public FaultInjectorFactory faultInjectorFactory() { + return new TomcatFaultInjectorFactory(); + } + @EventListener public void serverUp(EmbeddedServletContainerInitializedEvent event) { if (this.connector != null) { @@ -417,6 +437,11 @@ class ContainerConfiguration { return undertow; } + @Bean + public FaultInjectorFactory faultInjectorFactory() { + return new NoFaultInjectorFactory(); + } + @EventListener public void serverUp(EmbeddedServletContainerInitializedEvent event) { if (this.port != null) { @@ -457,6 +482,11 @@ class ContainerConfiguration { return jetty; } + @Bean + public JettyFaultInjectorFactory faultInjectorFactory() { + return new JettyFaultInjectorFactory(); + } + private org.eclipse.jetty.server.Connector createStandardConnector( Server server) { ServerConnector connector = new ServerConnector(server, -1, -1); diff --git a/spring-cloud-contract-wiremock/src/main/java/org/springframework/cloud/contract/wiremock/TomcatFaultInjector.java b/spring-cloud-contract-wiremock/src/main/java/org/springframework/cloud/contract/wiremock/TomcatFaultInjector.java new file mode 100644 index 0000000000..e7b4000873 --- /dev/null +++ b/spring-cloud-contract-wiremock/src/main/java/org/springframework/cloud/contract/wiremock/TomcatFaultInjector.java @@ -0,0 +1,95 @@ +/* + * 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.contract.wiremock; + +import java.io.IOException; +import java.lang.reflect.Field; +import java.nio.ByteBuffer; + +import javax.servlet.http.HttpServletResponse; + +import com.github.tomakehurst.wiremock.common.Exceptions; +import com.github.tomakehurst.wiremock.core.FaultInjector; +import com.google.common.base.Charsets; + +import org.apache.coyote.Response; +import org.apache.tomcat.util.net.SocketWrapperBase; + +import org.springframework.util.ReflectionUtils; + +import static com.github.tomakehurst.wiremock.common.Exceptions.throwUnchecked; + +/** + * @author Dave Syer + * + */ +public class TomcatFaultInjector implements FaultInjector { + + private static final byte[] GARBAGE = "lskdu018973t09sylgasjkfg1][]'./.sdlv" + .getBytes(Charsets.UTF_8); + private final Response response; + private SocketWrapperBase socket; + + public TomcatFaultInjector(HttpServletResponse response) { + this.response = ((org.apache.catalina.connector.Response) getField(response, + "response")).getCoyoteResponse(); + this.socket = (SocketWrapperBase) getField( + getField(this.response, "outputBuffer"), "socketWrapper"); + } + + private Object getField(Object target, String string) { + Field field = ReflectionUtils.findField(target.getClass(), string); + ReflectionUtils.makeAccessible(field); + return ReflectionUtils.getField(field, target); + } + + @Override + public void emptyResponseAndCloseConnection() { + try { + socket.close(); + } + catch (IOException e) { + Exceptions.throwUnchecked(e); + } + } + + @Override + public void malformedResponseChunk() { + try { + response.sendHeaders(); + response.doWrite(ByteBuffer.wrap(GARBAGE)); + socket.flush(true); + socket.close(); + } + catch (IOException e) { + throwUnchecked(e); + } + } + + @Override + public void randomDataAndCloseConnection() { + try { + socket.write(true, GARBAGE, 0, GARBAGE.length); + socket.flush(true); + socket.close(); + } + catch (IOException e) { + throwUnchecked(e); + } + } + +} diff --git a/spring-cloud-contract-wiremock/src/main/java/org/springframework/cloud/contract/wiremock/TomcatFaultInjectorFactory.java b/spring-cloud-contract-wiremock/src/main/java/org/springframework/cloud/contract/wiremock/TomcatFaultInjectorFactory.java new file mode 100644 index 0000000000..17cbcaea2d --- /dev/null +++ b/spring-cloud-contract-wiremock/src/main/java/org/springframework/cloud/contract/wiremock/TomcatFaultInjectorFactory.java @@ -0,0 +1,37 @@ +/* + * 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.contract.wiremock; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import com.github.tomakehurst.wiremock.core.FaultInjector; +import com.github.tomakehurst.wiremock.servlet.FaultInjectorFactory; + +/** + * @author Dave Syer + * + */ +public class TomcatFaultInjectorFactory implements FaultInjectorFactory { + + @Override + public FaultInjector buildFaultInjector(HttpServletRequest httpServletRequest, + HttpServletResponse httpServletResponse) { + return new TomcatFaultInjector(httpServletResponse); + } + +} diff --git a/spring-cloud-contract-wiremock/src/test/java/org/springframework/cloud/contract/wiremock/WiremockServerApplicationTests.java b/spring-cloud-contract-wiremock/src/test/java/org/springframework/cloud/contract/wiremock/WiremockServerApplicationTests.java index 34d7f57f9e..dd6a002d33 100644 --- a/spring-cloud-contract-wiremock/src/test/java/org/springframework/cloud/contract/wiremock/WiremockServerApplicationTests.java +++ b/spring-cloud-contract-wiremock/src/test/java/org/springframework/cloud/contract/wiremock/WiremockServerApplicationTests.java @@ -1,21 +1,22 @@ package org.springframework.cloud.contract.wiremock; -import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; -import static com.github.tomakehurst.wiremock.client.WireMock.get; -import static com.github.tomakehurst.wiremock.client.WireMock.stubFor; -import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo; -import static org.assertj.core.api.Assertions.assertThat; +import com.github.tomakehurst.wiremock.junit.WireMockClassRule; import org.junit.ClassRule; import org.junit.Test; import org.junit.runner.RunWith; + import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.context.junit4.SpringRunner; -import com.github.tomakehurst.wiremock.junit.WireMockClassRule; +import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; +import static com.github.tomakehurst.wiremock.client.WireMock.get; +import static com.github.tomakehurst.wiremock.client.WireMock.stubFor; +import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo; +import static org.assertj.core.api.Assertions.assertThat; @RunWith(SpringRunner.class) @SpringBootTest(classes=WiremockTestsApplication.class, properties="app.baseUrl=http://localhost:8080", webEnvironment=WebEnvironment.NONE) @@ -29,7 +30,7 @@ public class WiremockServerApplicationTests { private Service service; @Test - public void contextLoads() throws Exception { + public void hello() throws Exception { stubFor(get(urlEqualTo("/test")) .willReturn(aResponse().withHeader("Content-Type", "text/plain").withBody("Hello World!"))); assertThat(this.service.go()).isEqualTo("Hello World!");