Permalink
Browse files

Support READ_SEQUENTIAL and WRITE_SEQUENTIAL modes

Originally provided by @steveims

Fixes #7
  • Loading branch information...
1 parent 0fbfb13 commit 1f7166b5a10772ad6ae79ea03817ed0d2b9469c4 Rob Fletcher committed Oct 22, 2012
Showing with 581 additions and 151 deletions.
  1. +1 −1 build.gradle
  2. +8 −1 src/docs/index.md
  3. +9 −5 src/main/groovy/co/freeside/betamax/TapeMode.groovy
  4. +1 −1 src/main/groovy/co/freeside/betamax/{proxy → }/handler/ChainedHttpHandler.groovy
  5. +13 −0 src/main/groovy/co/freeside/betamax/handler/HandlerException.groovy
  6. +1 −1 src/main/groovy/co/freeside/betamax/{proxy → }/handler/HeaderFilter.groovy
  7. +1 −1 src/main/groovy/co/freeside/betamax/{proxy → }/handler/HttpHandler.groovy
  8. +15 −0 src/main/groovy/co/freeside/betamax/handler/NoTapeException.groovy
  9. +15 −0 src/main/groovy/co/freeside/betamax/handler/NonWritableTapeException.groovy
  10. +5 −6 src/main/groovy/co/freeside/betamax/{proxy → }/handler/TapeReader.groovy
  11. +4 −4 src/main/groovy/co/freeside/betamax/{proxy → }/handler/TapeWriter.groovy
  12. +6 −11 src/main/groovy/co/freeside/betamax/{proxy → }/handler/TargetConnector.groovy
  13. +15 −0 src/main/groovy/co/freeside/betamax/handler/TargetErrorException.groovy
  14. +16 −0 src/main/groovy/co/freeside/betamax/handler/TargetTimeoutException.groovy
  15. +14 −2 src/main/groovy/co/freeside/betamax/message/tape/RecordedMessage.groovy
  16. +0 −20 src/main/groovy/co/freeside/betamax/proxy/handler/ProxyException.groovy
  17. +7 −4 src/main/groovy/co/freeside/betamax/proxy/jetty/BetamaxProxy.groovy
  18. +1 −1 src/main/groovy/co/freeside/betamax/proxy/jetty/ProxyServer.groovy
  19. +55 −10 src/main/groovy/co/freeside/betamax/tape/MemoryTape.groovy
  20. +13 −1 src/main/groovy/co/freeside/betamax/tape/Tape.groovy
  21. +1 −1 src/test/groovy/co/freeside/betamax/{proxy → }/handler/ChainedHttpHandlerSpec.groovy
  22. +6 −11 src/test/groovy/co/freeside/betamax/{proxy → }/handler/TapeReaderSpec.groovy
  23. +6 −11 src/test/groovy/co/freeside/betamax/{proxy → }/handler/TapeWriterSpec.groovy
  24. +9 −14 src/test/groovy/co/freeside/betamax/{proxy → }/handler/TargetConnectorSpec.groovy
  25. +7 −10 src/test/groovy/co/freeside/betamax/proxy/jetty/BetamaxProxySpec.groovy
  26. +91 −0 src/test/groovy/co/freeside/betamax/recorder/SequentialTapeSpec.groovy
  27. +58 −0 src/test/groovy/co/freeside/betamax/recorder/SequentialTapeWritingSpec.groovy
  28. +48 −7 src/test/groovy/co/freeside/betamax/recorder/TapeModeSpec.groovy
  29. +45 −10 src/test/groovy/co/freeside/betamax/tape/MultiThreadedTapeAccessSpec.groovy
  30. +6 −11 src/test/groovy/co/freeside/betamax/util/server/EchoHandler.groovy
  31. +5 −7 src/test/groovy/co/freeside/betamax/util/server/HelloHandler.groovy
  32. +23 −0 src/test/groovy/co/freeside/betamax/util/server/IncrementingHandler.groovy
  33. +43 −0 src/test/resources/betamax/tapes/rest_conversation_tape.yaml
  34. +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
@@ -127,7 +127,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.
@@ -138,6 +138,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!
@@ -296,6 +302,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() {
@@ -1,4 +1,4 @@
-package co.freeside.betamax.proxy.handler
+package co.freeside.betamax.handler
import co.freeside.betamax.message.Request
import co.freeside.betamax.message.Response
@@ -0,0 +1,13 @@
+package co.freeside.betamax.handler
+
+import groovy.transform.InheritConstructors
+
+/**
+ * Thrown to indicates an exception with some part of the handling chain.
+ */
+@InheritConstructors
+abstract class HandlerException extends RuntimeException {
+
+ abstract int getHttpStatus()
+
+}
@@ -1,4 +1,4 @@
-package co.freeside.betamax.proxy.handler
+package co.freeside.betamax.handler
import co.freeside.betamax.message.Request
import co.freeside.betamax.message.Response
@@ -1,4 +1,4 @@
-package co.freeside.betamax.proxy.handler
+package co.freeside.betamax.handler
import co.freeside.betamax.message.Request
import co.freeside.betamax.message.Response
@@ -0,0 +1,15 @@
+package co.freeside.betamax.handler
+
+import static java.net.HttpURLConnection.HTTP_FORBIDDEN
+
+class NoTapeException extends HandlerException {
+
+ NoTapeException() {
+ super('No tape')
+ }
+
+ @Override
+ int getHttpStatus() {
+ HTTP_FORBIDDEN
+ }
+}
@@ -0,0 +1,15 @@
+package co.freeside.betamax.handler
+
+import static java.net.HttpURLConnection.HTTP_FORBIDDEN
+
+class NonWritableTapeException extends HandlerException {
+
+ NonWritableTapeException() {
+ super('Tape is not writable')
+ }
+
+ @Override
+ int getHttpStatus() {
+ HTTP_FORBIDDEN
+ }
+}
@@ -1,11 +1,10 @@
-package co.freeside.betamax.proxy.handler
+package co.freeside.betamax.handler
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.
@@ -23,16 +22,16 @@ class TapeReader extends ChainedHttpHandler {
Response handle(Request request) {
def tape = recorder.tape
if (!tape) {
- throw new ProxyException(HTTP_FORBIDDEN, 'No 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
} else if (tape.writable) {
chain(request)
} else {
- throw new ProxyException(HTTP_FORBIDDEN, 'Tape is read-only')
+ throw new NonWritableTapeException()
}
}
@@ -1,10 +1,10 @@
-package co.freeside.betamax.proxy.handler
+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 {
@@ -20,9 +20,9 @@ class TapeWriter extends ChainedHttpHandler {
Response handle(Request request) {
def tape = recorder.tape
if (!tape) {
- throw new ProxyException(HTTP_FORBIDDEN, 'No tape')
+ throw new NoTapeException()
} else if (!tape.writable) {
- throw new ProxyException(HTTP_FORBIDDEN, 'Tape is read-only')
+ throw new NonWritableTapeException()
}
def response = chain(request)
@@ -1,18 +1,13 @@
-package co.freeside.betamax.proxy.handler
+package co.freeside.betamax.handler
-import co.freeside.betamax.message.Request
-import co.freeside.betamax.message.Response
+import co.freeside.betamax.handler.*
+import co.freeside.betamax.message.*
import co.freeside.betamax.message.http.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 {
@@ -32,9 +27,9 @@ class TargetConnector implements HttpHandler {
def response = httpClient.execute(httpHost, outboundRequest)
new HttpResponseAdapter(response)
} catch (SocketTimeoutException e) {
- throw new ProxyException(HTTP_GATEWAY_TIMEOUT, "Timed out connecting to $request.uri", e)
+ throw new TargetTimeoutException(request.uri, e)
} catch (IOException e) {
- throw new ProxyException(HTTP_BAD_GATEWAY, "Problem connecting to $request.uri", e)
+ throw new TargetErrorException(request.uri, e)
}
}
@@ -0,0 +1,15 @@
+package co.freeside.betamax.handler
+
+import static java.net.HttpURLConnection.HTTP_BAD_GATEWAY
+
+class TargetErrorException extends HandlerException {
+
+ TargetErrorException(uri, Throwable cause) {
+ super("Problem connecting to $uri".toString(), cause)
+ }
+
+ @Override
+ int getHttpStatus() {
+ HTTP_BAD_GATEWAY
+ }
+}
@@ -0,0 +1,16 @@
+package co.freeside.betamax.handler
+
+import static java.net.HttpURLConnection.HTTP_GATEWAY_TIMEOUT
+
+class TargetTimeoutException extends HandlerException {
+
+ TargetTimeoutException(uri, Throwable cause) {
+ super("Timed out connecting to $uri".toString(), cause)
+ }
+
+ @Override
+ int getHttpStatus() {
+ HTTP_GATEWAY_TIMEOUT
+ }
+
+}
@@ -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)
}
@@ -1,20 +0,0 @@
-package co.freeside.betamax.proxy.handler
-
-/**
- * Thrown to indicates an exception with some part of the proxy handling chain. The HTTP status that should be returned
- * to the client is specified.
- */
-class ProxyException extends RuntimeException {
-
- final int httpStatus
-
- ProxyException(int httpStatus, String message) {
- super(message)
- this.httpStatus = httpStatus
- }
-
- ProxyException(int httpStatus, String message, Throwable cause) {
- super(message, cause)
- this.httpStatus = httpStatus
- }
-}
@@ -2,14 +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.*
+import co.freeside.betamax.proxy.handler.HttpHandler
+import co.freeside.betamax.handler.HttpHandler
+import co.freeside.betamax.handler.HandlerException
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 {
@@ -26,12 +29,12 @@ class BetamaxProxy extends AbstractHandler {
try {
def betamaxResponse = handlerChain.handle(betamaxRequest)
sendResponse(betamaxResponse, response)
- } catch (ProxyException e) {
+ } catch (HandlerException e) {
log.log SEVERE, 'exception in proxy processing', e
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)
}
}
@@ -17,7 +17,7 @@
package co.freeside.betamax.proxy.jetty
import co.freeside.betamax.*
-import co.freeside.betamax.proxy.handler.*
+import co.freeside.betamax.handler.*
import co.freeside.betamax.ssl.DummySSLSocketFactory
import co.freeside.betamax.util.*
import org.apache.http.client.HttpClient
Oops, something went wrong.

0 comments on commit 1f7166b

Please sign in to comment.