├── .gitignore
├── .travis.yml
├── LICENSE
├── README.md
├── pom.xml
└── src
├── main
├── java
│ └── io
│ │ └── github
│ │ └── ddebree
│ │ └── pact
│ │ └── proxy
│ │ ├── PactProxyApplication.java
│ │ ├── filters
│ │ └── PactRecorderFilter.java
│ │ └── service
│ │ └── PactResultWriter.java
└── resources
│ ├── application.yml
│ └── banner.txt
└── test
└── java
└── io
└── github
└── ddebree
└── pact
└── proxy
├── PactProxyApplicationTest.java
└── filters
└── PactRecorderFilterTest.java
/.gitignore:
--------------------------------------------------------------------------------
1 | *.class
2 |
3 | # Mobile Tools for Java (J2ME)
4 | .mtj.tmp/
5 |
6 | # Package Files #
7 | *.jar
8 | *.war
9 | *.ear
10 |
11 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
12 | hs_err_pid*
13 |
14 | # Operating System Files
15 |
16 | *.DS_Store
17 | Thumbs.db
18 | *.sw?
19 | .#*
20 | *#
21 | *~
22 | *.sublime-*
23 |
24 | # Build Artifacts
25 |
26 | target/
27 | dependency-reduced-pom.xml
28 |
29 | # Eclipse Project Files
30 | .classpath
31 | .project
32 | .settings/
33 |
34 | # IntelliJ IDEA Files
35 | *.iml
36 | *.ipr
37 | *.iws
38 | *.idea
39 |
40 | README.html
41 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: java
2 | jdk:
3 | - oraclejdk8
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2017 Dean de Bree
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # pact-proxy
2 |
3 | [](https://travis-ci.org/ddebree/pact-proxy)
4 |
5 | A proxy that generates pact files. All you need to do is point to a remote server and then access the services via the proxy server's url.
6 |
7 | ## Usage
8 |
9 | java -jar pact-proxy.jar --port=5555 --remote="https://api.github.com"
--------------------------------------------------------------------------------
/pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 | 4.0.0
5 |
6 |
7 | org.springframework.boot
8 | spring-boot-starter-parent
9 | 1.5.2.RELEASE
10 |
11 |
12 |
13 | io.github.ddebree
14 | pact-proxy
15 | 1.0.1-SNAPSHOT
16 | jar
17 |
18 |
19 | UTF-8
20 | 1.8
21 |
22 |
23 |
24 |
25 | org.springframework.cloud
26 | spring-cloud-starter-zuul
27 |
28 |
29 | org.springframework.boot
30 | spring-boot-starter-web
31 |
32 |
33 |
34 |
35 | au.com.dius
36 | pact-jvm-consumer_2.11
37 | 3.3.8
38 |
39 |
40 |
41 |
42 | org.springframework.boot
43 | spring-boot-starter-test
44 | test
45 |
46 |
47 |
48 |
49 |
50 |
51 | org.springframework.cloud
52 | spring-cloud-dependencies
53 | Dalston.RELEASE
54 | pom
55 | import
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 | org.springframework.boot
64 | spring-boot-maven-plugin
65 |
66 |
67 |
68 |
69 |
70 |
--------------------------------------------------------------------------------
/src/main/java/io/github/ddebree/pact/proxy/PactProxyApplication.java:
--------------------------------------------------------------------------------
1 | package io.github.ddebree.pact.proxy;
2 |
3 | import org.springframework.boot.SpringApplication;
4 | import org.springframework.boot.autoconfigure.SpringBootApplication;
5 | import org.springframework.cloud.netflix.zuul.EnableZuulProxy;
6 |
7 | @EnableZuulProxy
8 | @SpringBootApplication
9 | public class PactProxyApplication {
10 |
11 | public static void main(String[] args) {
12 | SpringApplication.run(PactProxyApplication.class, args);
13 | }
14 |
15 | }
16 |
--------------------------------------------------------------------------------
/src/main/java/io/github/ddebree/pact/proxy/filters/PactRecorderFilter.java:
--------------------------------------------------------------------------------
1 | package io.github.ddebree.pact.proxy.filters;
2 |
3 | import javax.servlet.http.HttpServletRequest;
4 |
5 | import au.com.dius.pact.consumer.ConsumerPactBuilder;
6 | import au.com.dius.pact.consumer.dsl.PactDslRequestWithPath;
7 | import au.com.dius.pact.consumer.dsl.PactDslResponse;
8 | import au.com.dius.pact.model.RequestResponsePact;
9 | import com.netflix.zuul.context.RequestContext;
10 | import com.netflix.zuul.ZuulFilter;
11 |
12 | import io.github.ddebree.pact.proxy.service.PactResultWriter;
13 | import org.slf4j.Logger;
14 | import org.slf4j.LoggerFactory;
15 | import org.springframework.beans.factory.annotation.Autowired;
16 | import org.springframework.beans.factory.annotation.Value;
17 | import org.springframework.stereotype.Component;
18 | import org.springframework.util.StreamUtils;
19 |
20 | import java.io.*;
21 | import java.nio.charset.Charset;
22 | import java.util.concurrent.atomic.AtomicLong;
23 | import java.util.zip.GZIPInputStream;
24 |
25 | import static org.springframework.cloud.netflix.zuul.filters.support.FilterConstants.POST_TYPE;
26 | import static org.springframework.util.ReflectionUtils.rethrowRuntimeException;
27 |
28 | @Component
29 | public class PactRecorderFilter extends ZuulFilter {
30 |
31 | private static final AtomicLong COUNTER = new AtomicLong(0);
32 | private static final Logger LOGGER = LoggerFactory.getLogger(PactRecorderFilter.class);
33 |
34 | private final PactResultWriter pactResultWriter;
35 | private final String clientName;
36 | private final String providerName;
37 |
38 | @Autowired
39 | public PactRecorderFilter(PactResultWriter pactResultWriter,
40 | @Value("${clientName}") String clientName,
41 | @Value("${providerName}") String providerName) {
42 | this.pactResultWriter = pactResultWriter;
43 | this.clientName = clientName;
44 | this.providerName = providerName;
45 | }
46 |
47 | @Override
48 | public String filterType() {
49 | return POST_TYPE;
50 | }
51 |
52 | @Override
53 | public int filterOrder() {
54 | return 1;
55 | }
56 |
57 | @Override
58 | public boolean shouldFilter() {
59 | return true;
60 | }
61 |
62 | @Override
63 | public Object run() {
64 | long requestId = COUNTER.incrementAndGet();
65 | try {
66 | RequestContext context = RequestContext.getCurrentContext();
67 | HttpServletRequest request = context.getRequest();
68 |
69 | String url = request.getRequestURL().toString();
70 |
71 | LOGGER.info("Request context: {}", context);
72 | LOGGER.info("Request {}: {} request to {}", requestId, request.getMethod(), url);
73 |
74 | PactDslRequestWithPath pactRequest = ConsumerPactBuilder
75 | .consumer(clientName)
76 | .hasPactWith(providerName)
77 | .uponReceiving("Request id " + requestId)
78 | .path(url)
79 | .method(request.getMethod());
80 | buildRequestBody(pactRequest);
81 |
82 | PactDslResponse pactResponse = pactRequest
83 | .willRespondWith()
84 | .status(context.getResponseStatusCode());
85 | buildResponseBody(pactResponse);
86 |
87 | RequestResponsePact pact = pactResponse.toFragment().toPact();
88 |
89 | pactResultWriter.writePact(url, requestId, pact);
90 | } catch (IOException e) {
91 | rethrowRuntimeException(e);
92 | }
93 |
94 | return null;
95 | }
96 |
97 | private void buildRequestBody(PactDslRequestWithPath pactRequest) throws IOException {
98 | final RequestContext context = RequestContext.getCurrentContext();
99 | String requestBody = null;
100 | InputStream in = (InputStream) context.get("requestEntity");
101 | if (in == null) {
102 | in = context.getRequest().getInputStream();
103 |
104 | }
105 | if (in != null) {
106 | String encoding = context.getRequest().getCharacterEncoding();
107 | requestBody = StreamUtils.copyToString(in,
108 | Charset.forName(encoding != null ? encoding : "UTF-8"));
109 | }
110 | if (requestBody != null && requestBody.length() > 0) {
111 | pactRequest.body(requestBody);
112 | }
113 | }
114 |
115 | private void buildResponseBody(PactDslResponse pactResponse) throws IOException {
116 | RequestContext context = RequestContext.getCurrentContext();
117 | if (context.getResponseBody() != null) {
118 | String body = context.getResponseBody();
119 | pactResponse.body(body);
120 | } else if (context.getResponseDataStream() != null) {
121 | String encoding = context.getRequest().getCharacterEncoding();
122 | InputStream stream = context.getResponseDataStream();
123 | byte[] responseBytes = StreamUtils.copyToByteArray(stream);
124 | context.setResponseDataStream(new ByteArrayInputStream(responseBytes));
125 |
126 | if (context.getResponseGZipped()) {
127 | LOGGER.warn("GZipped content found");
128 | final Long len = context.getOriginContentLength();
129 | if (len == null || len > 0) {
130 | try {
131 | String responseBody = StreamUtils.copyToString(new GZIPInputStream(new ByteArrayInputStream(responseBytes)),
132 | Charset.forName(encoding != null ? encoding : "UTF-8"));
133 | pactResponse.body(responseBody);
134 | } catch (java.util.zip.ZipException ex) {
135 | LOGGER.debug(
136 | "gzip expected but not "
137 | + "received assuming unencoded response "
138 | + RequestContext.getCurrentContext()
139 | .getRequest().getRequestURL()
140 | .toString());
141 | }
142 | }
143 | } else {
144 | String responseBody = StreamUtils.copyToString(new ByteArrayInputStream(responseBytes),
145 | Charset.forName(encoding != null ? encoding : "UTF-8"));
146 | pactResponse.body(responseBody);
147 | }
148 | }
149 | }
150 |
151 | }
--------------------------------------------------------------------------------
/src/main/java/io/github/ddebree/pact/proxy/service/PactResultWriter.java:
--------------------------------------------------------------------------------
1 | package io.github.ddebree.pact.proxy.service;
2 |
3 | import au.com.dius.pact.model.PactWriter;
4 | import au.com.dius.pact.model.RequestResponsePact;
5 | import io.github.ddebree.pact.proxy.filters.PactRecorderFilter;
6 | import org.slf4j.Logger;
7 | import org.slf4j.LoggerFactory;
8 | import org.springframework.beans.factory.annotation.Value;
9 | import org.springframework.stereotype.Service;
10 |
11 | import java.io.File;
12 | import java.io.IOException;
13 | import java.io.PrintWriter;
14 | import java.io.StringWriter;
15 |
16 | import static org.springframework.util.ReflectionUtils.rethrowRuntimeException;
17 |
18 | @Service
19 | public class PactResultWriter {
20 |
21 | private static final Logger LOGGER = LoggerFactory.getLogger(PactRecorderFilter.class);
22 |
23 | private final String outputPath;
24 |
25 | public PactResultWriter(@Value("${outputPath}") String outputPath) {
26 | this.outputPath = outputPath;
27 |
28 | File outputFolder = new File(outputPath);
29 | if ( ! outputFolder.exists()) {
30 | outputFolder.mkdir();
31 | }
32 | if ( ! outputFolder.isDirectory()) {
33 | throw new RuntimeException("Expected output folder " + outputFolder + " to be a directory");
34 | }
35 | }
36 |
37 | public void writePact(String url, long requestId, RequestResponsePact pact) {
38 | String filename = outputPath + "/" + url.replaceAll("[^\\p{Alnum}]", "_") + requestId;
39 | try {
40 | String pactDefinition;
41 | try (StringWriter strOut = new StringWriter()) {
42 | PactWriter.writePact(pact, new PrintWriter(strOut));
43 | pactDefinition = strOut.toString();
44 | }
45 | try (PrintWriter out = new PrintWriter(filename)) {
46 | out.println( pactDefinition );
47 | }
48 | LOGGER.debug("Pact file: {}", pactDefinition);
49 | } catch (IOException e) {
50 | rethrowRuntimeException(e);
51 | }
52 |
53 | }
54 |
55 | }
56 |
--------------------------------------------------------------------------------
/src/main/resources/application.yml:
--------------------------------------------------------------------------------
1 | outputPath: output
2 | clientName: ${client:client}
3 | providerName: ${server:server}
4 |
5 | zuul:
6 | routes:
7 | proxy:
8 | path: /**
9 | url: ${remote}
10 |
11 | ribbon:
12 | eureka:
13 | enabled: false
14 |
15 | server:
16 | port: ${port:9999}
17 |
--------------------------------------------------------------------------------
/src/main/resources/banner.txt:
--------------------------------------------------------------------------------
1 | /$$$$$$$ /$$ /$$$$$$$ Version: ${application.version}
2 | | $$__ $$ | $$ | $$__ $$
3 | | $$ \ $$ /$$$$$$ /$$$$$$$ /$$$$$$ | $$ \ $$ /$$$$$$ /$$$$$$ /$$ /$$ /$$ /$$
4 | | $$$$$$$/|____ $$ /$$_____/|_ $$_/ | $$$$$$$//$$__ $$ /$$__ $$| $$ /$$/| $$ | $$
5 | | $$____/ /$$$$$$$| $$ | $$ | $$____/| $$ \__/| $$ \ $$ \ $$$$/ | $$ | $$
6 | | $$ /$$__ $$| $$ | $$ /$$ | $$ | $$ | $$ | $$ >$$ $$ | $$ | $$
7 | | $$ | $$$$$$$| $$$$$$$ | $$$$/ | $$ | $$ | $$$$$$/ /$$/\ $$| $$$$$$$
8 | |__/ \_______/ \_______/ \___/ |__/ |__/ \______/ |__/ \__/ \____ $$
9 | /$$ | $$
10 | | $$$$$$/
11 | \______/
--------------------------------------------------------------------------------
/src/test/java/io/github/ddebree/pact/proxy/PactProxyApplicationTest.java:
--------------------------------------------------------------------------------
1 | package io.github.ddebree.pact.proxy;
2 |
3 | import au.com.dius.pact.model.RequestResponsePact;
4 | import com.netflix.zuul.context.RequestContext;
5 | import io.github.ddebree.pact.proxy.service.PactResultWriter;
6 | import org.junit.AfterClass;
7 | import org.junit.Before;
8 | import org.junit.BeforeClass;
9 | import org.junit.Test;
10 | import org.junit.runner.RunWith;
11 | import org.mockito.ArgumentCaptor;
12 | import org.springframework.beans.factory.annotation.Autowired;
13 | import org.springframework.boot.SpringApplication;
14 | import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
15 | import org.springframework.boot.test.context.SpringBootTest;
16 | import org.springframework.boot.test.mock.mockito.MockBean;
17 | import org.springframework.boot.test.web.client.TestRestTemplate;
18 | import org.springframework.context.ConfigurableApplicationContext;
19 | import org.springframework.context.annotation.Configuration;
20 | import org.springframework.test.context.junit4.SpringRunner;
21 | import org.springframework.web.bind.annotation.RequestMapping;
22 | import org.springframework.web.bind.annotation.RestController;
23 |
24 | import static org.assertj.core.api.Assertions.assertThat;
25 | import static org.mockito.Matchers.anyLong;
26 | import static org.mockito.Matchers.endsWith;
27 | import static org.mockito.Matchers.eq;
28 | import static org.mockito.Mockito.verify;
29 |
30 | @RunWith(SpringRunner.class)
31 | @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
32 | classes = PactProxyApplication.class, properties = {"remote=http://localhost:8090", "outputPath=target/results"})
33 | public class PactProxyApplicationTest {
34 |
35 | private static final String REQUEST_PATH = "/some-service";
36 | private static final String RESPONSE_BODY = "somebody";
37 |
38 | @Autowired
39 | private TestRestTemplate rest;
40 | @MockBean
41 | private PactResultWriter pactResultWriter;
42 |
43 | static ConfigurableApplicationContext testService;
44 |
45 | @BeforeClass
46 | public static void startMockRemoteService() {
47 | testService = SpringApplication.run(TestService.class, "--port=8090");
48 | }
49 |
50 | @AfterClass
51 | public static void closeMockRemoteService() {
52 | testService.close();
53 | }
54 |
55 | @Before
56 | public void setup() {
57 | RequestContext.testSetCurrentContext(new RequestContext());
58 | }
59 |
60 | @Test
61 | public void test() {
62 | String resp = rest.getForObject(REQUEST_PATH, String.class);
63 | assertThat(resp).isEqualTo(RESPONSE_BODY);
64 |
65 | ArgumentCaptor argument = ArgumentCaptor.forClass(RequestResponsePact.class);
66 | verify(pactResultWriter).writePact(endsWith("/some-service"), anyLong(), argument.capture());
67 | }
68 |
69 | @Configuration
70 | @EnableAutoConfiguration
71 | @RestController
72 | static class TestService {
73 | @RequestMapping(REQUEST_PATH)
74 | public String getAvailable() {
75 | return RESPONSE_BODY;
76 | }
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/src/test/java/io/github/ddebree/pact/proxy/filters/PactRecorderFilterTest.java:
--------------------------------------------------------------------------------
1 | package io.github.ddebree.pact.proxy.filters;
2 |
3 | import com.netflix.zuul.context.RequestContext;
4 | import io.github.ddebree.pact.proxy.service.PactResultWriter;
5 | import org.apache.catalina.servlet4preview.http.HttpServletRequest;
6 | import org.hamcrest.Matchers;
7 | import org.junit.Before;
8 | import org.junit.Ignore;
9 | import org.junit.Rule;
10 | import org.junit.Test;
11 | import org.junit.runner.RunWith;
12 | import org.mockito.Mock;
13 | import org.mockito.runners.MockitoJUnitRunner;
14 | import org.springframework.boot.test.rule.OutputCapture;
15 |
16 | import static org.assertj.core.api.Assertions.assertThat;
17 | import static org.mockito.Mockito.mock;
18 | import static org.mockito.Mockito.when;
19 |
20 | @RunWith(MockitoJUnitRunner.class)
21 | public class PactRecorderFilterTest {
22 |
23 | @Mock
24 | private PactResultWriter pactResultWriter;
25 |
26 | private PactRecorderFilter filter;
27 |
28 | @Rule
29 | public OutputCapture outputCapture = new OutputCapture();
30 |
31 | @Before
32 | public void setup() {
33 | this.filter = new PactRecorderFilter(pactResultWriter, "client", "server");
34 | }
35 |
36 | @Test
37 | public void testFilterType() {
38 | assertThat(filter.filterType()).isEqualTo("post");
39 | }
40 |
41 | @Test
42 | public void testFilterOrder() {
43 | assertThat(filter.filterOrder()).isEqualTo(1);
44 | }
45 |
46 | @Test
47 | public void testShouldFilter() {
48 | assertThat(filter.shouldFilter()).isTrue();
49 | }
50 |
51 | @Test
52 | public void testRun() {
53 | HttpServletRequest req = mock(HttpServletRequest.class);
54 | when(req.getMethod()).thenReturn("GET");
55 | when(req.getRequestURL()).thenReturn(new StringBuffer("http://foo"));
56 | RequestContext context = mock(RequestContext.class);
57 | when(context.getRequest()).thenReturn(req);
58 | RequestContext.testSetCurrentContext(context);
59 | filter.run();
60 | this.outputCapture.expect(Matchers.containsString("GET request to http://foo"));
61 | }
62 | }
63 |
--------------------------------------------------------------------------------