Merge pull request #4 from TouK/max.uses.cyclic

Add limited mock uses
This commit is contained in:
Piotr Fus 2020-08-10 11:52:48 +02:00 committed by GitHub
commit 6044b3a275
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 369 additions and 51 deletions

View file

@ -1,5 +1,5 @@
language: groovy
jdk:
- oraclejdk8
- openjdk8

114
README.md
View file

@ -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`.

View file

@ -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>

View file

@ -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)
}
}

View file

@ -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)
}
}
}

View file

@ -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
}

View file

@ -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)
}
}