Add mocks configuration dump and restore

This commit is contained in:
Dominik Przybysz 2015-12-23 11:14:49 +01:00
parent c02e93edc3
commit 44f44ee392
6 changed files with 285 additions and 32 deletions

View file

@ -8,4 +8,4 @@ RUN mkdir /externalSchema
VOLUME /externalSchema VOLUME /externalSchema
CMD java -cp /mockserver.jar:/externalSchema -jar /mockserver.jar CMD java -cp /mockserver.jar:/externalSchema pl.touk.mockserver.server.Main

168
README.md
View file

@ -1,24 +1,71 @@
[![Build Status](https://img.shields.io/travis/TouK/http-mock-server/master.svg?style=flat)](https://travis-ci.org/TouK/http-mock-server) [![Build Status](https://img.shields.io/travis/TouK/http-mock-server/master.svg?style=flat)](https://travis-ci.org/TouK/http-mock-server)
# HTTP MOCK SERVER HTTP MOCK SERVER
================
## Create server jar Http Mock Server allows to mock HTTP request using groovy closures.
Create server jar
-----------------
``` ```
cd mockserver cd mockserver
mvn clean package assembly:single mvn clean package assembly:single
``` ```
## Start server Start server
------------
### Native start ### Native start
``` ```
java -jar mockserver.jar [PORT] java -jar mockserver-full.jar [PORT] [CONFIGURATION_FILE]
``` ```
Default port is 9999. Default port is 9999.
If configuration file is passed then port must be defined.
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'
}
testRest4 {
soap=true
port=9999
path='testEndpoint'
name='testRest4'
method='PUT'
statusCode=204
}
testRest3 {
port=9999
path='testEndpoint2'
name='testRest3'
}
testRest6 {
port=9999
path='testEndpoint2'
name='testRest6'
}
testRest {
imports {
aaa='bbb'
ccc='bla'
}
port=10001
path='testEndpoint'
name='testRest'
}
```
### Start with docker ### Start with docker
Docker and docker-compose is needed. Docker and docker-compose is needed.
@ -28,7 +75,8 @@ Docker and docker-compose is needed.
docker-compose up -d docker-compose up -d
``` ```
## Create mock on server Create mock on server
---------------------
### Via client ### Via client
@ -47,11 +95,11 @@ remoteMockServer.addMock(new AddMock(
schema: ... schema: ...
)) ))
``` ```
### Via HTTP ### Via HTTP
Send POST request to localhost:<PORT>/serverControl Send POST request to localhost:<PORT>/serverControl
```xml ```xml
<addMock xmlns="http://touk.pl/mockserver/api/request"> <addMock xmlns="http://touk.pl/mockserver/api/request">
<name>...</name> <name>...</name>
@ -64,33 +112,35 @@ Send POST request to localhost:<PORT>/serverControl
<method>...</method> <method>...</method>
<responseHeaders>...</responseHeaders> <responseHeaders>...</responseHeaders>
<schema>...</schema> <schema>...</schema>
<imports alias="..." fullClassName="..."/>
</addMock> </addMock>
``` ```
### Parameters ### Parameters
* name - name of mock, must be unique - name - name of mock, must be unique
* path - path on which mock should be created - path - path on which mock should be created
* port - inteer, port on which mock should be created, cannot be the same as mock server port - port - inteer, port on which mock should be created, cannot be the same as mock server port
* predicate - groovy closure as string which must evaluate to true, when request object will be given to satisfy mock, optional, default {_ -> true} - predicate - groovy closure as string which must evaluate to true, when request object will be given to satisfy mock, optional, default {_ -> true}
* response - groovy closure as string which must evaluate to string which will be response of mock when predicate is satisfied, optional, default { _ -> '' } - response - groovy closure as string which must evaluate to string which will be response of mock when predicate is satisfied, optional, default { _ -> '' }
* soap - true or false, is request and response should be wrapped in soap Envelope and Body elements, default false - soap - true or false, is request and response should be wrapped in soap Envelope and Body elements, default false
* statusCode - integer, status code of response when predicate is satisfied, default 200 - statusCode - integer, status code of response when predicate is satisfied, default 200
* method - POST|PUT|DELETE|GET|TRACE|OPTION|HEAD, expected http method of request, default POST - method - POST|PUT|DELETE|GET|TRACE|OPTION|HEAD, expected http method of request, default POST
* responseHeaders - groovyClosure as string which must evaluate to Map which will be added to response headers, default { _ -> [:] } - responseHeaders - groovyClosure as string which must evaluate to Map which will be added to response headers, default { _ -> \[:] }
* 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 - 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
### Closures request properties ### Closures request properties
In closures input parameter (called req) contains properties: In closures input parameter (called req) contains properties:
* text - request body as java.util.String - text - request body as java.util.String
* headers - java.util.Map with request headers - headers - java.util.Map with request headers
* query - java.util.Map with query parameters - query - java.util.Map with query parameters
* xml - groovy.util.slurpersupport.GPathResult created from request body (if request body is valid xml) - xml - groovy.util.slurpersupport.GPathResult created from request body (if request body is valid xml)
* soap - groovy.util.slurpersupport.GPathResult created from request body without Envelope and Body elements (if request body is valid soap xml) - soap - groovy.util.slurpersupport.GPathResult created from request body without Envelope and Body elements (if request body is valid soap xml)
* json - java.lang.Object created from request body (if request body is valid json) - json - java.lang.Object created from request body (if request body is valid json)
* path - java.util.List<String> with not empty parts of request path - path - java.util.List<String> with not empty parts of request path
Response if success: Response if success:
@ -104,7 +154,9 @@ Response with error message if failure:
<exceptionOccured xmlns="http://touk.pl/mockserver/api/response">...</exceptionOccured> <exceptionOccured xmlns="http://touk.pl/mockserver/api/response">...</exceptionOccured>
``` ```
## Peek mock Peek mock
---------
Mock could be peeked to get get report of its invocations. Mock could be peeked to get get report of its invocations.
### Via client ### Via client
@ -114,6 +166,7 @@ List<MockEvent> mockEvents = remoteMockServer.peekMock('...')
``` ```
### Via HTTP ### Via HTTP
Send POST request to localhost:<PORT>/serverControl Send POST request to localhost:<PORT>/serverControl
```xml ```xml
@ -160,7 +213,8 @@ Response with error message if failure:
<exceptionOccured xmlns="http://touk.pl/mockserver/api/response">...</exceptionOccured> <exceptionOccured xmlns="http://touk.pl/mockserver/api/response">...</exceptionOccured>
``` ```
## Remove mock Remove mock
-----------
When mock was used it could be unregistered by name. It also optionally returns report of mock invocations if second parameter is true. When mock was used it could be unregistered by name. It also optionally returns report of mock invocations if second parameter is true.
@ -169,7 +223,9 @@ When mock was used it could be unregistered by name. It also optionally returns
```java ```java
List<MockEvent> mockEvents = remoteMockServer.removeMock('...', ...) List<MockEvent> mockEvents = remoteMockServer.removeMock('...', ...)
``` ```
### Via HTTP ### Via HTTP
Send POST request to localhost:<PORT>/serverControl Send POST request to localhost:<PORT>/serverControl
```xml ```xml
@ -223,7 +279,8 @@ Response with error message if failure:
<exceptionOccured xmlns="http://touk.pl/mockserver/api/response">...</exceptionOccured> <exceptionOccured xmlns="http://touk.pl/mockserver/api/response">...</exceptionOccured>
``` ```
## List mocks definitions List mocks definitions
----------------------
### Via client ### Via client
@ -249,12 +306,69 @@ Response:
<soap>...</soap> <soap>...</soap>
<method>...</method> <method>...</method>
<statusCode>...</statusCode> <statusCode>...</statusCode>
<imports alias="..." fullClassName="..."/>
</mock> </mock>
... ...
</mocks> </mocks>
``` ```
## Remote repository Get mocks configuration
-----------------------
### Via client
```java
ConfigObject mocks = remoteMockServer.getConfiguration()
```
### Via HTTP
Send GET request to localhost:<PORT>/serverControl/configuration
Response:
```groovy
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
}
testRest3 {
port=9999
path='testEndpoint2'
name='testRest3'
}
testRest6 {
port=9999
path='testEndpoint2'
name='testRest6'
}
testRest {
imports {
aaa='bbb'
ccc='bla'
}
port=10001
path='testEndpoint'
name='testRest'
}
```
This response could be saved to file and passed as it is during mock server creation.
Remote repository
-----------------
Mockserver is available at `philanthropist.touk.pl`. Mockserver is available at `philanthropist.touk.pl`.

View file

@ -11,7 +11,11 @@ import pl.touk.mockserver.api.request.AddMock
import pl.touk.mockserver.api.request.MockServerRequest import pl.touk.mockserver.api.request.MockServerRequest
import pl.touk.mockserver.api.request.PeekMock import pl.touk.mockserver.api.request.PeekMock
import pl.touk.mockserver.api.request.RemoveMock import pl.touk.mockserver.api.request.RemoveMock
import pl.touk.mockserver.api.response.* import pl.touk.mockserver.api.response.MockEventReport
import pl.touk.mockserver.api.response.MockPeeked
import pl.touk.mockserver.api.response.MockRemoved
import pl.touk.mockserver.api.response.MockReport
import pl.touk.mockserver.api.response.Mocks
import javax.xml.bind.JAXBContext import javax.xml.bind.JAXBContext
@ -47,6 +51,13 @@ class RemoteMockServer {
return mockPeeked.mockEvents ?: [] return mockPeeked.mockEvents ?: []
} }
ConfigObject getConfiguration() {
HttpGet get = new HttpGet(address + '/configuration')
CloseableHttpResponse response = client.execute(get)
String configuration = Util.extractStringResponse(response)
return new ConfigSlurper().parse(configuration)
}
private static StringEntity buildRemoveMockRequest(RemoveMock data) { private static StringEntity buildRemoveMockRequest(RemoveMock data) {
return new StringEntity(marshallRequest(data), ContentType.create("text/xml", "UTF-8")) return new StringEntity(marshallRequest(data), ContentType.create("text/xml", "UTF-8"))
} }

View file

@ -1029,4 +1029,66 @@ class MockServerIntegrationTest extends Specification {
expect: expect:
remoteMockServer.removeMock('testRest')?.size() == 1 remoteMockServer.removeMock('testRest')?.size() == 1
} }
def "should get configuration of mocks and reconfigure new mock server based on it"() {
given:
remoteMockServer.addMock(new AddMock(
name: 'testRest2',
path: 'testEndpoint',
port: 9998,
predicate: '''{ req -> req.xml.name() == 'request1'}''',
response: '''{ req -> '<response/>' }''',
responseHeaders: '{ _ -> [a: "b"] }'
))
remoteMockServer.addMock(new AddMock(
name: 'testRest4',
path: 'testEndpoint',
port: 9999,
soap: true,
statusCode: 204,
method: Method.PUT
))
remoteMockServer.addMock(new AddMock(
name: 'testRest3',
path: 'testEndpoint2',
port: 9999
))
remoteMockServer.addMock(new AddMock(
name: 'testRest5',
path: 'testEndpoint',
port: 9999
))
remoteMockServer.addMock(new AddMock(
name: 'testRest6',
path: 'testEndpoint2',
port: 9999
))
remoteMockServer.addMock(new AddMock(
name: 'testRest',
path: 'testEndpoint',
port: 9999,
schema: 'schema2.xsd',
imports: [
new ImportAlias(alias: 'aaa', fullClassName: 'bbb'),
new ImportAlias(alias: 'ccc', fullClassName: 'bla')
]
))
remoteMockServer.removeMock('testRest5')
when:
ConfigObject configObject = remoteMockServer.configuration
httpMockServer.stop()
httpMockServer = new HttpMockServer(9000, configObject)
then:
List<MockReport> mockReport = remoteMockServer.listMocks()
mockReport.size() == 5
assertMockReport(mockReport[0], [name: 'testRest', path: 'testEndpoint', port: 9999, predicate: '{ _ -> true }', response: '''{ _ -> '' }''', responseHeaders: '{ _ -> [:] }', soap: false, statusCode: 200, method: Method.POST, schema: 'schema2.xsd'])
assertMockReport(mockReport[1], [name: 'testRest2', path: 'testEndpoint', port: 9998, predicate: '''{ req -> req.xml.name() == 'request1'}''', response: '''{ req -> '<response/>' }''', responseHeaders: '{ _ -> [a: "b"] }', soap: false, statusCode: 200, method: Method.POST])
assertMockReport(mockReport[2], [name: 'testRest3', path: 'testEndpoint2', port: 9999, predicate: '{ _ -> true }', response: '''{ _ -> '' }''', responseHeaders: '{ _ -> [:] }', soap: false, statusCode: 200, method: Method.POST])
assertMockReport(mockReport[3], [name: 'testRest4', path: 'testEndpoint', port: 9999, predicate: '{ _ -> true }', response: '''{ _ -> '' }''', responseHeaders: '{ _ -> [:] }', soap: true, statusCode: 204, method: Method.PUT])
assertMockReport(mockReport[4], [name: 'testRest6', path: 'testEndpoint2', port: 9999, predicate: '{ _ -> true }', response: '''{ _ -> '' }''', responseHeaders: '{ _ -> [:] }', soap: false, statusCode: 200, method: Method.POST])
mockReport[0].imports.find { it.alias == 'aaa' }?.fullClassName == 'bbb'
mockReport[0].imports.find { it.alias == 'ccc' }?.fullClassName == 'bla'
}
} }

View file

@ -3,6 +3,7 @@ package pl.touk.mockserver.server
import com.sun.net.httpserver.HttpExchange import com.sun.net.httpserver.HttpExchange
import groovy.util.logging.Slf4j import groovy.util.logging.Slf4j
import pl.touk.mockserver.api.common.ImportAlias import pl.touk.mockserver.api.common.ImportAlias
import pl.touk.mockserver.api.common.Method
import pl.touk.mockserver.api.request.AddMock import pl.touk.mockserver.api.request.AddMock
import pl.touk.mockserver.api.request.MockServerRequest import pl.touk.mockserver.api.request.MockServerRequest
import pl.touk.mockserver.api.request.PeekMock import pl.touk.mockserver.api.request.PeekMock
@ -30,18 +31,27 @@ class HttpMockServer {
private final HttpServerWraper httpServerWraper private final HttpServerWraper httpServerWraper
private final Map<Integer, HttpServerWraper> childServers = new ConcurrentHashMap<>() private final Map<Integer, HttpServerWraper> childServers = new ConcurrentHashMap<>()
private final Set<String> mockNames = new CopyOnWriteArraySet<>() private final Set<String> mockNames = new CopyOnWriteArraySet<>()
private final ConfigObject configuration = new ConfigObject()
private static private static
final JAXBContext requestJaxbContext = JAXBContext.newInstance(AddMock.package.name, AddMock.classLoader) final JAXBContext requestJaxbContext = JAXBContext.newInstance(AddMock.package.name, AddMock.classLoader)
HttpMockServer(int port = 9999) { HttpMockServer(int port = 9999, ConfigObject initialConfiguration = new ConfigObject()) {
httpServerWraper = new HttpServerWraper(port) httpServerWraper = new HttpServerWraper(port)
initialConfiguration.values()?.each { ConfigObject co ->
addMock(co)
}
httpServerWraper.createContext('/serverControl', { httpServerWraper.createContext('/serverControl', {
HttpExchange ex -> HttpExchange ex ->
try { try {
if (ex.requestMethod == 'GET') { if (ex.requestMethod == 'GET') {
if (ex.requestURI.path == '/serverControl/configuration') {
createResponse(ex, configuration.prettyPrint(), 200)
} else {
listMocks(ex) listMocks(ex)
}
} else if (ex.requestMethod == 'POST') { } else if (ex.requestMethod == 'POST') {
MockServerRequest request = requestJaxbContext.createUnmarshaller().unmarshal(ex.requestBody) as MockServerRequest MockServerRequest request = requestJaxbContext.createUnmarshaller().unmarshal(ex.requestBody) as MockServerRequest
if (request instanceof AddMock) { if (request instanceof AddMock) {
@ -95,10 +105,41 @@ class HttpMockServer {
Mock mock = mockFromRequest(request) Mock mock = mockFromRequest(request)
HttpServerWraper child = getOrCreateChildServer(mock.port) HttpServerWraper child = getOrCreateChildServer(mock.port)
child.addMock(mock) child.addMock(mock)
saveConfiguration(request)
mockNames << name mockNames << name
createResponse(ex, new MockAdded(), 200) createResponse(ex, new MockAdded(), 200)
} }
private void addMock(ConfigObject co) {
String name = co.name
if (name in mockNames) {
throw new RuntimeException('mock already registered')
}
Mock mock = mockFromConfig(co)
HttpServerWraper child = getOrCreateChildServer(mock.port)
child.addMock(mock)
configuration.put(name, co)
mockNames << name
}
private void saveConfiguration(AddMock request) {
ConfigObject mockDefinition = new ConfigObject()
request.metaPropertyValues.findAll { it.name != 'class' && it.value }.each {
if (it.name == 'imports') {
ConfigObject configObject = new ConfigObject()
it.value.each { ImportAlias imp ->
configObject.put(imp.alias, imp.fullClassName)
}
mockDefinition.put(it.name, configObject)
} else if (it.name == 'method') {
mockDefinition.put(it.name, it.value.name())
} else {
mockDefinition.put(it.name, it.value)
}
}
configuration.put(request.name, mockDefinition)
}
private static Mock mockFromRequest(AddMock request) { private static Mock mockFromRequest(AddMock request) {
Mock mock = new Mock(request.name, request.path, request.port) Mock mock = new Mock(request.name, request.path, request.port)
mock.imports = request.imports?.collectEntries { [(it.alias): it.fullClassName] } ?: [:] mock.imports = request.imports?.collectEntries { [(it.alias): it.fullClassName] } ?: [:]
@ -112,6 +153,19 @@ class HttpMockServer {
return mock return mock
} }
private static Mock mockFromConfig(ConfigObject co) {
Mock mock = new Mock(co.name, co.path, co.port)
mock.imports = co.imports
mock.predicate = co.predicate ?: null
mock.response = co.response ?: null
mock.soap = co.soap ?: null
mock.statusCode = co.statusCode ?: null
mock.method = co.method ? Method.valueOf(co.method) : null
mock.responseHeaders = co.responseHeaders ?: null
mock.schema = co.schema ?: null
return mock
}
private HttpServerWraper getOrCreateChildServer(int mockPort) { private HttpServerWraper getOrCreateChildServer(int mockPort) {
HttpServerWraper child = childServers[mockPort] HttpServerWraper child = childServers[mockPort]
if (!child) { if (!child) {
@ -132,6 +186,7 @@ class HttpMockServer {
it.removeMock(name) it.removeMock(name)
}.flatten() as List<MockEvent> }.flatten() as List<MockEvent>
mockNames.remove(name) mockNames.remove(name)
configuration.remove(name)
MockRemoved mockRemoved = new MockRemoved( MockRemoved mockRemoved = new MockRemoved(
mockEvents: createMockEventReports(mockEvents) mockEvents: createMockEventReports(mockEvents)
) )

View file

@ -5,7 +5,7 @@ import groovy.util.logging.Slf4j
@Slf4j @Slf4j
class Main { class Main {
static void main(String[] args) { static void main(String[] args) {
HttpMockServer httpMockServer = args.length == 1 ? new HttpMockServer(args[0] as int) : new HttpMockServer() HttpMockServer httpMockServer = startMockServer(args)
Runtime.runtime.addShutdownHook(new Thread({ Runtime.runtime.addShutdownHook(new Thread({
log.info('Http server is stopping...') log.info('Http server is stopping...')
@ -17,4 +17,15 @@ class Main {
Thread.sleep(10000) Thread.sleep(10000)
} }
} }
private static HttpMockServer startMockServer(String... args) {
switch (args.length) {
case 1:
return new HttpMockServer(args[0] as int)
case 2:
return new HttpMockServer(args[0] as int, new ConfigSlurper().parse(new File(args[1]).toURI().toURL()))
default:
return new HttpMockServer()
}
}
} }