Permalink
Browse files

Merge branch 'custom_matcher_problems' of https://github.com/dkowis/b…

…etamax into dkowis-custom_matcher_problems
  • Loading branch information...
2 parents d1fc7c1 + 54b9bac commit 7d1f1848096772b8ee8a5f4b983d45f155e98a95 Rob Fletcher committed Nov 23, 2013
View
11 betamax-proxy/src/main/java/co/freeside/betamax/ProxyConfiguration.java
@@ -24,20 +24,23 @@
public class ProxyConfiguration extends Configuration {
public static final String DEFAULT_PROXY_HOST = "0.0.0.0";
+ public static final int DEFAULT_REQUEST_BUFFER_SIZE = 8388608; //8MB
public static final int DEFAULT_PROXY_PORT = 5555;
public static final int DEFAULT_PROXY_TIMEOUT = 5;
private final String proxyHost;
private final int proxyPort;
private final int proxyTimeoutSeconds;
private final boolean sslEnabled;
+ private final int requestBufferSize;
protected ProxyConfiguration(ProxyConfigurationBuilder<?> builder) {
super(builder);
this.proxyHost = builder.proxyHost;
this.proxyPort = builder.proxyPort;
this.proxyTimeoutSeconds = builder.proxyTimeoutSeconds;
this.sslEnabled = builder.sslEnabled;
+ this.requestBufferSize = builder.requestBufferSize;
}
public static ProxyConfigurationBuilder<?> builder() {
@@ -59,6 +62,14 @@ public int getProxyTimeoutSeconds() {
}
/**
+ * The buffer size the proxy will use to aggregate incoming requests.
+ * Needed if you want to match on request body.
+ */
+ public int getRequestBufferSize() {
+ return requestBufferSize;
+ }
+
+ /**
* If set to true add support for proxying SSL (disable certificate
* checking).
*/
View
11 betamax-proxy/src/main/java/co/freeside/betamax/ProxyConfigurationBuilder.java
@@ -22,13 +22,15 @@
public abstract class ProxyConfigurationBuilder<T extends ProxyConfigurationBuilder<T>> extends ConfigurationBuilder<T> {
+
public ProxyConfiguration build() {
return new ProxyConfiguration(this);
}
protected String proxyHost = DEFAULT_PROXY_HOST;
protected int proxyPort = DEFAULT_PROXY_PORT;
protected int proxyTimeoutSeconds = DEFAULT_PROXY_TIMEOUT;
+ protected int requestBufferSize = DEFAULT_REQUEST_BUFFER_SIZE;
protected boolean sslEnabled;
@Override
@@ -47,6 +49,10 @@ public T withProperties(Properties properties) {
proxyTimeoutSeconds(TypedProperties.getInteger(properties, "betamax.proxyTimeoutSeconds"));
}
+ if (properties.containsKey("betamax.requestBufferSize")) {
+ requestBufferSize(TypedProperties.getInteger(properties, "betamax.requestBufferSize"));
+ }
+
if (properties.containsKey("betamax.sslEnabled")) {
sslEnabled(TypedProperties.getBoolean(properties, "betamax.sslEnabled"));
}
@@ -69,6 +75,11 @@ public T proxyTimeoutSeconds(int proxyTimeoutSeconds) {
return self();
}
+ public T requestBufferSize(int requestBufferSize){
+ this.requestBufferSize = requestBufferSize;
+ return self();
+ }
+
public T sslEnabled(boolean sslEnabled) {
this.sslEnabled = sslEnabled;
return self();
View
33 betamax-proxy/src/main/java/co/freeside/betamax/proxy/BetamaxFilters.java
@@ -19,6 +19,7 @@
import java.io.IOException;
import java.util.Map;
import java.util.logging.Logger;
+
import co.freeside.betamax.encoding.*;
import co.freeside.betamax.handler.NonWritableTapeException;
import co.freeside.betamax.message.Response;
@@ -29,6 +30,7 @@
import io.netty.buffer.ByteBuf;
import io.netty.handler.codec.http.*;
import org.littleshoot.proxy.HttpFiltersAdapter;
+
import static co.freeside.betamax.Headers.*;
import static io.netty.buffer.Unpooled.wrappedBuffer;
import static io.netty.handler.codec.http.HttpHeaders.Names.*;
@@ -56,8 +58,19 @@ public HttpResponse requestPre(HttpObject httpObject) {
HttpResponse response = null;
if (httpObject instanceof HttpRequest) {
request.copyHeaders((HttpMessage) httpObject);
- response = onRequestIntercepted((HttpRequest) httpObject).orNull();
}
+
+ //If we're getting content stick it in there.
+ if (httpObject instanceof HttpContent) {
+ request.append((HttpContent) httpObject);
+ //If it's the last one, we want to take further steps, like checking to see if we've recorded on it!
+ if (httpObject instanceof LastHttpContent) {
+ //We will have collected the last of the http Request finally
+ //And now we're ready to intercept it and do proxy-type-things
+ response = onRequestIntercepted().orNull();
+ }
+ }
+
return response;
} catch (IOException e) {
return createErrorResponse(e);
@@ -66,19 +79,11 @@ public HttpResponse requestPre(HttpObject httpObject) {
@Override
public HttpResponse requestPost(HttpObject httpObject) {
- try {
- if (httpObject instanceof HttpContent) {
- request.append((HttpContent) httpObject);
- }
-
- if (httpObject instanceof HttpRequest) {
- setViaHeader((HttpMessage) httpObject);
- }
-
- return null;
- } catch (IOException e) {
- return createErrorResponse(e);
+ if (httpObject instanceof HttpRequest) {
+ setViaHeader((HttpMessage) httpObject);
}
+
+ return null;
}
@Override
@@ -114,7 +119,7 @@ public void responsePost(HttpObject httpObject) {
}
}
- private Optional<? extends FullHttpResponse> onRequestIntercepted(HttpRequest httpObject) throws IOException {
+ private Optional<? extends FullHttpResponse> onRequestIntercepted() throws IOException {
if (tape == null) {
return Optional.of(new DefaultFullHttpResponse(HTTP_1_1, new HttpResponseStatus(403, "No tape")));
} else if (tape.isReadable() && tape.seek(request)) {
View
5 betamax-proxy/src/main/java/co/freeside/betamax/proxy/ProxyServer.java
@@ -86,6 +86,11 @@ public void start(final Tape tape) {
proxyServerBootstrap.withFiltersSource(new HttpFiltersSourceAdapter() {
@Override
+ public int getMaximumRequestBufferSizeInBytes() {
+ return configuration.getRequestBufferSize();
+ }
+
+ @Override
public HttpFilters filterRequest(HttpRequest originalRequest) {
HttpFilters filters = new BetamaxFilters(originalRequest, tape);
return new PredicatedHttpFilters(filters, NOT_CONNECT, originalRequest);
View
3 betamax-proxy/src/main/java/co/freeside/betamax/proxy/netty/NettyMessageAdapter.java
@@ -81,6 +81,7 @@ public boolean hasBody() {
@Override
protected InputStream getBodyAsStream() throws IOException {
- return new ByteBufInputStream(body);
+ //Copy the body into a new ByteBuf so that it can be consumed multiple times.
+ return new ByteBufInputStream(Unpooled.copiedBuffer(body));
}
}
View
93 betamax-proxy/src/test/groovy/co/freeside/betamax/proxy/matchRules/CustomMatcherSpec.groovy
@@ -0,0 +1,93 @@
+package co.freeside.betamax.proxy.matchRules
+
+import co.freeside.betamax.ProxyConfiguration
+import co.freeside.betamax.Recorder
+import co.freeside.betamax.TapeMode
+import com.google.common.io.Files
+import spock.lang.Shared
+import spock.lang.Specification
+import spock.lang.Unroll
+
+import javax.net.ssl.HttpsURLConnection
+
+/**
+ * Testing a custom matcher when being used in the proxy.
+ */
+@Unroll
+class CustomMatcherSpec extends Specification {
+ @Shared
+ def tapeRoot = new File(CustomMatcherSpec.class.getResource("/betamax/tapes/").toURI())
+
+ def simplePost(String url, String payload) {
+ def output = null
+ HttpsURLConnection conn = new URL(url).openConnection()
+ conn.setDoOutput(true)
+ conn.setRequestMethod("POST")
+ conn.setFixedLengthStreamingMode(payload.getBytes().length)
+ def out = new PrintWriter(conn.getOutputStream())
+ out.print(payload)
+ out.flush()
+ out.close()
+
+ output = conn.getInputStream().getText()
+ conn.disconnect()
+
+ return output
+ }
+
+ void "Using a custom matcher it should replay"() {
+ given:
+ def imr = new InstrumentedMatchRule()
+ def proxyConfig = ProxyConfiguration.builder()
+ .sslEnabled(true)
+ .tapeRoot(tapeRoot)
+ .defaultMode(TapeMode.READ_ONLY)
+ .defaultMatchRule(imr)
+ .build()
+
+ def recorder = new Recorder(proxyConfig)
+ recorder.start("httpBinTape")
+ imr.requestValidations << { r ->
+ //Will run this request validation on both requests being matched
+ //No matter what, either recorded, or sent, I should have a payload of "BUTTS"
+ //I'm posting "BUTTS" and the recorded interaction should have "BUTTS"
+ if(!r.hasBody() ){
+ println("REQUEST BODY WASNT THERE!!!")
+ }
+ }
+
+ when:
+ def response = simplePost("https://httpbin.org/post", "BUTTS")
+ then:
+ def content = response.toString()
+ recorder.stop()
+
+ content == "Hey look some text: BUTTS"
+ }
+
+ void "Using a custom matcher it should record a new one"() {
+ given:
+ def tapeRoot = Files.createTempDir() //Using a temp dir this time
+ def imr = new InstrumentedMatchRule()
+ def proxyConfig = ProxyConfiguration.builder()
+ .sslEnabled(true)
+ .tapeRoot(tapeRoot)
+ .defaultMode(TapeMode.READ_WRITE)
+ .defaultMatchRule(imr)
+ .build()
+
+ def recorder = new Recorder(proxyConfig)
+ recorder.start("httpBinTape")
+ when:
+ def response = simplePost("https://httpbin.org/post", "LOLWUT")
+ then:
+ def content = response.toString()
+ recorder.stop()
+ //The tape is written when it's referenced not in this dir
+ //make sure there's a file in there
+ def recordedTape = new File(tapeRoot, "httpBinTape.yaml")
+ //It should have recorded it to the tape
+ recordedTape.exists()
+ }
+
+}
View
49 ...x-proxy/src/test/groovy/co/freeside/betamax/proxy/matchRules/InstrumentedMatchRule.groovy
@@ -0,0 +1,49 @@
+package co.freeside.betamax.proxy.matchRules
+
+import co.freeside.betamax.MatchRule
+import co.freeside.betamax.message.Request
+import com.google.common.io.ByteStreams
+
+import java.util.concurrent.atomic.AtomicInteger
+
+
+class InstrumentedMatchRule implements MatchRule {
+
+ def counter = new AtomicInteger(0)
+
+ def requestValidations = []
+
+ @Override
+ boolean isMatch(Request a, Request b) {
+
+ requestValidations.each { rv ->
+ rv.call(a)
+ rv.call(b)
+ }
+
+ def current = counter.incrementAndGet()
+ println("Matching attempt: ${current}")
+ println("A request class: ${a.getClass()}")
+ println("B request class: ${b.getClass()}")
+
+ if(a.getUri() == b.getUri() && a.getMethod() == b.getMethod()) {
+ //Same method and URI, lets do a body comparison
+ //Can only consume the body once, once it's gone it's gone.
+ def aBody = a.getBodyAsText().getInput().getText()
+ def bBody = b.getBodyAsText().getInput().getText()
+
+ //Ideally in the real world, we'd parse the XML or the JSON and compare the ASTs instead
+ // of just comparing the body strings, so that meaningless whitespace doesn't mean anything
+ println("aBody: |" + aBody + "|")
+ println("bBody: |" + bBody + "|")
+
+ def textMatch = aBody.equals(bBody)
+
+ //Right now, lets just compare the bodies also
+ return textMatch
+ } else {
+ //URI and method don't match, so we're going to bail
+ return false
+ }
+ }
+}
View
27 betamax-proxy/src/test/resources/betamax/tapes/httpBinTape.yaml
@@ -0,0 +1,27 @@
+!tape
+name: httpbinTape
+interactions:
+- recorded: 2013-11-11T17:53:35.524Z
+ request:
+ method: POST
+ uri: https://httpbin.org/post
+ headers:
+ Accept: '*/*'
+ Accept-Charset: UTF-8,*;q=.1
+ Accept-Encoding: gzip
+ Connection: keep-alive
+ Content-Length: '5'
+ Content-Type: text/plain
+ Host: httpbin.org
+ User-Agent: Java/1.7.0_45
+ body: "BUTTS"
+ response:
+ status: 200
+ headers:
+ Access-Control-Allow-Origin: '*'
+ Connection: keep-alive
+ Content-Length: '425'
+ Content-Type: application/json
+ Date: Mon, 11 Nov 2013 17:53:35 GMT
+ Server: gunicorn/0.17.4
+ body: "Hey look some text: BUTTS"
View
4 readme.md
@@ -15,7 +15,7 @@ Add a `@Rule RecorderRule` property to your test and annotate test methods with
Future test runs replay responses from _tape_ without traffic going to the real target. No more 3rd party downtime or rate limits breaking your tests. You can even run your tests offline! Insert different _tapes_ to stub different responses.
### Customize
-_Tapes_ are just [YAML][yaml] files so you can edit them with a text editor, commit to source control, share with your team & use on continuous integration.
+_Tapes_ are just [YAML][yaml] files so you can edit them with a text editor, commit to source control, share with your team & use on continuous integration. An example tape file can be found [here](https://github.com/robfletcher/betamax/blob/master/betamax-proxy/src/test/resources/betamax/tapes/smoke_spec.yaml).
## Full documentation
@@ -50,4 +50,4 @@ Please get in touch if you have any feedback. You can raise defects and feature
## Notes on running tests from inside IntelliJ IDEA
Go to _Settings -> Compiler_ and ensure that `*.keystore` appears in the _Resource patterns_ otherwise IDEA will not
-make the SSL keystore available on the classpath when tests run.
+make the SSL keystore available on the classpath when tests run.

0 comments on commit 7d1f184

Please sign in to comment.