diff --git a/.travis.yml b/.travis.yml
index bfa78aa..672c068 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,5 +1,5 @@
language: groovy
jdk:
- - oraclejdk8
+ - openjdk8
diff --git a/README.md b/README.md
index 2552e18..7439888 100644
--- a/README.md
+++ b/README.md
@@ -30,39 +30,41 @@ Configuration file is groovy configuration script e.g. :
```groovy
testRest2 {
- port=9998
- response='{ req -> \'\' }'
- responseHeaders='{ _ -> [a: "b"] }'
- path='testEndpoint'
- predicate='{ req -> req.xml.name() == \'request1\'}'
- name='testRest2'
+ port=9998
+ response='{ req -> \'\' }'
+ responseHeaders='{ _ -> [a: "b"] }'
+ path='testEndpoint'
+ predicate='{ req -> req.xml.name() == \'request1\'}'
+ name='testRest2'
}
testRest4 {
- soap=true
- port=9999
- path='testEndpoint'
- name='testRest4'
- method='PUT'
- statusCode=204
+ soap=true
+ port=9999
+ path='testEndpoint'
+ name='testRest4'
+ method='PUT'
+ statusCode=204
}
testRest3 {
- port=9999
- path='testEndpoint2'
- name='testRest3'
+ port=9999
+ path='testEndpoint2'
+ name='testRest3'
}
testRest6 {
- port=9999
- path='testEndpoint2'
- name='testRest6'
+ port=9999
+ path='testEndpoint2'
+ name='testRest6'
+ maxUses=1
+ cyclic=true
}
testRest {
- imports {
- aaa='bbb'
- ccc='bla'
- }
- port=10001
- path='testEndpoint'
- name='testRest'
+ imports {
+ aaa='bbb'
+ ccc='bla'
+ }
+ port=10001
+ path='testEndpoint'
+ name='testRest'
}
testHttps {
soap=false
@@ -112,6 +114,8 @@ remoteMockServer.addMock(new AddMock(
method: ...,
responseHeaders: ...,
schema: ...,
+ maxUses: ...,
+ cyclic: ...,
https: new Https(
keystorePath: '/tmp/keystore.jks',
keystorePassword: 'keystorePass',
@@ -140,6 +144,8 @@ Send POST request to localhost:/serverControl
...
...
+ ...
+ ...
/tmp/keystore.jks
keystorePass
@@ -165,6 +171,8 @@ Send POST request to localhost:/serverControl
- schema - path to xsd schema file on mockserver classpath; default empty, so no vallidation of request is performed; if validation fails then response has got status 400 and response is raw message from validator
- imports - list of imports for closures (each import is separate tag); `alias` is the name of `fullClassName` available in closure; `fullClassName` must be available on classpath of mock server
- https - HTTPS configuration
+- maxUses - limit uses of mock to the specific number, after that mock is removed (any negative number means unlimited - default, cannot set value to 0)
+- cyclic - should mock be added after `maxUses` uses at the end of the mock list (by default false)
#### HTTPS configuration
@@ -376,39 +384,39 @@ Response:
```groovy
testRest2 {
- port=9998
- response='{ req -> \'\' }'
- responseHeaders='{ _ -> [a: "b"] }'
- path='testEndpoint'
- predicate='{ req -> req.xml.name() == \'request1\'}'
- name='testRest2'
+ port=9998
+ response='{ req -> \'\' }'
+ responseHeaders='{ _ -> [a: "b"] }'
+ path='testEndpoint'
+ predicate='{ req -> req.xml.name() == \'request1\'}'
+ name='testRest2'
}
testRest4 {
- soap=true
- port=9999
- path='testEndpoint'
- name='testRest4'
- method='PUT'
- statusCode=204
+ soap=true
+ port=9999
+ path='testEndpoint'
+ name='testRest4'
+ method='PUT'
+ statusCode=204
}
testRest3 {
- port=9999
- path='testEndpoint2'
- name='testRest3'
+ port=9999
+ path='testEndpoint2'
+ name='testRest3'
}
testRest6 {
- port=9999
- path='testEndpoint2'
- name='testRest6'
+ port=9999
+ path='testEndpoint2'
+ name='testRest6'
}
testRest {
- imports {
- aaa='bbb'
- ccc='bla'
- }
- port=10001
- path='testEndpoint'
- name='testRest'
+ imports {
+ aaa='bbb'
+ ccc='bla'
+ }
+ port=10001
+ path='testEndpoint'
+ name='testRest'
}
```
@@ -435,3 +443,9 @@ Just add repository to maven pom:
...
```
+
+FAQ
+---
+
+Q: *Can I have two mocks returning responses interchangeably for the same request?*
+A: Yes, you can. Just set two mocks with `maxUses: 1` and `cyclic: true`.
diff --git a/mockserver-api/src/main/xsd/pl/touk/mockserver/api/request.xsd b/mockserver-api/src/main/xsd/pl/touk/mockserver/api/request.xsd
index 23786bc..25287c8 100644
--- a/mockserver-api/src/main/xsd/pl/touk/mockserver/api/request.xsd
+++ b/mockserver-api/src/main/xsd/pl/touk/mockserver/api/request.xsd
@@ -25,6 +25,8 @@
+
+
diff --git a/mockserver-tests/src/test/groovy/pl/touk/mockserver/tests/MockServerMaxUsesTest.groovy b/mockserver-tests/src/test/groovy/pl/touk/mockserver/tests/MockServerMaxUsesTest.groovy
new file mode 100644
index 0000000..08d2c12
--- /dev/null
+++ b/mockserver-tests/src/test/groovy/pl/touk/mockserver/tests/MockServerMaxUsesTest.groovy
@@ -0,0 +1,239 @@
+package pl.touk.mockserver.tests
+
+
+import org.apache.http.client.methods.CloseableHttpResponse
+import org.apache.http.client.methods.HttpPost
+import org.apache.http.entity.ContentType
+import org.apache.http.entity.StringEntity
+import org.apache.http.impl.client.CloseableHttpClient
+import org.apache.http.impl.client.HttpClients
+import pl.touk.mockserver.api.request.AddMock
+import pl.touk.mockserver.client.RemoteMockServer
+import pl.touk.mockserver.server.HttpMockServer
+import spock.lang.AutoCleanup
+import spock.lang.Shared
+import spock.lang.Specification
+
+class MockServerMaxUsesTest extends Specification {
+
+ RemoteMockServer remoteMockServer
+
+ @AutoCleanup('stop')
+ HttpMockServer httpMockServer
+
+ @Shared
+ CloseableHttpClient client = HttpClients.createDefault()
+
+ def setup() {
+ httpMockServer = new HttpMockServer(9000)
+ remoteMockServer = new RemoteMockServer('localhost', 9000)
+ }
+
+ def 'should return two mocks in order'() {
+ given:'mock with predicate is given but for only one use'
+ remoteMockServer.addMock(new AddMock(
+ name: 'mock1',
+ path: 'testEndpoint',
+ port: 9999,
+ predicate: '''{req -> req.xml.name() == 'request'}''',
+ response: '''{req -> 'mock1'}''',
+ maxUses: 1
+ ))
+ and:'mock with the same predicate is given'
+ remoteMockServer.addMock(new AddMock(
+ name: 'mock2',
+ path: 'testEndpoint',
+ port: 9999,
+ predicate: '''{req -> req.xml.name() == 'request'}''',
+ response: '''{req -> 'mock2'}''',
+ ))
+ when:'we call the first time'
+ HttpPost restPost = new HttpPost('http://localhost:9999/testEndpoint')
+ restPost.entity = new StringEntity('', ContentType.create("text/xml", "UTF-8"))
+ CloseableHttpResponse response = client.execute(restPost)
+ then:'first mock should be returned and expired'
+ response.entity.content.text == 'mock1'
+ when:'we call the second time using the same request'
+ CloseableHttpResponse response2 = client.execute(restPost)
+ then:'second mock should be returned'
+ response2.entity.content.text == 'mock2'
+ when:'we call the third time using the same request'
+ CloseableHttpResponse response3 = client.execute(restPost)
+ then:'second mock should be returned, because it has unlimited uses'
+ response3.entity.content.text == 'mock2'
+ }
+
+ def 'should return two mocks in order but only once'() {
+ given:'mock with predicate is given but for only one use'
+ remoteMockServer.addMock(new AddMock(
+ name: 'mock1',
+ path: 'testEndpoint',
+ port: 9999,
+ predicate: '''{req -> req.xml.name() == 'request'}''',
+ response: '''{req -> 'mock1'}''',
+ maxUses: 1
+ ))
+ and:'mock with the same predicate is given'
+ remoteMockServer.addMock(new AddMock(
+ name: 'mock2',
+ path: 'testEndpoint',
+ port: 9999,
+ predicate: '''{req -> req.xml.name() == 'request'}''',
+ response: '''{req -> 'mock2'}''',
+ maxUses: 1,
+ ))
+ when:'we call the first time'
+ HttpPost restPost = new HttpPost('http://localhost:9999/testEndpoint')
+ restPost.entity = new StringEntity('', ContentType.create("text/xml", "UTF-8"))
+ CloseableHttpResponse response = client.execute(restPost)
+ then:'first mock should be returned and expired'
+ response.entity.content.text == 'mock1'
+ when:'we call the second time using the same request'
+ CloseableHttpResponse response2 = client.execute(restPost)
+ then:'second mock should be returned'
+ response2.entity.content.text == 'mock2'
+ when:'we call the third time using the same request'
+ CloseableHttpResponse response3 = client.execute(restPost)
+ then:'no mock should be found'
+ response3.statusLine.statusCode == 404
+ }
+
+ def 'should return two mocks in cyclic order'() {
+ given:'mock with predicate is given but for only one use'
+ remoteMockServer.addMock(new AddMock(
+ name: 'mock1',
+ path: 'testEndpoint',
+ port: 9999,
+ predicate: '''{req -> req.xml.name() == 'request'}''',
+ response: '''{req -> 'mock1'}''',
+ maxUses: 1,
+ cyclic: true,
+ preserveHistory: true
+ ))
+ and:'mock with the same predicate is given'
+ remoteMockServer.addMock(new AddMock(
+ name: 'mock2',
+ path: 'testEndpoint',
+ port: 9999,
+ predicate: '''{req -> req.xml.name() == 'request'}''',
+ response: '''{req -> 'mock2'}''',
+ maxUses: 1,
+ cyclic: true
+ ))
+ when:'we call the first time'
+ HttpPost restPost = new HttpPost('http://localhost:9999/testEndpoint')
+ restPost.entity = new StringEntity('', ContentType.create("text/xml", "UTF-8"))
+ CloseableHttpResponse response = client.execute(restPost)
+ then:'first mock should be returned and expired'
+ response.entity.content.text == 'mock1'
+ when:'we call the second time using the same request'
+ CloseableHttpResponse response2 = client.execute(restPost)
+ then:'second mock should be returned and expired'
+ response2.entity.content.text == 'mock2'
+ when:'we call the third time using the same request'
+ CloseableHttpResponse response3 = client.execute(restPost)
+ then:'first mock should be returned, because these mocks are cyclic'
+ response3.entity.content.text == 'mock1'
+ when:'we call the fourth time using the same request'
+ CloseableHttpResponse response4 = client.execute(restPost)
+ then:'second mock should be returned, because these mocks are cyclic'
+ response4.entity.content.text == 'mock2'
+ and:
+ remoteMockServer.peekMock('mock1').size() == 2
+ }
+
+ def 'should return two mocks with the same request interjected by another'() {
+ given:'mock with predicate is given but for only one use'
+ remoteMockServer.addMock(new AddMock(
+ name: 'mock1',
+ path: 'testEndpoint',
+ port: 9999,
+ predicate: '''{req -> req.xml.name() == 'request'}''',
+ response: '''{req -> 'mock1'}''',
+ maxUses: 1,
+ cyclic: true
+ ))
+ and:'mock with the same predicate is given'
+ remoteMockServer.addMock(new AddMock(
+ name: 'mock2',
+ path: 'testEndpoint',
+ port: 9999,
+ predicate: '''{req -> req.xml.name() == 'request'}''',
+ response: '''{req -> 'mock2'}''',
+ maxUses: 1,
+ cyclic: true
+ ))
+ and:'mock with other predicate is given'
+ remoteMockServer.addMock(new AddMock(
+ name: 'otherMock',
+ path: 'testEndpoint',
+ port: 9999,
+ predicate: '''{req -> req.xml.name() == 'otherRequest'}''',
+ response: '''{req -> 'otherMock'}'''
+ ))
+ when:'we call the first time'
+ HttpPost restPost = new HttpPost('http://localhost:9999/testEndpoint')
+ restPost.entity = new StringEntity('', ContentType.create("text/xml", "UTF-8"))
+ CloseableHttpResponse response = client.execute(restPost)
+ then:'first mock should be returned and expired'
+ response.entity.content.text == 'mock1'
+ when:'we call other request'
+ HttpPost otherRestPost = new HttpPost('http://localhost:9999/testEndpoint')
+ otherRestPost.entity = new StringEntity('', ContentType.create("text/xml", "UTF-8"))
+ CloseableHttpResponse otherResponse = client.execute(otherRestPost)
+ then:'other mock should be called'
+ otherResponse.entity.content.text == 'otherMock'
+ when:'we call the second time using the same request'
+ CloseableHttpResponse response2 = client.execute(restPost)
+ then:'second mock should be returned and expired'
+ response2.entity.content.text == 'mock2'
+ when:'we call the third time using the same request'
+ CloseableHttpResponse response3 = client.execute(restPost)
+ then:'first mock should be returned, because these mocks are cyclic'
+ response3.entity.content.text == 'mock1'
+ }
+
+ def 'should return first mock twice'() {
+ given:'mock with predicate is given but for only one use'
+ remoteMockServer.addMock(new AddMock(
+ name: 'mock1',
+ path: 'testEndpoint',
+ port: 9999,
+ predicate: '''{req -> req.xml.name() == 'request'}''',
+ response: '''{req -> 'mock1'}''',
+ maxUses: 2
+ ))
+ and:'mock with the same predicate is given'
+ remoteMockServer.addMock(new AddMock(
+ name: 'mock2',
+ path: 'testEndpoint',
+ port: 9999,
+ predicate: '''{req -> req.xml.name() == 'request'}''',
+ response: '''{req -> 'mock2'}''',
+ ))
+ when:'we call the first time'
+ HttpPost restPost = new HttpPost('http://localhost:9999/testEndpoint')
+ restPost.entity = new StringEntity('', ContentType.create("text/xml", "UTF-8"))
+ CloseableHttpResponse response = client.execute(restPost)
+ then:'first mock should be returned and expired'
+ response.entity.content.text == 'mock1'
+ when:'we call the second time using the same request'
+ CloseableHttpResponse response2 = client.execute(restPost)
+ then:'again first mock should be returned'
+ response2.entity.content.text == 'mock1'
+ when:'we call the third time using the same request'
+ CloseableHttpResponse response3 = client.execute(restPost)
+ then:'second mock should be returned'
+ response3.entity.content.text == 'mock2'
+ }
+
+ def 'should throw exception if adding mock with incorrect maxUses'() {
+ when:
+ remoteMockServer.addMock(new AddMock(
+ name: 'mock1',
+ maxUses: 0
+ ))
+ then:
+ thrown(RuntimeException)
+ }
+}
diff --git a/mockserver/src/main/groovy/pl/touk/mockserver/server/ContextExecutor.groovy b/mockserver/src/main/groovy/pl/touk/mockserver/server/ContextExecutor.groovy
index ce11c4c..7b31b22 100644
--- a/mockserver/src/main/groovy/pl/touk/mockserver/server/ContextExecutor.groovy
+++ b/mockserver/src/main/groovy/pl/touk/mockserver/server/ContextExecutor.groovy
@@ -39,6 +39,7 @@ class ContextExecutor {
try {
if (mock.match(Method.valueOf(ex.requestMethod), request)) {
log.debug("Mock ${mock.name} match request ${request.text}")
+ handleMaxUses(mock)
MockResponse httpResponse = mock.apply(request)
fillExchange(ex, httpResponse)
log.trace("Mock ${mock.name} response with body ${httpResponse.text}")
@@ -92,4 +93,22 @@ class ContextExecutor {
List getMocks() {
return mocks
}
+
+ private synchronized void handleMaxUses(Mock mock) {
+ if (mock.hasLimitedUses()) {
+ mock.decrementUses()
+ removeAndResetIfNeeded(mock)
+ log.debug("Uses left ${mock.usesLeft} of ${mock.maxUses} (is cyclic: ${mock.cyclic})")
+ }
+ }
+
+ private void removeAndResetIfNeeded(Mock mock) {
+ if (mock.shouldBeRemoved()) {
+ mocks.remove(mock)
+ }
+ if (mock.shouldUsesBeReset()) {
+ mock.resetUses()
+ mocks.add(mock)
+ }
+ }
}
diff --git a/mockserver/src/main/groovy/pl/touk/mockserver/server/HttpMockServer.groovy b/mockserver/src/main/groovy/pl/touk/mockserver/server/HttpMockServer.groovy
index 6a57f12..f0352ae 100644
--- a/mockserver/src/main/groovy/pl/touk/mockserver/server/HttpMockServer.groovy
+++ b/mockserver/src/main/groovy/pl/touk/mockserver/server/HttpMockServer.groovy
@@ -108,6 +108,9 @@ class HttpMockServer {
if (name in mockNames) {
throw new RuntimeException('mock already registered')
}
+ if (request.maxUses == 0) {
+ throw new RuntimeException('cannot set maxUses to 0')
+ }
Mock mock = mockFromRequest(request)
HttpServerWrapper child = getOrCreateChildServer(mock.port, mock.https)
child.addMock(mock)
@@ -121,6 +124,9 @@ class HttpMockServer {
if (name in mockNames) {
throw new RuntimeException('mock already registered')
}
+ if (co.maxUses == 0) {
+ throw new RuntimeException('cannot set maxUses to 0')
+ }
Mock mock = mockFromConfig(co)
HttpServerWrapper child = getOrCreateChildServer(mock.port, mock.https)
child.addMock(mock)
@@ -158,6 +164,8 @@ class HttpMockServer {
mock.schema = request.schema
mock.preserveHistory = request.preserveHistory != false
mock.https = request.https
+ mock.maxUses = request.maxUses
+ mock.cyclic = request.cyclic
return mock
}
@@ -182,6 +190,8 @@ class HttpMockServer {
requireClientAuth: co.https?.requireClientAuth?.asBoolean() ?: false
)
}
+ mock.maxUses = co.maxUses ?: null
+ mock.cyclic = co.cyclic ?: null
return mock
}
diff --git a/mockserver/src/main/groovy/pl/touk/mockserver/server/Mock.groovy b/mockserver/src/main/groovy/pl/touk/mockserver/server/Mock.groovy
index a285d6e..0a4fa42 100644
--- a/mockserver/src/main/groovy/pl/touk/mockserver/server/Mock.groovy
+++ b/mockserver/src/main/groovy/pl/touk/mockserver/server/Mock.groovy
@@ -37,6 +37,9 @@ class Mock implements Comparable {
Map imports = [:]
boolean preserveHistory = true
Https https
+ int maxUses = -1
+ int usesLeft
+ boolean cyclic
Mock(String name, String path, int port) {
if (!(name)) {
@@ -148,6 +151,17 @@ class Mock implements Comparable {
}
}
+ void setMaxUses(Integer maxUses) {
+ if (maxUses > 0) {
+ this.maxUses = maxUses
+ this.usesLeft = maxUses
+ }
+ }
+
+ void setCyclic(Boolean cyclic) {
+ this.cyclic = cyclic ?: false
+ }
+
@Override
int compareTo(Mock o) {
return name.compareTo(o.name)
@@ -165,4 +179,24 @@ class Mock implements Comparable {
}
}
}
+
+ boolean hasLimitedUses() {
+ return maxUses > 0
+ }
+
+ void decrementUses() {
+ usesLeft--
+ }
+
+ boolean shouldBeRemoved() {
+ return hasLimitedUses() && usesLeft <= 0
+ }
+
+ boolean shouldUsesBeReset() {
+ return shouldBeRemoved() && cyclic
+ }
+
+ void resetUses() {
+ setMaxUses(maxUses)
+ }
}