commit
6044b3a275
7 changed files with 369 additions and 51 deletions
|
@ -1,5 +1,5 @@
|
|||
language: groovy
|
||||
|
||||
jdk:
|
||||
- oraclejdk8
|
||||
- openjdk8
|
||||
|
||||
|
|
114
README.md
114
README.md
|
@ -30,39 +30,41 @@ Configuration file is groovy configuration script e.g. :
|
|||
|
||||
```groovy
|
||||
testRest2 {
|
||||
port=9998
|
||||
response='{ req -> \'<response/>\' }'
|
||||
responseHeaders='{ _ -> [a: "b"] }'
|
||||
path='testEndpoint'
|
||||
predicate='{ req -> req.xml.name() == \'request1\'}'
|
||||
name='testRest2'
|
||||
port=9998
|
||||
response='{ req -> \'<response/>\' }'
|
||||
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:<PORT>/serverControl
|
|||
<responseHeaders>...</responseHeaders>
|
||||
<schema>...</schema>
|
||||
<imports alias="..." fullClassName="..."/>
|
||||
<maxUses>...</maxUses>
|
||||
<cyclic>...</cyclic>
|
||||
<https>
|
||||
<keystorePath>/tmp/keystore.jks</keystorePath>
|
||||
<keystorePassword>keystorePass</keystorePassword>
|
||||
|
@ -165,6 +171,8 @@ Send POST request to localhost:<PORT>/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 -> \'<response/>\' }'
|
||||
responseHeaders='{ _ -> [a: "b"] }'
|
||||
path='testEndpoint'
|
||||
predicate='{ req -> req.xml.name() == \'request1\'}'
|
||||
name='testRest2'
|
||||
port=9998
|
||||
response='{ req -> \'<response/>\' }'
|
||||
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:
|
|||
...
|
||||
</project>
|
||||
```
|
||||
|
||||
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`.
|
||||
|
|
|
@ -25,6 +25,8 @@
|
|||
<xs:element name="schema" type="xs:string" minOccurs="0"/>
|
||||
<xs:element name="imports" type="common:importAlias" minOccurs="0" maxOccurs="unbounded"/>
|
||||
<xs:element name="preserveHistory" type="xs:boolean" minOccurs="0"/>
|
||||
<xs:element name="maxUses" type="xs:int" minOccurs="0" />
|
||||
<xs:element name="cyclic" type="xs:boolean" minOccurs="0" default="false" />
|
||||
</xs:sequence>
|
||||
</xs:extension>
|
||||
</xs:complexContent>
|
||||
|
|
|
@ -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('<request/>', 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('<request/>', 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('<request/>', 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('<request/>', 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('<otherRequest/>', 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('<request/>', 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)
|
||||
}
|
||||
}
|
|
@ -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<Mock> 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -37,6 +37,9 @@ class Mock implements Comparable<Mock> {
|
|||
Map<String, String> 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<Mock> {
|
|||
}
|
||||
}
|
||||
|
||||
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<Mock> {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
boolean hasLimitedUses() {
|
||||
return maxUses > 0
|
||||
}
|
||||
|
||||
void decrementUses() {
|
||||
usesLeft--
|
||||
}
|
||||
|
||||
boolean shouldBeRemoved() {
|
||||
return hasLimitedUses() && usesLeft <= 0
|
||||
}
|
||||
|
||||
boolean shouldUsesBeReset() {
|
||||
return shouldBeRemoved() && cyclic
|
||||
}
|
||||
|
||||
void resetUses() {
|
||||
setMaxUses(maxUses)
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue