Add Wiremock features (Tomcat only for now)

User can depend on spring-cloud-contract-wiremock and
spring-cloud-starter-web (or tomcat) to get tomcat to run
the wiremock server. The idea is that wiremock runs in the
"ambient" server from Sprign Boot. So far we only support
Tomcat (should be easy to extend).

See tests in spring-cloud-contract-wiremock for details.
This commit is contained in:
Dave Syer
2016-07-12 18:08:24 +01:00
parent e28bf1f66e
commit 5fe510f45e
28 changed files with 977 additions and 122 deletions

View File

@@ -0,0 +1,250 @@
/*
* Copyright 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.cloud.contract.wiremock;
import javax.servlet.ServletContext;
import org.apache.catalina.connector.Connector;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
import org.springframework.boot.Banner.Mode;
import org.springframework.boot.autoconfigure.PropertyPlaceholderAutoConfiguration;
import org.springframework.boot.autoconfigure.context.ConfigurationPropertiesAutoConfiguration;
import org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration;
import org.springframework.boot.autoconfigure.web.DispatcherServletAutoConfiguration;
import org.springframework.boot.autoconfigure.web.EmbeddedServletContainerAutoConfiguration.BeanPostProcessorsRegistrar;
import org.springframework.boot.autoconfigure.web.HttpMessageConvertersAutoConfiguration;
import org.springframework.boot.autoconfigure.web.ServerProperties;
import org.springframework.boot.autoconfigure.web.ServerPropertiesAutoConfiguration;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.boot.context.embedded.EmbeddedServletContainerFactory;
import org.springframework.boot.context.embedded.Ssl;
import org.springframework.boot.context.embedded.tomcat.TomcatEmbeddedServletContainerFactory;
import org.springframework.boot.context.event.ApplicationPreparedEvent;
import org.springframework.boot.web.servlet.ServletRegistrationBean;
import org.springframework.context.ApplicationListener;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.context.support.GenericApplicationContext;
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;
/**
* @author Dave Syer
*
*/
public class SpringBootHttpServerFactory implements HttpServerFactory {
@Override
public HttpServer buildHttpServer(Options options,
AdminRequestHandler adminRequestHandler,
StubRequestHandler stubRequestHandler) {
return new SpringBootHttpServer(options, adminRequestHandler, stubRequestHandler);
}
}
class SpringBootHttpServer
implements HttpServer, ApplicationListener<ApplicationPreparedEvent> {
private volatile boolean running;
private Options options;
private AdminRequestHandler adminRequestHandler;
private StubRequestHandler stubRequestHandler;
private ConfigurableApplicationContext context;
public SpringBootHttpServer(Options options, AdminRequestHandler adminRequestHandler,
StubRequestHandler stubRequestHandler) {
this.options = options;
this.adminRequestHandler = adminRequestHandler;
this.stubRequestHandler = stubRequestHandler;
}
@Override
public void start() {
this.context = new SpringApplicationBuilder(WiremockServerConfiguration.class)
.logStartupInfo(false).bannerMode(Mode.OFF).listeners(this).run();
this.running = true;
}
@Override
public void stop() {
if (this.context != null) {
this.context.close();
}
this.running = false;
}
@Override
public boolean isRunning() {
return this.running;
}
@Override
public int port() {
return this.options.portNumber();
}
@Override
public int httpsPort() {
return this.options.httpsSettings().port();
}
@Override
public void onApplicationEvent(ApplicationPreparedEvent event) {
GenericApplicationContext context = (GenericApplicationContext) event
.getApplicationContext();
DefaultListableBeanFactory beanFactory = context.getDefaultListableBeanFactory();
if (beanFactory.containsBean("wireMockOptions")) {
// Be idempotent (the event might be emitted more than once)
return;
}
beanFactory.registerSingleton("wireMockOptions", this.options);
beanFactory.registerSingleton("adminRequestHandler", this.adminRequestHandler);
beanFactory.registerSingleton("stubRequestHandler", this.stubRequestHandler);
beanFactory.addBeanPostProcessor(new ServerPropertiesPostProcessor());
}
class ServerPropertiesPostProcessor implements BeanPostProcessor {
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName)
throws BeansException {
return bean;
}
@Override
public Object postProcessAfterInitialization(Object bean, String beanName)
throws BeansException {
if (bean instanceof ServerProperties) {
ServerProperties server = (ServerProperties) bean;
server.setPort(getPort());
setupHttps(server, SpringBootHttpServer.this.options.httpsSettings());
// TODO: other options
}
return bean;
}
private void setupHttps(ServerProperties server, HttpsSettings httpsSettings) {
if (httpsSettings.port() < 0 || !httpsSettings.enabled()) {
return;
}
Ssl ssl = server.getSsl();
if (ssl == null) {
ssl = new Ssl();
server.setSsl(ssl);
}
ssl.setKeyStore(httpsSettings.keyStorePath());
ssl.setKeyPassword(httpsSettings.keyStorePassword());
if (httpsSettings.hasTrustStore()) {
ssl.setTrustStore(httpsSettings.trustStorePath());
ssl.setTrustStorePassword(httpsSettings.trustStorePassword());
}
}
private int getPort() {
if (SpringBootHttpServer.this.options.httpsSettings().port() >= 0) {
return SpringBootHttpServer.this.options.httpsSettings().port();
}
return SpringBootHttpServer.this.options.portNumber();
}
}
}
@Configuration
@Import({ ServerPropertiesAutoConfiguration.class, BeanPostProcessorsRegistrar.class,
ConfigurationPropertiesAutoConfiguration.class, JacksonAutoConfiguration.class,
HttpMessageConvertersAutoConfiguration.class,
PropertyPlaceholderAutoConfiguration.class })
class WiremockServerConfiguration {
@Autowired
private AdminRequestHandler adminRequestHandler;
@Autowired
private StubRequestHandler stubRequestHandler;
@Autowired
private Options options;
@Bean(name = DispatcherServletAutoConfiguration.DEFAULT_DISPATCHER_SERVLET_REGISTRATION_BEAN_NAME)
public ServletRegistrationBean stubServletRegistration() {
ServletRegistrationBean reg = new ServletRegistrationBean();
reg.addInitParameter(RequestHandler.HANDLER_CLASS_KEY,
StubRequestHandler.class.getName());
reg.setServlet(new WireMockHandlerDispatchingServlet());
reg.setName("stub");
reg.addUrlMappings("/");
return reg;
}
@Bean
public ServletRegistrationBean adminServletRegistration() {
ServletRegistrationBean reg = new ServletRegistrationBean();
reg.addInitParameter(RequestHandler.HANDLER_CLASS_KEY,
AdminRequestHandler.class.getName());
reg.setServlet(new WireMockHandlerDispatchingServlet());
reg.setName("admin");
reg.addUrlMappings(WireMockApp.ADMIN_CONTEXT_ROOT + "/*");
return reg;
}
@Bean
public ServletContextAware servletContextSetUp() {
return new ServletContextAware() {
@Override
public void setServletContext(ServletContext servletContext) {
servletContext.setAttribute(AdminRequestHandler.class.getName(),
WiremockServerConfiguration.this.adminRequestHandler);
servletContext.setAttribute(StubRequestHandler.class.getName(),
WiremockServerConfiguration.this.stubRequestHandler);
servletContext.setAttribute(Notifier.KEY, options.notifier());
}
};
}
@Bean
public EmbeddedServletContainerFactory servletContainer() {
// TODO support for other containers
TomcatEmbeddedServletContainerFactory tomcat = new TomcatEmbeddedServletContainerFactory();
if (this.options.httpsSettings().enabled()) {
tomcat.addAdditionalTomcatConnectors(createStandardConnector());
}
return tomcat;
}
private Connector createStandardConnector() {
Connector connector = new Connector("org.apache.coyote.http11.Http11NioProtocol");
connector.setPort(this.options.portNumber());
return connector;
}
}

View File

@@ -0,0 +1,89 @@
/*
* Copyright 2012-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.cloud.contract.wiremock;
import javax.annotation.PostConstruct;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.SmartLifecycle;
import org.springframework.context.annotation.Configuration;
import com.github.tomakehurst.wiremock.WireMockServer;
import com.github.tomakehurst.wiremock.client.WireMock;
import com.github.tomakehurst.wiremock.core.Options;
/**
* @author Dave Syer
*
*/
@Configuration
public class WireMockConfiguration implements SmartLifecycle {
private volatile boolean running;
private WireMockServer server;
@Autowired(required = false)
private Options options;
@PostConstruct
public void init() {
if (options == null) {
this.options = com.github.tomakehurst.wiremock.core.WireMockConfiguration
.wireMockConfig()
.httpServerFactory(new SpringBootHttpServerFactory());
}
server = new WireMockServer(options);
}
@Override
public void start() {
server.start();
WireMock.configureFor("localhost", options.portNumber());
running = true;
}
@Override
public void stop() {
if (running) {
server.stop();
running = false;
}
}
@Override
public boolean isRunning() {
return running;
}
@Override
public int getPhase() {
return 0;
}
@Override
public boolean isAutoStartup() {
return true;
}
@Override
public void stop(Runnable callback) {
stop();
callback.run();
}
}

View File

@@ -0,0 +1,83 @@
/*
* Copyright 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.cloud.contract.wiremock;
import static org.springframework.test.web.client.match.MockRestRequestMatchers.requestTo;
import static org.springframework.test.web.client.response.MockRestResponseCreators.withSuccess;
import java.io.IOException;
import java.nio.charset.Charset;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.http.MediaType;
import org.springframework.test.web.client.MockRestServiceServer;
import org.springframework.util.StreamUtils;
import org.springframework.web.client.RestTemplate;
import com.github.tomakehurst.wiremock.common.Json;
import com.github.tomakehurst.wiremock.stubbing.StubMapping;
/**
* @author Dave Syer
*
*/
public class WireMockExpectations {
private PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
private String prefix = "classpath:/stubs/";
private String suffix = ".json";
private String baseUrl = "";
private MockRestServiceServer server;
private WireMockExpectations(RestTemplate restTemplate) {
this.server = MockRestServiceServer.bindTo(restTemplate).build();
}
public WireMockExpectations baseUrl(String baseUrl) {
this.baseUrl = baseUrl;
return this;
}
public MockRestServiceServer expect(String... locations) {
for (String location : locations) {
try {
for (Resource resource : this.resolver.getResources(this.prefix + location + this.suffix)) {
StubMapping mapping;
mapping = Json.read(StreamUtils.copyToString(resource.getInputStream(), Charset.defaultCharset()),
StubMapping.class);
this.server.expect(requestTo(this.baseUrl + mapping.getRequest().getUrlPath()))
.andRespond(withSuccess(mapping.getResponse().getBody(), MediaType.TEXT_PLAIN));
}
}
catch (IOException e) {
throw new IllegalStateException("Cannot load resources for: " + location, e);
}
}
return this.server;
}
public static WireMockExpectations with(RestTemplate restTemplate) {
WireMockExpectations result = new WireMockExpectations(restTemplate);
return result;
}
}

View File

@@ -0,0 +1,74 @@
/*
* Copyright 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.cloud.contract.wiremock;
import javax.net.ssl.HttpsURLConnection;
import org.apache.http.conn.ssl.NoopHostnameVerifier;
import org.apache.http.conn.ssl.TrustSelfSignedStrategy;
import org.apache.http.ssl.SSLContexts;
import org.junit.Assert;
import org.springframework.util.ClassUtils;
import com.github.tomakehurst.wiremock.client.WireMock;
import com.github.tomakehurst.wiremock.core.WireMockConfiguration;
/**
* Convenience factory class for a {@link WireMockConfiguration} that knows how to use
* Spring Boot to create a stub server. Use, for example, in a JUnit rule:
*
* <pre>
* &#64;ClassRule
* public static WireMockClassRule wiremock = new WireMockClassRule(
* WireMockSpring.config());
* </pre>
*
* and then use {@link WireMock} as normal in your test methods.
*
* @author Dave Syer
*
*/
public abstract class WireMockSpring {
private static boolean initialized = false;
public static WireMockConfiguration config() {
if (!initialized) {
if (ClassUtils.isPresent("org.apache.http.conn.ssl.NoopHostnameVerifier",
null)) {
HttpsURLConnection
.setDefaultHostnameVerifier(NoopHostnameVerifier.INSTANCE);
try {
HttpsURLConnection
.setDefaultSSLSocketFactory(SSLContexts.custom()
.loadTrustMaterial(null,
TrustSelfSignedStrategy.INSTANCE)
.build().getSocketFactory());
}
catch (Exception e) {
Assert.fail("Cannot install custom socket factory: [" + e.getMessage()
+ "]");
}
}
initialized = true;
}
WireMockConfiguration config = new WireMockConfiguration();
config.httpServerFactory(new SpringBootHttpServerFactory());
return config;
}
}

View File

@@ -0,0 +1,37 @@
/*
* Copyright 2012-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.cloud.contract.wiremock;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.context.annotation.Import;
/**
* @author Dave Syer
*
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(WireMockConfiguration.class)
public @interface WireMockTest {
}

View File

@@ -0,0 +1,38 @@
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 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.test.annotation.DirtiesContext;
import org.springframework.test.context.junit4.SpringRunner;
import com.github.tomakehurst.wiremock.junit.WireMockClassRule;
@RunWith(SpringRunner.class)
@SpringBootTest(classes=WiremockTestsApplication.class, properties="app.baseUrl=https://localhost:8443")
@DirtiesContext
public class WiremockHttpsServerApplicationTests {
@ClassRule
public static WireMockClassRule wiremock = new WireMockClassRule(
WireMockSpring.config().httpsPort(8443));
@Autowired
private Service service;
@Test
public void contextLoads() throws Exception {
stubFor(get(urlEqualTo("/resource"))
.willReturn(aResponse().withHeader("Content-Type", "text/plain").withBody("Hello World!")));
assertThat(this.service.go()).isEqualTo("Hello World!");
}
}

View File

@@ -0,0 +1,33 @@
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 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;
@RunWith(SpringRunner.class)
@SpringBootTest(classes=WiremockTestsApplication.class, properties="app.baseUrl=http://localhost:8080", webEnvironment=WebEnvironment.NONE)
@DirtiesContext
@WireMockTest
public class WiremockImportApplicationTests {
@Autowired
private Service service;
@Test
public void contextLoads() throws Exception {
stubFor(get(urlEqualTo("/resource"))
.willReturn(aResponse().withHeader("Content-Type", "text/plain").withBody("Hello World!")));
assertThat(this.service.go()).isEqualTo("Hello World!");
}
}

View File

@@ -0,0 +1,35 @@
package org.springframework.cloud.contract.wiremock;
import static org.assertj.core.api.Assertions.assertThat;
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 org.springframework.test.web.client.MockRestServiceServer;
import org.springframework.web.client.RestTemplate;
@RunWith(SpringRunner.class)
@SpringBootTest(classes=WiremockTestsApplication.class, webEnvironment=WebEnvironment.NONE)
@DirtiesContext
public class WiremockMockServerApplicationTests {
@Autowired
private RestTemplate restTemplate;
@Autowired
private Service service;
@Test
public void contextLoads() throws Exception {
MockRestServiceServer server = WireMockExpectations.with(this.restTemplate) //
.baseUrl("http://example.org") //
.expect("resource");
assertThat(this.service.go()).isEqualTo("Hello World");
server.verify();
}
}

View File

@@ -0,0 +1,38 @@
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 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;
@RunWith(SpringRunner.class)
@SpringBootTest(classes=WiremockTestsApplication.class, properties="app.baseUrl=http://localhost:8080", webEnvironment=WebEnvironment.NONE)
@DirtiesContext
public class WiremockServerApplicationTests {
@ClassRule
public static WireMockClassRule wiremock = new WireMockClassRule(WireMockSpring.config());
@Autowired
private Service service;
@Test
public void contextLoads() throws Exception {
stubFor(get(urlEqualTo("/resource"))
.willReturn(aResponse().withHeader("Content-Type", "text/plain").withBody("Hello World!")));
assertThat(this.service.go()).isEqualTo("Hello World!");
}
}

View File

@@ -0,0 +1,59 @@
package org.springframework.cloud.contract.wiremock;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;
@Configuration
@EnableAutoConfiguration
@Import({Service.class, Controller.class})
public class WiremockTestsApplication {
@Bean
public RestTemplate restTemplate() {
return new RestTemplate();
}
public static void main(String[] args) {
SpringApplication.run(WiremockTestsApplication.class, args);
}
}
@RestController
class Controller {
private final Service service;
public Controller(Service service) {
this.service = service;
}
@RequestMapping("/")
public String home() {
return this.service.go();
}
}
@Component
class Service {
@Value("${app.baseUrl:http://example.org}")
private String base;
private RestTemplate restTemplate;
public Service(RestTemplate restTemplate) {
this.restTemplate = restTemplate;
}
public String go() {
return this.restTemplate.getForEntity(this.base + "/resource", String.class).getBody();
}
}

View File

@@ -0,0 +1,10 @@
{
"request" : {
"urlPath" : "/resource",
"method" : "GET"
},
"response" : {
"status" : 200,
"body" : "Hello World"
}
}