Permalink
Browse files

Merge branch 'refs/heads/master' into httpclient

Conflicts:
	src/docs/index.md
	src/main/groovy/co/freeside/betamax/handler/HandlerException.groovy
	src/main/groovy/co/freeside/betamax/handler/TargetConnector.groovy
	src/main/groovy/co/freeside/betamax/proxy/jetty/BetamaxProxy.groovy
	src/main/groovy/co/freeside/betamax/proxy/jetty/ProxyServer.groovy
	src/test/groovy/co/freeside/betamax/handler/TapeReaderSpec.groovy
	src/test/groovy/co/freeside/betamax/handler/TapeWriterSpec.groovy
	src/test/groovy/co/freeside/betamax/handler/TargetConnectorSpec.groovy
	src/test/groovy/co/freeside/betamax/recorder/TapeModeSpec.groovy
  • Loading branch information...
Rob Fletcher
Rob Fletcher committed Oct 22, 2012
2 parents 0e35fe0 + 610fad6 commit 6c50089a32ed5a67b1fa77061c356204783f7eca
Showing with 489 additions and 113 deletions.
  1. +1 −1 build.gradle
  2. +14 −3 src/docs/index.md
  3. +9 −5 src/main/groovy/co/freeside/betamax/TapeMode.groovy
  4. +1 −2 src/main/groovy/co/freeside/betamax/handler/HandlerException.groovy
  5. +2 −3 src/main/groovy/co/freeside/betamax/handler/TapeReader.groovy
  6. +1 −1 src/main/groovy/co/freeside/betamax/handler/TapeWriter.groovy
  7. +2 −8 src/main/groovy/co/freeside/betamax/handler/TargetConnector.groovy
  8. +14 −2 src/main/groovy/co/freeside/betamax/message/tape/RecordedMessage.groovy
  9. +4 −3 src/main/groovy/co/freeside/betamax/proxy/jetty/BetamaxProxy.groovy
  10. +55 −10 src/main/groovy/co/freeside/betamax/tape/MemoryTape.groovy
  11. +13 −1 src/main/groovy/co/freeside/betamax/tape/Tape.groovy
  12. +5 −10 src/test/groovy/co/freeside/betamax/handler/TapeReaderSpec.groovy
  13. +5 −10 src/test/groovy/co/freeside/betamax/handler/TapeWriterSpec.groovy
  14. +8 −13 src/test/groovy/co/freeside/betamax/handler/TargetConnectorSpec.groovy
  15. +4 −7 src/test/groovy/co/freeside/betamax/proxy/jetty/BetamaxProxySpec.groovy
  16. +91 −0 src/test/groovy/co/freeside/betamax/recorder/SequentialTapeSpec.groovy
  17. +58 −0 src/test/groovy/co/freeside/betamax/recorder/SequentialTapeWritingSpec.groovy
  18. +47 −6 src/test/groovy/co/freeside/betamax/recorder/TapeModeSpec.groovy
  19. +45 −10 src/test/groovy/co/freeside/betamax/tape/MultiThreadedTapeAccessSpec.groovy
  20. +6 −11 src/test/groovy/co/freeside/betamax/util/server/EchoHandler.groovy
  21. +5 −7 src/test/groovy/co/freeside/betamax/util/server/HelloHandler.groovy
  22. +23 −0 src/test/groovy/co/freeside/betamax/util/server/IncrementingHandler.groovy
  23. +43 −0 src/test/resources/betamax/tapes/rest_conversation_tape.yaml
  24. +33 −0 src/test/resources/betamax/tapes/sequential_tape.yaml
View
@@ -13,7 +13,7 @@ buildscript {
}
}
-version = '1.1.2'
+version = '1.2-SNAPSHOT'
group = 'co.freeside'
archivesBaseName = 'betamax'
View
@@ -2,7 +2,7 @@
title: Home
layout: index
version: 1.1.2
-dev-version: 1.1.2-SNAPSHOT
+dev-version: 1.2-SNAPSHOT
---
## Introduction
@@ -23,6 +23,8 @@ Tapes are stored to disk as [YAML][yaml] files and can be modified (or even crea
The current stable version of Betamax is _{{ page.version }}_.
+The current development version of Betamax is _{{page.dev-version}}_.
+
## Implementations
Betamax comes in two flavors. The first is an HTTP and HTTPS proxy that can intercept traffic made in any way that respects Java's `http.proxyHost` and `http.proxyPort` system properties. The second is a simple wrapper for Apache _HttpClient_.
@@ -37,10 +39,12 @@ The _HttpClient_ wrapper is a simpler implementation but only works with _HttpCl
## Installation
-Stable versions of Betamax are available from the Maven central repository. Stable and development versions are available from the [Sonatype OSS Maven repository][sonatype]. To install with your favourite build system see below:
+Stable versions of Betamax are available from the Maven central repository. Stable and development versions are available from the [Sonatype OSS Maven repository][sonatype]. To install with your favourite build system see below.
Please note the Maven group changed between versions 1.0 and 1.1. Make sure you are specifying the group `co.freeside` when referencing Betamax in your build.
+If you are installing a development version you will need to add the repository `http://oss.sonatype.org/content/groups/public/` to your build.
+
### Gradle
To use Betamax in a project using [Gradle][gradle] add the following dependency to your `build.gradle` file:
@@ -139,7 +143,7 @@ By default recorded interactions are matched based on the _method_ and _URI_ of
### Tape modes
-Betamax supports three different read/write modes for tapes. The tape mode is set by adding a `mode` argument to the `@Betamax` annotation.
+Betamax supports different read/write modes for tapes. The tape mode is set by adding a `mode` argument to the `@Betamax` annotation.
`READ_WRITE`
: This is the default mode. If the proxy intercepts a request that matches a recording on the tape then the recorded response is played back. Otherwise the request is forwarded to the target URI and the response recorded.
@@ -150,6 +154,12 @@ Betamax supports three different read/write modes for tapes. The tape mode is se
`WRITE_ONLY`
: The proxy will always forward the request to the target URI and record the response regardless of whether or not a matching request is already on the tape. Any existing recorded interactions will be overwritten.
+`READ_SEQUENTIAL`
+: The proxy will replay recordings from the tape in strict sequential order. If the current request does not match the next recorded request on the tape an error is raised. Likewise if a request arrives after all the recordings have already been played back an error is raised. This is primarily useful for testing stateful endpoints. Note that in this mode multiple recordings that match the current request may exist on the tape.
+
+`WRITE_SEQUENTIAL`
+: The proxy will behave as per `WRITE_ONLY` except that no matching on existing requests is done. All requests are recorded in sequence regardless of whether they match an existing recording or not. This mode is intended for preparing tapes for use with `READ_SEQUENTIAL` mode.
+
### Ignoring certain hosts
Sometimes you may need to have Betamax ignore traffic to certain hosts. A typical example would be if you are using Betamax when end-to-end testing a web application using something like _[HtmlUnit][htmlunit]_ - you would not want Betamax to intercept connections to _localhost_ as that would mean traffic between _HtmlUnit_ and your app was recorded and played back!
@@ -303,6 +313,7 @@ If your project gets dependencies from a [Maven][maven] repository these depende
* [Marcin Erdmann](https://github.com/erdi)
* [Lari Hotari](https://github.com/lhotari)
+* [Steve Ims](https://github.com/steveims)
* [Nobuhiro Sue](https://github.com/nobusue)
### Acknowledgements
@@ -18,17 +18,21 @@ package co.freeside.betamax
enum TapeMode {
- READ_WRITE(true, true),
- READ_ONLY(true, false),
- WRITE_ONLY(false, true),
- DEFAULT(false, false)
+ READ_WRITE(true, true, false),
+ READ_ONLY(true, false, false),
+ READ_SEQUENTIAL(true, false, true),
+ WRITE_ONLY(false, true, false),
+ WRITE_SEQUENTIAL(false, true, true),
+ DEFAULT(false, false, false)
final boolean readable
final boolean writable
+ final boolean sequential
- private TapeMode(boolean readable, boolean writable) {
+ private TapeMode(boolean readable, boolean writable, boolean sequential) {
this.readable = readable
this.writable = writable
+ this.sequential = sequential
}
boolean asBoolean() {
@@ -3,8 +3,7 @@ package co.freeside.betamax.handler
import groovy.transform.InheritConstructors
/**
- * Thrown to indicates an exception with some part of the handling chain. The HTTP status that should be returned to the
- * client is specified.
+ * Thrown to indicates an exception with some part of the handling chain.
*/
@InheritConstructors
abstract class HandlerException extends RuntimeException {
@@ -4,8 +4,7 @@ import java.util.logging.Logger
import co.freeside.betamax.Recorder
import co.freeside.betamax.message.*
import static co.freeside.betamax.proxy.jetty.BetamaxProxy.X_BETAMAX
-import static java.net.HttpURLConnection.HTTP_FORBIDDEN
-import static java.util.logging.Level.INFO
+
/**
* Reads the tape to find a matching exchange, returning the response if found otherwise proceeding the request, storing
* & returning the new response.
@@ -25,7 +24,7 @@ class TapeReader extends ChainedHttpHandler {
if (!tape) {
throw new NoTapeException()
} else if (tape.readable && tape.seek(request)) {
- log.log INFO, "Playing back from '$tape.name'"
+ log.info "Playing back from '$tape.name'"
def response = tape.play(request)
response.addHeader(X_BETAMAX, 'PLAY')
response
@@ -2,9 +2,9 @@ package co.freeside.betamax.handler
import java.util.logging.Logger
import co.freeside.betamax.Recorder
+import co.freeside.betamax.handler.*
import co.freeside.betamax.message.*
import static co.freeside.betamax.proxy.jetty.BetamaxProxy.X_BETAMAX
-import static java.net.HttpURLConnection.HTTP_FORBIDDEN
import static java.util.logging.Level.INFO
class TapeWriter extends ChainedHttpHandler {
@@ -1,18 +1,12 @@
package co.freeside.betamax.handler
-import co.freeside.betamax.message.Request
-import co.freeside.betamax.message.Response
+import co.freeside.betamax.message.*
import co.freeside.betamax.message.httpclient.HttpResponseAdapter
-import org.apache.http.HttpHost
-import org.apache.http.HttpRequest
-import org.apache.http.HttpRequestFactory
+import org.apache.http.*
import org.apache.http.client.HttpClient
import org.apache.http.entity.ByteArrayEntity
import org.apache.http.impl.DefaultHttpRequestFactory
-
import static co.freeside.betamax.proxy.jetty.BetamaxProxy.VIA_HEADER
-import static java.net.HttpURLConnection.HTTP_BAD_GATEWAY
-import static java.net.HttpURLConnection.HTTP_GATEWAY_TIMEOUT
import static org.apache.http.HttpHeaders.VIA
class TargetConnector implements HttpHandler {
@@ -27,12 +27,24 @@ abstract class RecordedMessage extends AbstractMessage implements Message {
@Override
final Reader getBodyAsText() {
- def string = body instanceof String ? body : getEncoder().decode(bodyAsBinary, charset)
+ String string
+ if (body) {
+ string = body instanceof String ? body : getEncoder().decode(bodyAsBinary, charset)
+ } else {
+ string = ''
+ }
+
new StringReader(string)
}
final InputStream getBodyAsBinary() {
- byte[] bytes = body instanceof String ? getEncoder().encode(body, charset) : body
+ byte [] bytes
+ if (body) {
+ bytes = body instanceof String ? getEncoder().encode(body, charset) : body
+ } else {
+ bytes = new byte [0]
+ }
+
new ByteArrayInputStream(bytes)
}
@@ -2,16 +2,17 @@ package co.freeside.betamax.proxy.jetty
import java.util.logging.Logger
import javax.servlet.http.*
+import co.freeside.betamax.handler.HandlerException
import co.freeside.betamax.message.Response
import co.freeside.betamax.message.servlet.ServletRequestAdapter
+import co.freeside.betamax.proxy.handler.HttpHandler
import co.freeside.betamax.handler.HttpHandler
import co.freeside.betamax.handler.HandlerException
-import co.freeside.betamax.proxy.handler.*
import org.eclipse.jetty.server.Request
import org.eclipse.jetty.server.handler.AbstractHandler
-import static java.net.HttpURLConnection.HTTP_INTERNAL_ERROR
import static java.util.logging.Level.SEVERE
import static org.apache.http.HttpHeaders.VIA
+import static org.apache.http.HttpStatus.SC_INTERNAL_SERVER_ERROR
class BetamaxProxy extends AbstractHandler {
@@ -33,7 +34,7 @@ class BetamaxProxy extends AbstractHandler {
response.sendError(e.httpStatus, e.message)
} catch (Exception e) {
log.log SEVERE, 'error recording HTTP exchange', e
- response.sendError(HTTP_INTERNAL_ERROR, e.message)
+ response.sendError(SC_INTERNAL_SERVER_ERROR, e.message)
}
}
@@ -16,6 +16,7 @@
package co.freeside.betamax.tape
+import java.util.concurrent.atomic.AtomicInteger
import co.freeside.betamax.TapeMode
import co.freeside.betamax.message.*
import co.freeside.betamax.message.tape.*
@@ -24,6 +25,7 @@ import static TapeMode.READ_WRITE
import static co.freeside.betamax.MatchRule.*
import static co.freeside.betamax.proxy.jetty.BetamaxProxy.X_BETAMAX
import static org.apache.http.HttpHeaders.VIA
+
/**
* Represents a set of recorded HTTP interactions that can be played back or appended to.
*/
@@ -32,6 +34,7 @@ class MemoryTape implements Tape {
String name
List<RecordedInteraction> interactions = []
private TapeMode mode = READ_WRITE
+ private AtomicInteger orderedIndex = new AtomicInteger()
private Comparator<Request>[] matchRules = [method, uri]
void setMode(TapeMode mode) {
@@ -50,28 +53,65 @@ class MemoryTape implements Tape {
mode.writable
}
+ boolean isSequential() {
+ mode.sequential
+ }
+
int size() {
interactions.size()
}
-
boolean seek(Request request) {
- findMatch(request) >= 0
+ if (sequential) {
+ // TODO: it's a complete waste of time using an AtomicInteger when this method is called before play in a non-transactional way
+ def index = orderedIndex.get()
+ def nextRequest = interactions[index]?.request
+ def requestMatcher = new RequestMatcher(request, matchRules)
+ nextRequest && requestMatcher.matches(nextRequest)
+ } else {
+ findMatch(request) >= 0
+ }
+ }
+
+ boolean isAtEnd() {
+ sequential && !writable && orderedIndex.get() >= interactions.size()
}
Response play(Request request) {
if (!mode.readable) {
throw new IllegalStateException('the tape is not readable')
}
- int position = findMatch(request)
- if (position < 0) {
- throw new IllegalStateException('no matching recording found')
+ if (mode.sequential) {
+ def requestMatcher = new RequestMatcher(request, matchRules)
+ def nextIndex = orderedIndex.getAndIncrement()
+ def nextInteraction = interactions[nextIndex]
+ if (!nextInteraction) {
+ throw new IllegalStateException("No recording found at position $nextIndex")
+ } else if (!requestMatcher.matches(nextInteraction.request)) {
+ throw new IllegalStateException("Request ${stringify(request)} does not match recorded request ${stringify(nextInteraction.request)}")
+ } else {
+ nextInteraction.response
+ }
} else {
- interactions[position].response
+ int position = findMatch(request)
+ if (position < 0) {
+ throw new IllegalStateException('no matching recording found')
+ } else {
+ interactions[position].response
+ }
}
}
+ private String stringify(Request request) {
+ [
+ method: request.method,
+ uri: request.uri,
+ headers: request.headers,
+ body: request.bodyAsText.text
+ ]
+ }
+
synchronized void record(Request request, Response response) {
if (!mode.writable) {
throw new IllegalStateException('the tape is not writable')
@@ -83,11 +123,16 @@ class MemoryTape implements Tape {
recorded: new Date()
)
- int position = findMatch(request)
- if (position >= 0) {
- interactions[position] = interaction
- } else {
+ if (mode.sequential) {
interactions << interaction
+
+ } else {
+ int position = findMatch(request)
+ if (position >= 0) {
+ interactions[position] = interaction
+ } else {
+ interactions << interaction
+ }
}
}
@@ -17,6 +17,7 @@
package co.freeside.betamax.tape
import co.freeside.betamax.TapeMode
+import co.freeside.betamax.handler.HandlerException
import co.freeside.betamax.message.Request
import co.freeside.betamax.message.Response
/**
@@ -44,11 +45,22 @@ interface Tape {
*/
boolean isWritable()
+ /**
+ * @return `true` if access is sequential, `false` otherwise.
+ */
+ boolean isSequential()
+
/**
* @return the number of recorded HTTP interactions currently stored on the tape.
*/
int size()
+ /**
+ * @return `true` if access is read-only, sequential and all interactions have been replayed already, `false`
+ * otherwise.
+ */
+ boolean isAtEnd()
+
/**
* Attempts to find a recorded interaction on the tape that matches the supplied request.
* @param request the HTTP request to match.
@@ -61,7 +73,7 @@ interface Tape {
* @param request the HTTP request to match.
* @throws IllegalStateException if no matching recorded interaction exists.
*/
- Response play(Request request)
+ Response play(Request request) throws HandlerException
/**
* Records a new interaction to the tape. If `request` matches an existing interaction this method will overwrite
Oops, something went wrong.

0 comments on commit 6c50089

Please sign in to comment.