From 44f44ee392835edbd91d64c212242898a2013769 Mon Sep 17 00:00:00 2001 From: Dominik Przybysz Date: Wed, 23 Dec 2015 11:14:49 +0100 Subject: [PATCH] Add mocks configuration dump and restore --- Dockerfile | 2 +- README.md | 168 +++++++++++++++--- .../mockserver/client/RemoteMockServer.groovy | 13 +- .../tests/MockServerIntegrationTest.groovy | 62 +++++++ .../mockserver/server/HttpMockServer.groovy | 59 +++++- .../pl/touk/mockserver/server/Main.groovy | 13 +- 6 files changed, 285 insertions(+), 32 deletions(-) diff --git a/Dockerfile b/Dockerfile index fc44fd1..87dd9dd 100644 --- a/Dockerfile +++ b/Dockerfile @@ -8,4 +8,4 @@ RUN mkdir /externalSchema VOLUME /externalSchema -CMD java -cp /mockserver.jar:/externalSchema -jar /mockserver.jar +CMD java -cp /mockserver.jar:/externalSchema pl.touk.mockserver.server.Main diff --git a/README.md b/README.md index a06f405..139c19e 100644 --- a/README.md +++ b/README.md @@ -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) -# 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 mvn clean package assembly:single ``` -## Start server +Start server +------------ ### Native start ``` -java -jar mockserver.jar [PORT] +java -jar mockserver-full.jar [PORT] [CONFIGURATION_FILE] ``` 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 -> \'\' }' + 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 Docker and docker-compose is needed. @@ -28,7 +75,8 @@ Docker and docker-compose is needed. docker-compose up -d ``` -## Create mock on server +Create mock on server +--------------------- ### Via client @@ -47,11 +95,11 @@ remoteMockServer.addMock(new AddMock( schema: ... )) ``` + ### Via HTTP Send POST request to localhost:/serverControl - ```xml ... @@ -64,33 +112,35 @@ Send POST request to localhost:/serverControl ... ... ... + ``` ### Parameters -* name - name of mock, must be unique -* 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 -* 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 { _ -> '' } -* 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 -* 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 { _ -> [:] } -* 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 +- name - name of mock, must be unique +- 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 +- 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 { _ -> '' } +- 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 +- 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 { _ -> \[:] } +- 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 In closures input parameter (called req) contains properties: -* text - request body as java.util.String -* headers - java.util.Map with request headers -* query - java.util.Map with query parameters -* 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) -* json - java.lang.Object created from request body (if request body is valid json) -* path - java.util.List with not empty parts of request path +- text - request body as java.util.String +- headers - java.util.Map with request headers +- query - java.util.Map with query parameters +- 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) +- json - java.lang.Object created from request body (if request body is valid json) +- path - java.util.List with not empty parts of request path Response if success: @@ -104,7 +154,9 @@ Response with error message if failure: ... ``` -## Peek mock +Peek mock +--------- + Mock could be peeked to get get report of its invocations. ### Via client @@ -114,6 +166,7 @@ List mockEvents = remoteMockServer.peekMock('...') ``` ### Via HTTP + Send POST request to localhost:/serverControl ```xml @@ -160,7 +213,8 @@ Response with error message if failure: ... ``` -## 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. @@ -169,7 +223,9 @@ When mock was used it could be unregistered by name. It also optionally returns ```java List mockEvents = remoteMockServer.removeMock('...', ...) ``` + ### Via HTTP + Send POST request to localhost:/serverControl ```xml @@ -223,7 +279,8 @@ Response with error message if failure: ... ``` -## List mocks definitions +List mocks definitions +---------------------- ### Via client @@ -249,12 +306,69 @@ Response: ... ... ... + ... ``` -## Remote repository +Get mocks configuration +----------------------- + +### Via client + +```java +ConfigObject mocks = remoteMockServer.getConfiguration() +``` + +### Via HTTP + +Send GET request to localhost:/serverControl/configuration + +Response: + +```groovy +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 +} +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`. diff --git a/mockserver-client/src/main/groovy/pl/touk/mockserver/client/RemoteMockServer.groovy b/mockserver-client/src/main/groovy/pl/touk/mockserver/client/RemoteMockServer.groovy index ffbc9a0..91ebc33 100644 --- a/mockserver-client/src/main/groovy/pl/touk/mockserver/client/RemoteMockServer.groovy +++ b/mockserver-client/src/main/groovy/pl/touk/mockserver/client/RemoteMockServer.groovy @@ -11,7 +11,11 @@ import pl.touk.mockserver.api.request.AddMock import pl.touk.mockserver.api.request.MockServerRequest import pl.touk.mockserver.api.request.PeekMock 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 @@ -47,6 +51,13 @@ class RemoteMockServer { 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) { return new StringEntity(marshallRequest(data), ContentType.create("text/xml", "UTF-8")) } diff --git a/mockserver-tests/src/test/groovy/pl/touk/mockserver/tests/MockServerIntegrationTest.groovy b/mockserver-tests/src/test/groovy/pl/touk/mockserver/tests/MockServerIntegrationTest.groovy index 1412de1..6476a96 100644 --- a/mockserver-tests/src/test/groovy/pl/touk/mockserver/tests/MockServerIntegrationTest.groovy +++ b/mockserver-tests/src/test/groovy/pl/touk/mockserver/tests/MockServerIntegrationTest.groovy @@ -1029,4 +1029,66 @@ class MockServerIntegrationTest extends Specification { expect: 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 -> '' }''', + 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 = 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 -> '' }''', 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' + } + } 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 c25b990..171d72a 100644 --- a/mockserver/src/main/groovy/pl/touk/mockserver/server/HttpMockServer.groovy +++ b/mockserver/src/main/groovy/pl/touk/mockserver/server/HttpMockServer.groovy @@ -3,6 +3,7 @@ package pl.touk.mockserver.server import com.sun.net.httpserver.HttpExchange import groovy.util.logging.Slf4j 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.MockServerRequest import pl.touk.mockserver.api.request.PeekMock @@ -30,18 +31,27 @@ class HttpMockServer { private final HttpServerWraper httpServerWraper private final Map childServers = new ConcurrentHashMap<>() private final Set mockNames = new CopyOnWriteArraySet<>() + private final ConfigObject configuration = new ConfigObject() private static 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) + initialConfiguration.values()?.each { ConfigObject co -> + addMock(co) + } + httpServerWraper.createContext('/serverControl', { HttpExchange ex -> try { if (ex.requestMethod == 'GET') { - listMocks(ex) + if (ex.requestURI.path == '/serverControl/configuration') { + createResponse(ex, configuration.prettyPrint(), 200) + } else { + listMocks(ex) + } } else if (ex.requestMethod == 'POST') { MockServerRequest request = requestJaxbContext.createUnmarshaller().unmarshal(ex.requestBody) as MockServerRequest if (request instanceof AddMock) { @@ -95,10 +105,41 @@ class HttpMockServer { Mock mock = mockFromRequest(request) HttpServerWraper child = getOrCreateChildServer(mock.port) child.addMock(mock) + saveConfiguration(request) mockNames << name 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) { Mock mock = new Mock(request.name, request.path, request.port) mock.imports = request.imports?.collectEntries { [(it.alias): it.fullClassName] } ?: [:] @@ -112,6 +153,19 @@ class HttpMockServer { 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) { HttpServerWraper child = childServers[mockPort] if (!child) { @@ -132,6 +186,7 @@ class HttpMockServer { it.removeMock(name) }.flatten() as List mockNames.remove(name) + configuration.remove(name) MockRemoved mockRemoved = new MockRemoved( mockEvents: createMockEventReports(mockEvents) ) diff --git a/mockserver/src/main/groovy/pl/touk/mockserver/server/Main.groovy b/mockserver/src/main/groovy/pl/touk/mockserver/server/Main.groovy index 759a11a..aba7f6e 100644 --- a/mockserver/src/main/groovy/pl/touk/mockserver/server/Main.groovy +++ b/mockserver/src/main/groovy/pl/touk/mockserver/server/Main.groovy @@ -5,7 +5,7 @@ import groovy.util.logging.Slf4j @Slf4j class Main { 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({ log.info('Http server is stopping...') @@ -17,4 +17,15 @@ class Main { 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() + } + } }