diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8b71f67 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +*.iml +target/ +.idea + +.mvn/wrapper/maven-wrapper.jar \ No newline at end of file diff --git a/.mvn/wrapper/maven-wrapper.properties b/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 0000000..f3283b0 --- /dev/null +++ b/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1,18 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.6.3/apache-maven-3.6.3-bin.zip +wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index ad81e63..0000000 --- a/.travis.yml +++ /dev/null @@ -1,6 +0,0 @@ -language: groovy - -jdk: - - oraclejdk7 - - oraclejdk8 - diff --git a/.woodpecker.yaml b/.woodpecker.yaml new file mode 100644 index 0000000..5921e36 --- /dev/null +++ b/.woodpecker.yaml @@ -0,0 +1,59 @@ +variables: + &maven_image maven:3.9.6-eclipse-temurin-11-alpine + +when: + evaluate: 'not (CI_COMMIT_MESSAGE contains "Release")' + +steps: + - name: build + image: *maven_image + commands: + - mvn -B clean install -DskipTests -Dmaven.test.skip + - name: test + image: *maven_image + commands: + - mvn -B -pl :mockserver-tests verify + - name: deploy to public + image: *maven_image + commands: + - mvn -B jar:jar deploy:deploy + secrets: [reposilite_user, reposilite_token] + when: + branch: [dev, master] + - name: deploy to releases + image: woodpeckerci/plugin-gitea-release + settings: + base-url: https://git.ztsh.eu + files: + - "mockserver-client/target/mockserver-client*.jar" + - "mockserver/target/mockserver-full.jar" + api_key: + from_secret: git_pat + when: + - event: tag + - name: tag docker image + image: woodpeckerci/plugin-docker-buildx + settings: + platforms: linux/amd64,linux/arm64/v8,linux/ppc64le,linux/s390x + repo: ztsheu/http-mock-server + registry: docker.io + tags: ${CI_COMMIT_TAG} + username: ztsheu + password: + from_secret: docker_pat + when: + - event: tag + - name: build docker image + image: woodpeckerci/plugin-docker-buildx + settings: + platforms: linux/amd64,linux/arm64/v8,linux/ppc64le,linux/s390x + repo: ztsheu/http-mock-server + registry: docker.io + tags: latest + username: ztsheu + password: + from_secret: docker_pat + when: + - event: tag + - event: push + branch: dev diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..3250eed --- /dev/null +++ b/Dockerfile @@ -0,0 +1,11 @@ +FROM eclipse-temurin:11.0.22_7-jre-jammy + +ADD mockserver/target/mockserver-full.jar /mockserver.jar + +EXPOSE 9999 + +RUN mkdir /externalSchema + +VOLUME /externalSchema + +CMD java -cp /mockserver.jar:/externalSchema eu.ztsh.mockserver.server.Main diff --git a/README.md b/README.md index e2ce7c8..cb67c96 100644 --- a/README.md +++ b/README.md @@ -1,24 +1,109 @@ [![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 (in mockserver directory) +Http Mock Server allows to mock HTTP request using groovy closures. + +Create server jar +----------------- ``` +cd mockserver mvn clean package assembly:single ``` -## Start server on port (default 9999) +Start server +------------ + +### Native start ``` -java -jar mockserver--jar-with-dependencies.jar [PORT] +java -jar mockserver-full.jar [PORT] [CONFIGURATION_FILE] ``` -## Create mock on server via client +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' + maxUses=1 + cyclic=true +} +testRest { + imports { + aaa='bbb' + ccc='bla' + } + port=10001 + path='testEndpoint' + name='testRest' +} +testHttps { + soap=false + port=10443 + path='testHttps' + name='testHttps' + method='GET' + https={ + keystorePath='/tmp/keystore.jks' + keystorePassword='keystorePass' + keyPassword='keyPass' + truststorePath='/tmp/truststore.jks' + truststorePassword='truststorePass' + requireClientAuth=true + } +} +``` + +### Build with docker + +Docker and docker-compose is needed. + +``` +./buildImage.sh +docker-compose up -d +``` + +### Docker repoository + +Currently unavailable + +Create mock on server +--------------------- + +### Via client ```java RemoteMockServer remoteMockServer = new RemoteMockServer('localhost', ) -remoteMockServer.addMock(new AddMockRequestData( +remoteMockServer.addMock(new AddMock( name: '...', path: '...', port: ..., @@ -27,15 +112,27 @@ remoteMockServer.addMock(new AddMockRequestData( soap: ..., statusCode: ..., method: ..., - responseHeaders: ... + responseHeaders: ..., + schema: ..., + maxUses: ..., + cyclic: ..., + https: new Https( + keystorePath: '/tmp/keystore.jks', + keystorePassword: 'keystorePass', + keyPassword: 'keyPass', + truststorePath: '/tmp/truststore.jks', + truststorePassword: 'truststorePass', + requireClientAuth: true + ) )) ``` - -or via sending POST request to localhost:/serverControl +### Via HTTP + +Send POST request to localhost:/serverControl ```xml - + ... ... ... @@ -45,159 +142,210 @@ or via sending POST request to localhost:/serverControl ... ... ... + ... + + ... + ... + + /tmp/keystore.jks + keystorePass + keyPass + /tmp/truststore.jks + truststorePass + true + ``` -* 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 { _ -> [:] } +### 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|ANY_METHOD, expected http method of request, default `POST`, `ANY_METHOD` matches all HTTP methods +- 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 +- https - HTTPS configuration +- maxUses - limit uses of mock to the specific number, after that mock is marked as ignored (any negative number means unlimited - default, cannot set value to 0), after this number of invocation mock history is still available, but mock does not apply to any request +- cyclic - should mock be added after `maxUses` uses at the end of the mock list (by default false) + +#### HTTPS configuration + +- keystorePath - path to keystore in JKS format, keystore should contains only one privateKeyEntry +- keystorePassword - keystore password +- keyPassword - key password +- truststorePath - path to truststore in JKS format +- truststorePassword - truststore password +- requireClientAuth - whether client auth is required (two-way SSL) + +**HTTP** and **HTTPS** should be started on separated ports. + +### 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: ```xml - + ``` Response with error message if failure: ```xml -... +... ``` -## Mock could be peeked to get get report of its invocations. -Via client: +Peek mock +--------- + +Mock could be peeked to get get report of its invocations. + +### Via client ```java List mockEvents = remoteMockServer.peekMock('...') ``` -Via sending POST request to localhost:/serverControl +### Via HTTP + +Send POST request to localhost:/serverControl ```xml - - ... + + ... ``` Response if success: ```xml - + ... - ... +
...
...
- - ... + + ... ... - + - ... + ... ...
+ ... ... - ... +
...
...
- ...
- ...
``` Response with error message if failure: ```xml -... +... ``` -## When mock was used it could be unregistered by name. It also returns report of mock invocations. -Via client: +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. + +### Via client ```java -List mockEvents = remoteMockServer.removeMock('...') +List mockEvents = remoteMockServer.removeMock('...', ...) ``` -Via sending POST request to localhost:/serverControl +### Via HTTP + +Send POST request to localhost:/serverControl ```xml - + ... + ... ``` -Response if success: +Response if success (and skipReport not given or equal false): ```xml - + ... - ... +
...
...
- - ... + + ... ... - + - ... + ... ...
+ ... ... - ... +
...
...
- ...
- ...
``` +If skipReport is set to true then response will be: + +```xml + +``` + Response with error message if failure: ```xml -... +... ``` +List mocks definitions +---------------------- -## List of current registered mocks could be retrieved: -Via client: +### Via client ```java List mocks = remoteMockServer.listMocks() ``` -or via sending GET request to localhost:/serverControl +### Via HTTP + +Send GET request to localhost:/serverControl Response: @@ -207,7 +355,97 @@ Response: ... ... ... + ... + ... + ... + ... + ... + ... + ... ``` + +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.ztsh.eu`. + +Just add repository to maven pom: + +```xml + + ... + + ... + + touk + https://philanthropist.ztsh.eu/nexus/content/repositories/releases + + ... + + ... + +``` + +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/buildImage.sh b/buildImage.sh new file mode 100755 index 0000000..7916512 --- /dev/null +++ b/buildImage.sh @@ -0,0 +1,5 @@ +#!/bin/sh + +mvn -f mockserver/pom.xml clean package assembly:single + +docker build -t mockserver . diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..6cb6ea7 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,6 @@ +mocks: + image: mockserver + ports: + - "9999:9999" + volumes: + - /tmp:/externalSchema diff --git a/mockserver-api/pom.xml b/mockserver-api/pom.xml new file mode 100644 index 0000000..de5029a --- /dev/null +++ b/mockserver-api/pom.xml @@ -0,0 +1,42 @@ + + + 4.0.0 + + + eu.ztsh.mockserver + http-mock-server + 3.0.0-SNAPSHOT + + + mockserver-api + + + + jakarta.xml.bind + jakarta.xml.bind-api + + + org.projectlombok + lombok + + + + + + + org.codehaus.mojo + jaxb2-maven-plugin + + + xjc + process-resources + + xjc + + + + + + + \ No newline at end of file diff --git a/mockserver-api/src/main/xjb/binding.xjb b/mockserver-api/src/main/xjb/binding.xjb new file mode 100644 index 0000000..fdf2585 --- /dev/null +++ b/mockserver-api/src/main/xjb/binding.xjb @@ -0,0 +1,9 @@ + + + + + + + diff --git a/mockserver-api/src/main/xsd/eu/ztsh/mockserver/api/common.xsd b/mockserver-api/src/main/xsd/eu/ztsh/mockserver/api/common.xsd new file mode 100644 index 0000000..6dac2a9 --- /dev/null +++ b/mockserver-api/src/main/xsd/eu/ztsh/mockserver/api/common.xsd @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/mockserver-api/src/main/xsd/eu/ztsh/mockserver/api/request.xsd b/mockserver-api/src/main/xsd/eu/ztsh/mockserver/api/request.xsd new file mode 100644 index 0000000..891aebf --- /dev/null +++ b/mockserver-api/src/main/xsd/eu/ztsh/mockserver/api/request.xsd @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/mockserver-api/src/main/xsd/eu/ztsh/mockserver/api/response.xsd b/mockserver-api/src/main/xsd/eu/ztsh/mockserver/api/response.xsd new file mode 100644 index 0000000..66d4566 --- /dev/null +++ b/mockserver-api/src/main/xsd/eu/ztsh/mockserver/api/response.xsd @@ -0,0 +1,137 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/mockserver-client/pom.xml b/mockserver-client/pom.xml index b5a1ba6..cb7acfb 100644 --- a/mockserver-client/pom.xml +++ b/mockserver-client/pom.xml @@ -1,22 +1,44 @@ - - - http-mock-server - pl.touk.mockserver - 1.1.0 - + 4.0.0 + + eu.ztsh.mockserver + http-mock-server + 3.0.0-SNAPSHOT + + mockserver-client - - clean install - - org.codehaus.groovy - groovy-all + eu.ztsh.mockserver + mockserver-api + + + org.apache.groovy + groovy + + + org.apache.groovy + groovy-json + + + org.apache.groovy + groovy-xml + + + + org.glassfish.jaxb + jaxb-core + + + org.glassfish.jaxb + jaxb-runtime + + org.apache.httpcomponents httpclient @@ -26,4 +48,14 @@ commons-lang3 + + + + + org.codehaus.gmavenplus + gmavenplus-plugin + + + + diff --git a/mockserver-client/src/main/groovy/pl/touk/mockserver/client/InvalidMockDefinition.groovy b/mockserver-client/src/main/groovy/eu/ztsh/mockserver/client/InvalidMockDefinition.groovy similarity index 86% rename from mockserver-client/src/main/groovy/pl/touk/mockserver/client/InvalidMockDefinition.groovy rename to mockserver-client/src/main/groovy/eu/ztsh/mockserver/client/InvalidMockDefinition.groovy index f95b584..a39ef77 100644 --- a/mockserver-client/src/main/groovy/pl/touk/mockserver/client/InvalidMockDefinition.groovy +++ b/mockserver-client/src/main/groovy/eu/ztsh/mockserver/client/InvalidMockDefinition.groovy @@ -1,4 +1,4 @@ -package pl.touk.mockserver.client +package eu.ztsh.mockserver.client import groovy.transform.CompileStatic import groovy.transform.TypeChecked diff --git a/mockserver-client/src/main/groovy/pl/touk/mockserver/client/PeekMockRequestData.groovy b/mockserver-client/src/main/groovy/eu/ztsh/mockserver/client/InvalidMockRequestSchema.groovy similarity index 53% rename from mockserver-client/src/main/groovy/pl/touk/mockserver/client/PeekMockRequestData.groovy rename to mockserver-client/src/main/groovy/eu/ztsh/mockserver/client/InvalidMockRequestSchema.groovy index 3d0e546..b1de96f 100644 --- a/mockserver-client/src/main/groovy/pl/touk/mockserver/client/PeekMockRequestData.groovy +++ b/mockserver-client/src/main/groovy/eu/ztsh/mockserver/client/InvalidMockRequestSchema.groovy @@ -1,10 +1,9 @@ -package pl.touk.mockserver.client +package eu.ztsh.mockserver.client import groovy.transform.CompileStatic import groovy.transform.TypeChecked @CompileStatic @TypeChecked -class PeekMockRequestData { - String name +class InvalidMockRequestSchema extends RuntimeException { } diff --git a/mockserver-client/src/main/groovy/pl/touk/mockserver/client/MockAlreadyExists.groovy b/mockserver-client/src/main/groovy/eu/ztsh/mockserver/client/MockAlreadyExists.groovy similarity index 82% rename from mockserver-client/src/main/groovy/pl/touk/mockserver/client/MockAlreadyExists.groovy rename to mockserver-client/src/main/groovy/eu/ztsh/mockserver/client/MockAlreadyExists.groovy index f0c00a9..40d5dce 100644 --- a/mockserver-client/src/main/groovy/pl/touk/mockserver/client/MockAlreadyExists.groovy +++ b/mockserver-client/src/main/groovy/eu/ztsh/mockserver/client/MockAlreadyExists.groovy @@ -1,4 +1,4 @@ -package pl.touk.mockserver.client +package eu.ztsh.mockserver.client import groovy.transform.CompileStatic import groovy.transform.TypeChecked diff --git a/mockserver-client/src/main/groovy/pl/touk/mockserver/client/MockDoesNotExist.groovy b/mockserver-client/src/main/groovy/eu/ztsh/mockserver/client/MockDoesNotExist.groovy similarity index 82% rename from mockserver-client/src/main/groovy/pl/touk/mockserver/client/MockDoesNotExist.groovy rename to mockserver-client/src/main/groovy/eu/ztsh/mockserver/client/MockDoesNotExist.groovy index 02d0bee..fe93dc0 100644 --- a/mockserver-client/src/main/groovy/pl/touk/mockserver/client/MockDoesNotExist.groovy +++ b/mockserver-client/src/main/groovy/eu/ztsh/mockserver/client/MockDoesNotExist.groovy @@ -1,4 +1,4 @@ -package pl.touk.mockserver.client +package eu.ztsh.mockserver.client import groovy.transform.CompileStatic import groovy.transform.TypeChecked diff --git a/mockserver-client/src/main/groovy/eu/ztsh/mockserver/client/RemoteMockServer.groovy b/mockserver-client/src/main/groovy/eu/ztsh/mockserver/client/RemoteMockServer.groovy new file mode 100644 index 0000000..f594614 --- /dev/null +++ b/mockserver-client/src/main/groovy/eu/ztsh/mockserver/client/RemoteMockServer.groovy @@ -0,0 +1,85 @@ +package eu.ztsh.mockserver.client + +import org.apache.http.client.methods.CloseableHttpResponse +import org.apache.http.client.methods.HttpGet +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 eu.ztsh.mockserver.api.request.AddMock +import eu.ztsh.mockserver.api.request.MockServerRequest +import eu.ztsh.mockserver.api.request.PeekMock +import eu.ztsh.mockserver.api.request.RemoveMock +import eu.ztsh.mockserver.api.response.MockEventReport +import eu.ztsh.mockserver.api.response.MockPeeked +import eu.ztsh.mockserver.api.response.MockRemoved +import eu.ztsh.mockserver.api.response.MockReport +import eu.ztsh.mockserver.api.response.Mocks + +import jakarta.xml.bind.JAXBContext + +class RemoteMockServer { + private final String address + private final CloseableHttpClient client = HttpClients.createDefault() + private static final JAXBContext requestContext = JAXBContext.newInstance(AddMock, PeekMock, RemoveMock) + + RemoteMockServer(String host, int port) { + address = "http://$host:$port/serverControl" + } + + void addMock(AddMock addMockData) { + HttpPost addMockPost = new HttpPost(address) + addMockPost.entity = buildAddMockRequest(addMockData) + CloseableHttpResponse response = client.execute(addMockPost) + Util.extractResponse(response) + } + + List removeMock(String name, boolean skipReport = false) { + HttpPost removeMockPost = new HttpPost(address) + removeMockPost.entity = buildRemoveMockRequest(new RemoveMock(name: name, skipReport: skipReport)) + CloseableHttpResponse response = client.execute(removeMockPost) + MockRemoved mockRemoved = Util.extractResponse(response) as MockRemoved + return mockRemoved.mockEvents ?: [] + } + + List peekMock(String name) { + HttpPost removeMockPost = new HttpPost(address) + removeMockPost.entity = buildPeekMockRequest(new PeekMock(name: name)) + CloseableHttpResponse response = client.execute(removeMockPost) + MockPeeked mockPeeked = Util.extractResponse(response) as MockPeeked + 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")) + } + + private static String marshallRequest(MockServerRequest data) { + StringWriter sw = new StringWriter() + requestContext.createMarshaller().marshal(data, sw) + return sw.toString() + } + + private static StringEntity buildPeekMockRequest(PeekMock peekMock) { + return new StringEntity(marshallRequest(peekMock), ContentType.create("text/xml", "UTF-8")) + } + + private static StringEntity buildAddMockRequest(AddMock data) { + return new StringEntity(marshallRequest(data), ContentType.create("text/xml", "UTF-8")) + } + + List listMocks() { + HttpGet get = new HttpGet(address) + CloseableHttpResponse response = client.execute(get) + Mocks mocks = Util.extractResponse(response) as Mocks + return mocks.mocks + } +} diff --git a/mockserver-client/src/main/groovy/eu/ztsh/mockserver/client/Util.groovy b/mockserver-client/src/main/groovy/eu/ztsh/mockserver/client/Util.groovy new file mode 100644 index 0000000..e79e0c3 --- /dev/null +++ b/mockserver-client/src/main/groovy/eu/ztsh/mockserver/client/Util.groovy @@ -0,0 +1,67 @@ +package eu.ztsh.mockserver.client + +import groovy.json.JsonSlurper +import groovy.transform.CompileStatic +import groovy.transform.TypeChecked +import groovy.xml.XmlSlurper +import groovy.xml.slurpersupport.GPathResult +import org.apache.http.HttpEntity +import org.apache.http.client.methods.CloseableHttpResponse +import org.apache.http.util.EntityUtils +import eu.ztsh.mockserver.api.response.ExceptionOccured +import eu.ztsh.mockserver.api.response.MockAdded +import eu.ztsh.mockserver.api.response.MockServerResponse + +import jakarta.xml.bind.JAXBContext + +@CompileStatic +@TypeChecked +class Util { + private static + final JAXBContext responseContext = JAXBContext.newInstance(MockAdded.package.name, MockAdded.classLoader) + + static GPathResult extractXmlResponse(CloseableHttpResponse response) { + return new XmlSlurper().parseText(extractStringResponse(response)) + } + static String extractStringResponse(CloseableHttpResponse response) { + HttpEntity entity = response.entity + String responseString = EntityUtils.toString(entity, 'UTF-8') + EntityUtils.consumeQuietly(entity) + return responseString + } + + static MockServerResponse extractResponse(CloseableHttpResponse response) { + String responseString = extractStringResponse(response) + if (response.statusLine.statusCode == 200) { + return responseContext.createUnmarshaller().unmarshal(new StringReader(responseString)) as MockServerResponse + } + ExceptionOccured exceptionOccured = responseContext.createUnmarshaller().unmarshal(new StringReader(responseString)) as ExceptionOccured + String message = exceptionOccured.value + if (message == 'mock already registered') { + throw new MockAlreadyExists() + } + if (message == 'mock not registered') { + throw new MockDoesNotExist() + } + if (message == 'mock request schema is invalid schema') { + throw new InvalidMockRequestSchema() + } + throw new InvalidMockDefinition(message) + } + + static String soap(String request) { + return """ + + $request + """ + } + + static Object extractJsonResponse(CloseableHttpResponse response) { + return new JsonSlurper().parseText(extractStringResponse(response)) + } + + static void consumeResponse(CloseableHttpResponse response) { + EntityUtils.consumeQuietly(response.entity) + } + +} diff --git a/mockserver-client/src/main/groovy/pl/touk/mockserver/client/AddMockRequestData.groovy b/mockserver-client/src/main/groovy/pl/touk/mockserver/client/AddMockRequestData.groovy deleted file mode 100644 index 6da9a6e..0000000 --- a/mockserver-client/src/main/groovy/pl/touk/mockserver/client/AddMockRequestData.groovy +++ /dev/null @@ -1,32 +0,0 @@ -package pl.touk.mockserver.client - -import groovy.transform.CompileStatic -import groovy.transform.TypeChecked -import org.apache.commons.lang3.StringEscapeUtils - -@CompileStatic -@TypeChecked -class AddMockRequestData { - String name - String path - Integer port - String predicate - String response - Boolean soap - Integer statusCode - Method method - String responseHeaders - - void setPredicate(String predicate) { - this.predicate = StringEscapeUtils.escapeXml11(predicate) - } - - void setResponse(String response) { - this.response = StringEscapeUtils.escapeXml11(response) - } - - void setResponseHeaders(String responseHeaders) { - this.responseHeaders = StringEscapeUtils.escapeXml11(responseHeaders) - } -} - diff --git a/mockserver-client/src/main/groovy/pl/touk/mockserver/client/Method.groovy b/mockserver-client/src/main/groovy/pl/touk/mockserver/client/Method.groovy deleted file mode 100644 index 82aefea..0000000 --- a/mockserver-client/src/main/groovy/pl/touk/mockserver/client/Method.groovy +++ /dev/null @@ -1,12 +0,0 @@ -package pl.touk.mockserver.client - -enum Method { - POST, - GET, - DELETE, - PUT, - TRACE, - HEAD, - OPTIONS, - PATCH -} diff --git a/mockserver-client/src/main/groovy/pl/touk/mockserver/client/MockEvent.groovy b/mockserver-client/src/main/groovy/pl/touk/mockserver/client/MockEvent.groovy deleted file mode 100644 index 8c61ab3..0000000 --- a/mockserver-client/src/main/groovy/pl/touk/mockserver/client/MockEvent.groovy +++ /dev/null @@ -1,18 +0,0 @@ -package pl.touk.mockserver.client - -import groovy.transform.CompileStatic -import groovy.transform.EqualsAndHashCode -import groovy.transform.TypeChecked - -@EqualsAndHashCode -@CompileStatic -@TypeChecked -class MockEvent { - final MockRequest request - final MockResponse response - - MockEvent(MockRequest request, MockResponse response) { - this.request = request - this.response = response - } -} diff --git a/mockserver-client/src/main/groovy/pl/touk/mockserver/client/MockRequest.groovy b/mockserver-client/src/main/groovy/pl/touk/mockserver/client/MockRequest.groovy deleted file mode 100644 index c73c11d..0000000 --- a/mockserver-client/src/main/groovy/pl/touk/mockserver/client/MockRequest.groovy +++ /dev/null @@ -1,22 +0,0 @@ -package pl.touk.mockserver.client - -import groovy.transform.CompileStatic -import groovy.transform.EqualsAndHashCode -import groovy.transform.TypeChecked - -@CompileStatic -@TypeChecked -@EqualsAndHashCode -class MockRequest { - final String text - final Map headers - final Map query - final List path - - MockRequest(String text, Map headers, Map query, List path) { - this.text = text - this.headers = headers - this.query = query - this.path = path - } -} diff --git a/mockserver-client/src/main/groovy/pl/touk/mockserver/client/MockResponse.groovy b/mockserver-client/src/main/groovy/pl/touk/mockserver/client/MockResponse.groovy deleted file mode 100644 index 07212fb..0000000 --- a/mockserver-client/src/main/groovy/pl/touk/mockserver/client/MockResponse.groovy +++ /dev/null @@ -1,20 +0,0 @@ -package pl.touk.mockserver.client - -import groovy.transform.CompileStatic -import groovy.transform.EqualsAndHashCode -import groovy.transform.TypeChecked - -@CompileStatic -@TypeChecked -@EqualsAndHashCode -class MockResponse { - final int statusCode - final String text - final Map headers - - MockResponse(int statusCode, String text, Map headers) { - this.statusCode = statusCode - this.text = text - this.headers = headers - } -} diff --git a/mockserver-client/src/main/groovy/pl/touk/mockserver/client/RegisteredMock.groovy b/mockserver-client/src/main/groovy/pl/touk/mockserver/client/RegisteredMock.groovy deleted file mode 100644 index f755592..0000000 --- a/mockserver-client/src/main/groovy/pl/touk/mockserver/client/RegisteredMock.groovy +++ /dev/null @@ -1,28 +0,0 @@ -package pl.touk.mockserver.client - -import groovy.transform.CompileStatic -import groovy.transform.EqualsAndHashCode -import groovy.transform.ToString -import groovy.transform.TypeChecked - -@CompileStatic -@TypeChecked -@EqualsAndHashCode -@ToString -class RegisteredMock { - final String name - final String path - final int port - final String predicate - final String response - final String responseHeaders - - RegisteredMock(String name, String path, int port, String predicate, String response, String responseHeaders) { - this.name = name - this.path = path - this.port = port - this.predicate = predicate - this.response = response - this.responseHeaders = responseHeaders - } -} 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 deleted file mode 100644 index 6afce3d..0000000 --- a/mockserver-client/src/main/groovy/pl/touk/mockserver/client/RemoteMockServer.groovy +++ /dev/null @@ -1,119 +0,0 @@ -package pl.touk.mockserver.client - -import groovy.util.slurpersupport.GPathResult -import org.apache.http.client.methods.CloseableHttpResponse -import org.apache.http.client.methods.HttpGet -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 - -class RemoteMockServer { - private final String address - private final CloseableHttpClient client = HttpClients.createDefault() - - RemoteMockServer(String host, int port) { - address = "http://$host:$port/serverControl" - } - - void addMock(AddMockRequestData addMockRequestData) { - HttpPost addMockPost = new HttpPost(address) - addMockPost.entity = buildAddMockRequest(addMockRequestData) - CloseableHttpResponse response = client.execute(addMockPost) - GPathResult responseXml = Util.extractXmlResponse(response) - if (responseXml.name() != 'mockAdded') { - if (responseXml.text() == 'mock already registered') { - throw new MockAlreadyExists() - - } - throw new InvalidMockDefinition(responseXml.text()) - } - } - - List removeMock(String name, boolean skipReport = false) { - HttpPost removeMockPost = new HttpPost(address) - removeMockPost.entity = buildRemoveMockRequest(new RemoveMockRequestData(name: name, skipReport: skipReport)) - CloseableHttpResponse response = client.execute(removeMockPost) - GPathResult responseXml = Util.extractXmlResponse(response) - if (responseXml.name() == 'mockRemoved') { - return responseXml.'mockEvent'.collect { - new MockEvent(mockRequestFromXml(it.request), mockResponseFromXml(it.response)) - } - } - throw new MockDoesNotExist() - } - - List peekMock(String name) { - HttpPost removeMockPost = new HttpPost(address) - removeMockPost.entity = buildPeekMockRequest(new PeekMockRequestData(name: name)) - CloseableHttpResponse response = client.execute(removeMockPost) - GPathResult responseXml = Util.extractXmlResponse(response) - if (responseXml.name() == 'mockPeeked') { - return responseXml.'mockEvent'.collect { - new MockEvent(mockRequestFromXml(it.request), mockResponseFromXml(it.response)) - } - } - throw new MockDoesNotExist() - } - - private static MockResponse mockResponseFromXml(GPathResult xml) { - return new MockResponse(xml.statusCode.text() as int, xml.text.text(), xml.headers.param.collectEntries { - [(it.@name.text()): it.text()] - }) - } - - private static MockRequest mockRequestFromXml(GPathResult xml) { - return new MockRequest( - xml.text.text(), - xml.headers.param.collectEntries { [(it.@name.text()): it.text()] }, - xml.query.param.collectEntries { [(it.@name.text()): it.text()] }, - xml.path.elem*.text() - ) - } - - private static StringEntity buildRemoveMockRequest(RemoveMockRequestData data) { - return new StringEntity("""\ - - ${data.name} - ${data.skipReport} - - """, ContentType.create("text/xml", "UTF-8")) - } - - private static StringEntity buildPeekMockRequest(PeekMockRequestData data) { - return new StringEntity("""\ - - ${data.name} - - """, ContentType.create("text/xml", "UTF-8")) - } - - private static StringEntity buildAddMockRequest(AddMockRequestData data) { - return new StringEntity("""\ - - ${data.name} - ${data.path} - ${data.port} - ${data.predicate ? "${data.predicate}" : ''} - ${data.response ? "${data.response}" : ''} - ${data.soap != null ? "${data.soap}" : ''} - ${data.statusCode ? "${data.statusCode}" : ''} - ${data.method ? "${data.method}" : ''} - ${data.responseHeaders ? "${data.responseHeaders}" : ''} - - """, ContentType.create("text/xml", "UTF-8")) - } - - List listMocks() { - HttpGet get = new HttpGet(address) - CloseableHttpResponse response = client.execute(get) - GPathResult xml = Util.extractXmlResponse(response) - if (xml.name() == 'mocks') { - return xml.mock.collect { - new RegisteredMock(it.name.text(), it.path.text(), it.port.text() as int, it.predicate.text(), it.response.text(), it.responseHeaders.text()) - } - } - return [] - } -} diff --git a/mockserver-client/src/main/groovy/pl/touk/mockserver/client/RemoveMockRequestData.groovy b/mockserver-client/src/main/groovy/pl/touk/mockserver/client/RemoveMockRequestData.groovy deleted file mode 100644 index 1d37545..0000000 --- a/mockserver-client/src/main/groovy/pl/touk/mockserver/client/RemoveMockRequestData.groovy +++ /dev/null @@ -1,11 +0,0 @@ -package pl.touk.mockserver.client - -import groovy.transform.CompileStatic -import groovy.transform.TypeChecked - -@CompileStatic -@TypeChecked -class RemoveMockRequestData { - String name - boolean skipReport = false -} diff --git a/mockserver-client/src/main/groovy/pl/touk/mockserver/client/Util.groovy b/mockserver-client/src/main/groovy/pl/touk/mockserver/client/Util.groovy deleted file mode 100644 index 8cffc3b..0000000 --- a/mockserver-client/src/main/groovy/pl/touk/mockserver/client/Util.groovy +++ /dev/null @@ -1,38 +0,0 @@ -package pl.touk.mockserver.client - -import groovy.json.JsonSlurper -import groovy.transform.CompileStatic -import groovy.transform.TypeChecked -import groovy.util.slurpersupport.GPathResult -import org.apache.http.HttpEntity -import org.apache.http.client.methods.CloseableHttpResponse -import org.apache.http.util.EntityUtils - -@CompileStatic -@TypeChecked -class Util { - static GPathResult extractXmlResponse(CloseableHttpResponse response) { - HttpEntity entity = response.entity - GPathResult xml = new XmlSlurper().parseText(EntityUtils.toString(entity, 'UTF-8')) - EntityUtils.consumeQuietly(entity) - return xml - } - - static String soap(String request) { - return """ - - $request - """ - } - - static Object extractJsonResponse(CloseableHttpResponse response) { - HttpEntity entity = response.entity - Object json = new JsonSlurper().parseText(EntityUtils.toString(entity, 'UTF-8')) - EntityUtils.consumeQuietly(entity) - return json - } - - static void consumeResponse(CloseableHttpResponse response) { - EntityUtils.consumeQuietly(response.entity) - } -} diff --git a/mockserver-tests/pom.xml b/mockserver-tests/pom.xml index 97e00da..2506a7a 100644 --- a/mockserver-tests/pom.xml +++ b/mockserver-tests/pom.xml @@ -1,49 +1,68 @@ - + + 4.0.0 + http-mock-server - pl.touk.mockserver - 1.1.0 + eu.ztsh.mockserver + 3.0.0-SNAPSHOT - 4.0.0 mockserver-tests - - clean install - - - org.codehaus.groovy - groovy-all + eu.ztsh.mockserver + mockserver - org.spockframework - spock-core + eu.ztsh.mockserver + mockserver-client + + + org.apache.groovy + groovy + + + + org.apache.httpcomponents + httpclient + + org.slf4j slf4j-api ch.qos.logback - logback-classic + logback-core + - org.apache.httpcomponents - httpclient - - - pl.touk.mockserver - mockserver - ${project.version} - - - pl.touk.mockserver - mockserver-client - ${project.version} + org.spockframework + spock-core + + + + org.codehaus.gmavenplus + gmavenplus-plugin + + + org.apache.maven.plugins + maven-surefire-plugin + 3.2.5 + + + **/*Test.java + + + + + + diff --git a/mockserver-tests/src/test/groovy/eu/ztsh/mockserver/tests/MockServerHttpsTest.groovy b/mockserver-tests/src/test/groovy/eu/ztsh/mockserver/tests/MockServerHttpsTest.groovy new file mode 100644 index 0000000..e1ecd95 --- /dev/null +++ b/mockserver-tests/src/test/groovy/eu/ztsh/mockserver/tests/MockServerHttpsTest.groovy @@ -0,0 +1,156 @@ +package eu.ztsh.mockserver.tests + +import eu.ztsh.mockserver.api.common.Https +import eu.ztsh.mockserver.api.request.AddMock +import eu.ztsh.mockserver.client.RemoteMockServer +import eu.ztsh.mockserver.client.Util +import eu.ztsh.mockserver.server.HttpMockServer +import groovy.xml.slurpersupport.GPathResult +import org.apache.http.client.methods.CloseableHttpResponse +import org.apache.http.client.methods.HttpPost +import org.apache.http.conn.ssl.SSLConnectionSocketFactory +import org.apache.http.conn.ssl.SSLContexts +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 spock.lang.AutoCleanup +import spock.lang.Ignore +import spock.lang.Shared +import spock.lang.Specification + +import javax.net.ssl.SSLContext +import javax.net.ssl.SSLHandshakeException +import java.security.KeyStore + +@Ignore +class MockServerHttpsTest extends Specification { + + RemoteMockServer remoteMockServer = new RemoteMockServer('localhost', 19000) + + @AutoCleanup('stop') + HttpMockServer httpMockServer = new HttpMockServer(19000) + + @Shared + SSLContext noClientAuthSslContext = SSLContexts.custom() + .loadTrustMaterial(trustStore()) + .build() + + @Shared + SSLContext trustedCertificateSslContext = SSLContexts.custom() + .loadKeyMaterial(trustedCertificateKeystore(), 'changeit'.toCharArray()) + .loadTrustMaterial(trustStore()) + .build() + + @Shared + SSLContext untrustedCertificateSslContext = SSLContexts.custom() + .loadKeyMaterial(untrustedCertificateKeystore(), 'changeit'.toCharArray()) + .loadTrustMaterial(trustStore()) + .build() + + @Ignore("TODO: SSL peer shut down incorrectly") + def 'should handle HTTPS server' () { + given: + remoteMockServer.addMock(new AddMock( + name: 'testHttps', + path: 'testEndpoint', + port: 10443, + predicate: '''{req -> req.xml.name() == 'request'}''', + response: '''{req -> ""}''', + https: new Https( + keyPassword: 'changeit', + keystorePassword: 'changeit', + keystorePath: MockServerHttpsTest.classLoader.getResource('keystore.jks').path + ), + soap: false + )) + when: + HttpPost restPost = new HttpPost('https://localhost:10443/testEndpoint') + restPost.entity = new StringEntity('', ContentType.create("text/xml", "UTF-8")) + CloseableHttpResponse response = client(noClientAuthSslContext).execute(restPost) + then: + GPathResult restPostResponse = Util.extractXmlResponse(response) + restPostResponse.name() == 'goodResponse-request' + } + + @Ignore("TODO: SSL peer shut down incorrectly") + def 'should handle HTTPS server with client auth' () { + given: + remoteMockServer.addMock(new AddMock( + name: 'testHttps', + path: 'testEndpoint', + port: 10443, + predicate: '''{req -> req.xml.name() == 'request'}''', + response: '''{req -> ""}''', + https: new Https( + keyPassword: 'changeit', + keystorePassword: 'changeit', + keystorePath: MockServerHttpsTest.classLoader.getResource('keystore.jks').path, + truststorePath: MockServerHttpsTest.classLoader.getResource('truststore.jks').path, + truststorePassword: 'changeit', + requireClientAuth: true + ), + soap: false + )) + when: + HttpPost restPost = new HttpPost('https://localhost:10443/testEndpoint') + restPost.entity = new StringEntity('', ContentType.create("text/xml", "UTF-8")) + CloseableHttpResponse response = client(trustedCertificateSslContext).execute(restPost) + then: + GPathResult restPostResponse = Util.extractXmlResponse(response) + restPostResponse.name() == 'goodResponse-request' + } + + def 'should handle HTTPS server with wrong client auth' () { + given: + remoteMockServer.addMock(new AddMock( + name: 'testHttps', + path: 'testEndpoint', + port: 10443, + predicate: '''{req -> req.xml.name() == 'request'}''', + response: '''{req -> ""}''', + https: new Https( + keyPassword: 'changeit', + keystorePassword: 'changeit', + keystorePath: MockServerHttpsTest.classLoader.getResource('keystore.jks').path, + truststorePath: MockServerHttpsTest.classLoader.getResource('truststore.jks').path, + truststorePassword: 'changeit', + requireClientAuth: true + ), + soap: false + )) + when: + HttpPost restPost = new HttpPost('https://localhost:10443/testEndpoint') + restPost.entity = new StringEntity('', ContentType.create("text/xml", "UTF-8")) + client(sslContext).execute(restPost) + then: + thrown(SSLHandshakeException) + where: + sslContext << [noClientAuthSslContext, untrustedCertificateSslContext] + } + + private CloseableHttpClient client(SSLContext sslContext) { + return HttpClients.custom() + .setHostnameVerifier(SSLConnectionSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER) + .setSslcontext(sslContext) + .build() + } + + private KeyStore trustedCertificateKeystore() { + return loadKeystore('trusted.jks') + } + + private KeyStore untrustedCertificateKeystore() { + return loadKeystore('untrusted.jks') + } + + private KeyStore trustStore() { + return loadKeystore('truststore.jks') + } + + private KeyStore loadKeystore(String fileName) { + KeyStore truststore = KeyStore.getInstance(KeyStore.defaultType) + truststore.load(new FileInputStream(MockServerHttpsTest.classLoader.getResource(fileName).path), "changeit".toCharArray()); + return truststore + } +} diff --git a/mockserver-tests/src/test/groovy/pl/touk/mockserver/tests/MockServerIntegrationTest.groovy b/mockserver-tests/src/test/groovy/eu/ztsh/mockserver/tests/MockServerIntegrationTest.groovy similarity index 59% rename from mockserver-tests/src/test/groovy/pl/touk/mockserver/tests/MockServerIntegrationTest.groovy rename to mockserver-tests/src/test/groovy/eu/ztsh/mockserver/tests/MockServerIntegrationTest.groovy index 077a893..038a143 100644 --- a/mockserver-tests/src/test/groovy/pl/touk/mockserver/tests/MockServerIntegrationTest.groovy +++ b/mockserver-tests/src/test/groovy/eu/ztsh/mockserver/tests/MockServerIntegrationTest.groovy @@ -1,25 +1,43 @@ -package pl.touk.mockserver.tests +package eu.ztsh.mockserver.tests -import groovy.util.slurpersupport.GPathResult -import org.apache.http.client.methods.* +import groovy.xml.slurpersupport.GPathResult +import org.apache.http.client.methods.CloseableHttpResponse +import org.apache.http.client.methods.HttpDelete +import org.apache.http.client.methods.HttpGet +import org.apache.http.client.methods.HttpHead +import org.apache.http.client.methods.HttpOptions +import org.apache.http.client.methods.HttpPatch +import org.apache.http.client.methods.HttpPost +import org.apache.http.client.methods.HttpPut +import org.apache.http.client.methods.HttpTrace 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 org.apache.http.util.EntityUtils -import pl.touk.mockserver.client.* -import pl.touk.mockserver.server.HttpMockServer -import spock.lang.Shared +import eu.ztsh.mockserver.api.common.ImportAlias +import eu.ztsh.mockserver.api.common.Method +import eu.ztsh.mockserver.api.request.AddMock +import eu.ztsh.mockserver.api.response.MockEventReport +import eu.ztsh.mockserver.api.response.MockReport +import eu.ztsh.mockserver.client.InvalidMockDefinition +import eu.ztsh.mockserver.client.InvalidMockRequestSchema +import eu.ztsh.mockserver.client.MockAlreadyExists +import eu.ztsh.mockserver.client.MockDoesNotExist +import eu.ztsh.mockserver.client.RemoteMockServer +import eu.ztsh.mockserver.client.Util +import eu.ztsh.mockserver.server.HttpMockServer +import spock.lang.AutoCleanup +import spock.lang.Ignore import spock.lang.Specification -import spock.lang.Unroll class MockServerIntegrationTest extends Specification { RemoteMockServer remoteMockServer + @AutoCleanup('stop') HttpMockServer httpMockServer - @Shared CloseableHttpClient client = HttpClients.createDefault() def setup() { @@ -27,13 +45,9 @@ class MockServerIntegrationTest extends Specification { remoteMockServer = new RemoteMockServer('localhost', 9000) } - def cleanup() { - httpMockServer.stop() - } - def "should add working rest mock on endpoint"() { expect: - remoteMockServer.addMock(new AddMockRequestData( + remoteMockServer.addMock(new AddMock( name: 'testRest', path: 'testEndpoint', port: 9999, @@ -52,9 +66,10 @@ class MockServerIntegrationTest extends Specification { remoteMockServer.removeMock('testRest')?.size() == 1 } + @Ignore("TODO: restPostResponse.name()") def "should add working rest mock on endpoint with utf"() { expect: - remoteMockServer.addMock(new AddMockRequestData( + remoteMockServer.addMock(new AddMock( name: 'testRestUtf', path: 'testEndpoint', port: 9999, @@ -76,7 +91,7 @@ class MockServerIntegrationTest extends Specification { def "should add soap mock on endpoint"() { expect: - remoteMockServer.addMock(new AddMockRequestData( + remoteMockServer.addMock(new AddMock( name: 'testSoap', path: 'testEndpoint', port: 9999, @@ -102,7 +117,7 @@ class MockServerIntegrationTest extends Specification { then: thrown(MockDoesNotExist) expect: - remoteMockServer.addMock(new AddMockRequestData( + remoteMockServer.addMock(new AddMock( name: 'testSoap', path: 'testEndpoint', port: 9999, @@ -120,7 +135,7 @@ class MockServerIntegrationTest extends Specification { def "should not add mock with existing name"() { expect: - remoteMockServer.addMock(new AddMockRequestData( + remoteMockServer.addMock(new AddMock( name: 'testSoap', path: 'testEndpoint', port: 9999, @@ -129,7 +144,7 @@ class MockServerIntegrationTest extends Specification { soap: true )) when: - remoteMockServer.addMock(new AddMockRequestData( + remoteMockServer.addMock(new AddMock( name: 'testSoap', path: 'testEndpoint2', port: 9998, @@ -141,9 +156,23 @@ class MockServerIntegrationTest extends Specification { thrown(MockAlreadyExists) } + def "should not add mock when schema does not exist"() { + when: + remoteMockServer.addMock(new AddMock( + name: 'test', + path: 'testEndpoint2', + port: 9998, + response: '''{req -> ""}''', + soap: false, + schema: 'ble.xsd' + )) + then: + thrown(InvalidMockRequestSchema) + } + def "should not add mock with empty name"() { when: - remoteMockServer.addMock(new AddMockRequestData( + remoteMockServer.addMock(new AddMock( name: '', path: 'testEndpoint2', port: 9998, @@ -157,7 +186,7 @@ class MockServerIntegrationTest extends Specification { def "should add mock after deleting old mock with the same name"() { expect: - remoteMockServer.addMock(new AddMockRequestData( + remoteMockServer.addMock(new AddMock( name: 'testSoap', path: 'testEndpoint', port: 9999, @@ -168,7 +197,7 @@ class MockServerIntegrationTest extends Specification { and: remoteMockServer.removeMock('testSoap') == [] and: - remoteMockServer.addMock(new AddMockRequestData( + remoteMockServer.addMock(new AddMock( name: 'testSoap', path: 'testEndpoint', port: 9999, @@ -180,14 +209,14 @@ class MockServerIntegrationTest extends Specification { def "should add simultaneously working post and rest mocks with the same predicate and endpoint nad port"() { given: - remoteMockServer.addMock(new AddMockRequestData( + remoteMockServer.addMock(new AddMock( name: 'testRest', path: 'testEndpoint', port: 9999, predicate: '''{req -> req.xml.name() == 'request'}''', response: '''{req -> ""}''' )) - remoteMockServer.addMock(new AddMockRequestData( + remoteMockServer.addMock(new AddMock( name: 'testSoap', path: 'testEndpoint', port: 9999, @@ -212,17 +241,16 @@ class MockServerIntegrationTest extends Specification { soapPostResponse.Body.'goodResponseSoap-request'.size() == 1 } - @Unroll def "should dispatch rest mocks when second on #name"() { given: - remoteMockServer.addMock(new AddMockRequestData( + remoteMockServer.addMock(new AddMock( name: 'testRest1', path: 'test1', port: 9999, predicate: '''{req -> req.xml.name() == 'request1'}''', response: '''{req -> ""}''' )) - remoteMockServer.addMock(new AddMockRequestData( + remoteMockServer.addMock(new AddMock( name: 'testRest2', path: secondPath, port: secondPort, @@ -251,10 +279,9 @@ class MockServerIntegrationTest extends Specification { 9998 | 'test2' | 'another port and path' } - @Unroll def "should dispatch rest mock with response code"() { given: - remoteMockServer.addMock(new AddMockRequestData( + remoteMockServer.addMock(new AddMock( name: 'testRest1', path: 'test1', port: 9999, @@ -276,7 +303,7 @@ class MockServerIntegrationTest extends Specification { def "should return response code 404 and error body the same as request body when mocks does not apply"() { given: - remoteMockServer.addMock(new AddMockRequestData( + remoteMockServer.addMock(new AddMock( name: 'testRest1', path: 'test1', port: 9999, @@ -295,7 +322,7 @@ class MockServerIntegrationTest extends Specification { def "should inform that there was problem during adding mock - invalid port"() { when: - remoteMockServer.addMock(new AddMockRequestData( + remoteMockServer.addMock(new AddMock( name: 'testSoap', path: 'testEndpoint2', port: -1, @@ -309,13 +336,13 @@ class MockServerIntegrationTest extends Specification { def "should dispatch rest mock with get method"() { given: - remoteMockServer.addMock(new AddMockRequestData( + remoteMockServer.addMock(new AddMock( name: 'testRest', path: 'testEndpoint', port: 9999, response: '''{_ -> ""}''' )) - remoteMockServer.addMock(new AddMockRequestData( + remoteMockServer.addMock(new AddMock( name: 'testRest2', path: 'testEndpoint', port: 9999, @@ -332,13 +359,13 @@ class MockServerIntegrationTest extends Specification { def "should dispatch rest mock with trace method"() { given: - remoteMockServer.addMock(new AddMockRequestData( + remoteMockServer.addMock(new AddMock( name: 'testRest', path: 'testEndpoint', port: 9999, response: '''{_ -> ""}''' )) - remoteMockServer.addMock(new AddMockRequestData( + remoteMockServer.addMock(new AddMock( name: 'testRest2', path: 'testEndpoint', port: 9999, @@ -355,13 +382,13 @@ class MockServerIntegrationTest extends Specification { def "should dispatch rest mock with head method"() { given: - remoteMockServer.addMock(new AddMockRequestData( + remoteMockServer.addMock(new AddMock( name: 'testRest', path: 'testEndpoint', port: 9999, response: '''{_ -> ""}''' )) - remoteMockServer.addMock(new AddMockRequestData( + remoteMockServer.addMock(new AddMock( name: 'testRest2', path: 'testEndpoint', port: 9999, @@ -377,13 +404,13 @@ class MockServerIntegrationTest extends Specification { def "should dispatch rest mock with options method"() { given: - remoteMockServer.addMock(new AddMockRequestData( + remoteMockServer.addMock(new AddMock( name: 'testRest', path: 'testEndpoint', port: 9999, response: '''{_ -> ""}''' )) - remoteMockServer.addMock(new AddMockRequestData( + remoteMockServer.addMock(new AddMock( name: 'testRest2', path: 'testEndpoint', port: 9999, @@ -399,13 +426,13 @@ class MockServerIntegrationTest extends Specification { def "should dispatch rest mock with put method"() { given: - remoteMockServer.addMock(new AddMockRequestData( + remoteMockServer.addMock(new AddMock( name: 'testRest', path: 'test1', port: 9999, response: '''{_ -> ""}''' )) - remoteMockServer.addMock(new AddMockRequestData( + remoteMockServer.addMock(new AddMock( name: 'testRest2', path: 'test1', port: 9999, @@ -424,13 +451,13 @@ class MockServerIntegrationTest extends Specification { def "should dispatch rest mock with delete method"() { given: - remoteMockServer.addMock(new AddMockRequestData( + remoteMockServer.addMock(new AddMock( name: 'testRest', path: 'test1', port: 9999, response: '''{_ -> ""}''' )) - remoteMockServer.addMock(new AddMockRequestData( + remoteMockServer.addMock(new AddMock( name: 'testRest2', path: 'test1', port: 9999, @@ -447,13 +474,13 @@ class MockServerIntegrationTest extends Specification { def "should dispatch rest mock with patch method"() { given: - remoteMockServer.addMock(new AddMockRequestData( + remoteMockServer.addMock(new AddMock( name: 'testRest', path: 'test1', port: 9999, response: '''{_ -> ""}''' )) - remoteMockServer.addMock(new AddMockRequestData( + remoteMockServer.addMock(new AddMock( name: 'testRest2', path: 'test1', port: 9999, @@ -472,7 +499,7 @@ class MockServerIntegrationTest extends Specification { def "should add mock that return headers"() { given: - remoteMockServer.addMock(new AddMockRequestData( + remoteMockServer.addMock(new AddMock( name: 'testRest', path: 'testEndpoint', port: 9999, @@ -492,7 +519,7 @@ class MockServerIntegrationTest extends Specification { def "should add mock that accepts only when certain request headers exists"() { given: - remoteMockServer.addMock(new AddMockRequestData( + remoteMockServer.addMock(new AddMock( name: 'testRest', path: 'testEndpoint', port: 9999, @@ -521,7 +548,7 @@ class MockServerIntegrationTest extends Specification { def "should add mock that accepts only when certain query params exists"() { given: - remoteMockServer.addMock(new AddMockRequestData( + remoteMockServer.addMock(new AddMock( name: 'testRest', path: 'testEndpoint', port: 9999, @@ -545,7 +572,7 @@ class MockServerIntegrationTest extends Specification { def "should add mock that accepts only when request has specific body"() { given: - remoteMockServer.addMock(new AddMockRequestData( + remoteMockServer.addMock(new AddMock( name: 'testRest', path: 'testEndpoint', port: 9999, @@ -570,7 +597,7 @@ class MockServerIntegrationTest extends Specification { def "should add mock which response json to json"() { given: - remoteMockServer.addMock(new AddMockRequestData( + remoteMockServer.addMock(new AddMock( name: 'testRest', path: 'testEndpoint', port: 9999, @@ -593,9 +620,9 @@ class MockServerIntegrationTest extends Specification { restPostResponse.name == 'goodResponse-1' } - def "should get list mocks"() { + def "should get list of mocks"() { given: - remoteMockServer.addMock(new AddMockRequestData( + remoteMockServer.addMock(new AddMock( name: 'testRest2', path: 'testEndpoint', port: 9998, @@ -603,45 +630,58 @@ class MockServerIntegrationTest extends Specification { response: '''{ req -> '' }''', responseHeaders: '{ _ -> [a: "b"] }' )) - remoteMockServer.addMock(new AddMockRequestData( + remoteMockServer.addMock(new AddMock( name: 'testRest4', path: 'testEndpoint', - port: 9999 + port: 9999, + soap: true, + statusCode: 204, + method: Method.PUT )) - remoteMockServer.addMock(new AddMockRequestData( + remoteMockServer.addMock(new AddMock( name: 'testRest3', path: 'testEndpoint2', port: 9999 )) - remoteMockServer.addMock(new AddMockRequestData( + remoteMockServer.addMock(new AddMock( name: 'testRest5', path: 'testEndpoint', port: 9999 )) - remoteMockServer.addMock(new AddMockRequestData( + remoteMockServer.addMock(new AddMock( name: 'testRest6', path: 'testEndpoint2', port: 9999 )) - remoteMockServer.addMock(new AddMockRequestData( + remoteMockServer.addMock(new AddMock( name: 'testRest', path: 'testEndpoint', - port: 9999 + port: 9999, + schema: 'schema2.xsd', + imports: [new ImportAlias(alias: 'aaa', fullClassName: 'bbb')] )) remoteMockServer.removeMock('testRest5') - expect: - remoteMockServer.listMocks() == [ - new RegisteredMock('testRest', 'testEndpoint', 9999, '{ _ -> true }', '''{ _ -> '' }''', '{ _ -> [:] }'), - new RegisteredMock('testRest2', 'testEndpoint', 9998, '''{ req -> req.xml.name() == 'request1'}''', '''{ req -> '' }''', '{ _ -> [a: "b"] }'), - new RegisteredMock('testRest3', 'testEndpoint2', 9999, '{ _ -> true }', '''{ _ -> '' }''', '{ _ -> [:] }'), - new RegisteredMock('testRest4', 'testEndpoint', 9999, '{ _ -> true }', '''{ _ -> '' }''', '{ _ -> [:] }'), - new RegisteredMock('testRest6', 'testEndpoint2', 9999, '{ _ -> true }', '''{ _ -> '' }''', '{ _ -> [:] }') - ] + when: + List mockReport = remoteMockServer.listMocks() + then: + 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' + } + + private static void assertMockReport(MockReport mockReport, Map props) { + props.each { + assert mockReport."${it.key}" == it.value + } } def "should add mock accepts path certain path params"() { given: - remoteMockServer.addMock(new AddMockRequestData( + remoteMockServer.addMock(new AddMock( name: 'testRest', path: 'testEndpoint', port: 9999, @@ -664,7 +704,7 @@ class MockServerIntegrationTest extends Specification { def "should get mock report when deleting mock"() { expect: - remoteMockServer.addMock(new AddMockRequestData( + remoteMockServer.addMock(new AddMock( name: 'testRest', path: 'testEndpoint', port: 9999, @@ -674,7 +714,7 @@ class MockServerIntegrationTest extends Specification { responseHeaders: '''{req -> ['aaa':'14']}''', soap: false )) - remoteMockServer.addMock(new AddMockRequestData( + remoteMockServer.addMock(new AddMock( name: 'testRest2', path: 'testEndpoint', port: 9999, @@ -706,40 +746,40 @@ class MockServerIntegrationTest extends Specification { GPathResult restPostResponse3 = Util.extractXmlResponse(response3) restPostResponse3.name() == 'goodResponseRest' when: - List mockEvents1 = remoteMockServer.removeMock('testRest') + List mockEvents1 = remoteMockServer.removeMock('testRest') then: mockEvents1.size() == 2 mockEvents1[0].request.text == '' - !mockEvents1[0].request.headers?.keySet()?.empty - mockEvents1[0].request.query == [:] - mockEvents1[0].request.path == ['testEndpoint'] - !mockEvents1[0].response.headers?.keySet()?.empty + !mockEvents1[0].request.headers?.headers?.empty + mockEvents1[0].request.queryParams.queryParams == [] + mockEvents1[0].request.path.pathParts == ['testEndpoint'] + !mockEvents1[0].response.headers?.headers?.empty mockEvents1[0].response.text == '' mockEvents1[0].response.statusCode == 201 mockEvents1[1].request.text == '' - !mockEvents1[1].request.headers?.keySet()?.empty - mockEvents1[1].request.query == [:] - mockEvents1[1].request.path == ['testEndpoint', 'hello'] - !mockEvents1[1].response.headers?.keySet()?.empty + !mockEvents1[1].request.headers?.headers?.empty + mockEvents1[1].request.queryParams.queryParams == [] + mockEvents1[1].request.path.pathParts == ['testEndpoint', 'hello'] + !mockEvents1[1].response.headers?.headers?.empty mockEvents1[1].response.text == '' mockEvents1[1].response.statusCode == 201 when: - List mockEvents2 = remoteMockServer.removeMock('testRest2') + List mockEvents2 = remoteMockServer.removeMock('testRest2') then: mockEvents2.size() == 1 mockEvents2[0].request.text == '' - !mockEvents2[0].request.headers?.keySet()?.empty - mockEvents2[0].request.query == [id: '123'] - mockEvents2[0].request.path == ['testEndpoint'] - mockEvents2[0].response.headers.aaa == '15' + !mockEvents2[0].request.headers?.headers?.empty + mockEvents2[0].request.queryParams.queryParams.find { it.name == 'id' }?.value == '123' + mockEvents2[0].request.path.pathParts == ['testEndpoint'] + mockEvents2[0].response.headers.headers.find { it.name == 'aaa' }?.value == '15' mockEvents2[0].response.text == '' mockEvents2[0].response.statusCode == 202 } def "should get mock report when peeking mock"() { expect: - remoteMockServer.addMock(new AddMockRequestData( + remoteMockServer.addMock(new AddMock( name: 'testRest', path: 'testEndpoint', port: 9999, @@ -749,7 +789,7 @@ class MockServerIntegrationTest extends Specification { responseHeaders: '''{req -> ['aaa':'14']}''', soap: false )) - remoteMockServer.addMock(new AddMockRequestData( + remoteMockServer.addMock(new AddMock( name: 'testRest2', path: 'testEndpoint', port: 9999, @@ -781,41 +821,40 @@ class MockServerIntegrationTest extends Specification { GPathResult restPostResponse3 = Util.extractXmlResponse(response3) restPostResponse3.name() == 'goodResponseRest' when: - List mockEvents1 = remoteMockServer.peekMock('testRest') + List mockEvents1 = remoteMockServer.peekMock('testRest') then: mockEvents1.size() == 2 mockEvents1[0].request.text == '' - !mockEvents1[0].request.headers?.keySet()?.empty - mockEvents1[0].request.query == [:] - mockEvents1[0].request.path == ['testEndpoint'] - !mockEvents1[0].response.headers?.keySet()?.empty + !mockEvents1[0].request.headers?.headers?.empty + mockEvents1[0].request.queryParams.queryParams == [] + mockEvents1[0].request.path.pathParts == ['testEndpoint'] + !mockEvents1[0].response.headers?.headers?.empty mockEvents1[0].response.text == '' mockEvents1[0].response.statusCode == 201 mockEvents1[1].request.text == '' - !mockEvents1[1].request.headers?.keySet()?.empty - mockEvents1[1].request.query == [:] - mockEvents1[1].request.path == ['testEndpoint', 'hello'] - !mockEvents1[1].response.headers?.keySet()?.empty + !mockEvents1[1].request.headers?.headers?.empty + mockEvents1[1].request.queryParams.queryParams == [] + mockEvents1[1].request.path.pathParts == ['testEndpoint', 'hello'] + !mockEvents1[1].response.headers?.headers?.empty mockEvents1[1].response.text == '' mockEvents1[1].response.statusCode == 201 when: - List mockEvents2 = remoteMockServer.peekMock('testRest2') + List mockEvents2 = remoteMockServer.peekMock('testRest2') then: mockEvents2.size() == 1 mockEvents2[0].request.text == '' - !mockEvents2[0].request.headers?.keySet()?.empty - mockEvents2[0].request.query == [id: '123'] - mockEvents2[0].request.path == ['testEndpoint'] - mockEvents2[0].response.headers.aaa == '15' + !mockEvents2[0].request.headers?.headers?.empty + mockEvents2[0].request.queryParams.queryParams.find { it.name == 'id' }?.value == '123' + mockEvents2[0].request.path.pathParts == ['testEndpoint'] + mockEvents2[0].response.headers.headers.find { it.name == 'aaa' }?.value == '15' mockEvents2[0].response.text == '' mockEvents2[0].response.statusCode == 202 } - @Unroll def "should return mock report with #mockEvents events when deleting mock with flag skip mock = #skipReport"() { expect: - remoteMockServer.addMock(new AddMockRequestData( + remoteMockServer.addMock(new AddMock( name: 'testRest', path: 'testEndpoint', port: 9999, @@ -839,4 +878,306 @@ class MockServerIntegrationTest extends Specification { false | 1 true | 0 } + + def "should reject mock when it has System.exit in closure"() { + when: + remoteMockServer.addMock(new AddMock( + name: 'testRest', + path: 'testEndpoint', + port: 9999, + predicate: predicate, + response: '''{req -> ""}''', + soap: false + )) + then: + thrown(InvalidMockDefinition) + expect: + remoteMockServer.listMocks() == [] + where: + predicate << [ + '''{req -> System.exit(-1); req.xml.name() == 'request'}''', + '''{req -> System .exit(-1); req.xml.name() == 'request'}''', + '''{req -> System + + .exit(-1); req.xml.name() == 'request'}''', + '''{req -> System. exit(-1); req.xml.name() == 'request'}''', + '''{req -> System.exit (-1); req.xml.name() == 'request'}''' + ] + } + + def "should validate request against multiple schema files"() { + expect: + remoteMockServer.addMock(new AddMock( + name: 'testRest', + path: 'testEndpoint', + port: 9999, + schema: 'schema1.xsd', + response: '''{req -> ''}''', + soap: false + )) + when: + HttpPost restPost = new HttpPost('http://localhost:9999/testEndpoint') + restPost.entity = new StringEntity('15unknown', ContentType.create("text/xml", "UTF-8")) + CloseableHttpResponse response = client.execute(restPost) + then: + response.statusLine.statusCode == 400 + Util.extractStringResponse(response).contains('''Value 'unknown' is not facet-valid with respect to enumeration '[test, prod, preprod]'.''') + when: + HttpPost restPost2 = new HttpPost('http://localhost:9999/testEndpoint') + restPost2.entity = new StringEntity('15test', ContentType.create("text/xml", "UTF-8")) + CloseableHttpResponse response2 = client.execute(restPost2) + then: + Util.consumeResponse(response2) + response2.statusLine.statusCode == 200 + expect: + remoteMockServer.removeMock('testRest')?.size() == 2 + } + + def "should validate soap request"() { + expect: + remoteMockServer.addMock(new AddMock( + name: 'testSoap', + path: 'testEndpoint', + port: 9999, + schema: 'schema1.xsd', + response: '''{req -> ''}''', + soap: true + )) + when: + HttpPost restPost = new HttpPost('http://localhost:9999/testEndpoint') + restPost.entity = new StringEntity(Util.soap('15unknown'), ContentType.create("text/xml", "UTF-8")) + CloseableHttpResponse response = client.execute(restPost) + then: + response.statusLine.statusCode == 400 + Util.extractStringResponse(response).contains('''Value 'unknown' is not facet-valid with respect to enumeration '[test, prod, preprod]'.''') + when: + HttpPost restPost2 = new HttpPost('http://localhost:9999/testEndpoint') + restPost2.entity = new StringEntity(Util.soap('15test'), ContentType.create("text/xml", "UTF-8")) + CloseableHttpResponse response2 = client.execute(restPost2) + then: + Util.consumeResponse(response2) + response2.statusLine.statusCode == 200 + expect: + remoteMockServer.removeMock('testSoap')?.size() == 2 + } + + def "should validate soap request with namespace in envelope"() { + expect: + remoteMockServer.addMock(new AddMock( + name: 'testSoap', + path: 'testEndpoint', + port: 9999, + schema: 'schema1.xsd', + response: '''{req -> ''}''', + soap: true + )) + when: + HttpPost restPost = new HttpPost('http://localhost:9999/testEndpoint') + restPost.entity = new StringEntity(''' + + 15unknown + +''', ContentType.create("text/xml", "UTF-8")) + CloseableHttpResponse response = client.execute(restPost) + then: + response.statusLine.statusCode == 400 + Util.extractStringResponse(response).contains('''Value 'unknown' is not facet-valid with respect to enumeration '[test, prod, preprod]'.''') + when: + HttpPost restPost2 = new HttpPost('http://localhost:9999/testEndpoint') + restPost2.entity = new StringEntity(''' + + 15test + +''', ContentType.create("text/xml", "UTF-8")) + CloseableHttpResponse response2 = client.execute(restPost2) + then: + Util.consumeResponse(response2) + response2.statusLine.statusCode == 200 + expect: + remoteMockServer.removeMock('testSoap')?.size() == 2 + } + + def "should add mock with alias"() { + expect: + remoteMockServer.addMock(new AddMock( + name: 'testRest', + path: 'testEndpoint', + port: 9999, + predicate: '''{req -> req.xml.name() == 'request'}''', + response: '''{req -> ""}''', + soap: false, + imports: [new ImportAlias(alias: 'AAA', fullClassName: 'javax.xml.XMLConstants')] + )) + when: + HttpPost restPost = new HttpPost('http://localhost:9999/testEndpoint') + restPost.entity = new StringEntity('', ContentType.create("text/xml", "UTF-8")) + CloseableHttpResponse response = client.execute(restPost) + then: + GPathResult restPostResponse = Util.extractXmlResponse(response) + restPostResponse.name() == 'goodResponseRest-xmlns' + 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') + ], + preserveHistory: true + )) + 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', preserveHistory: true]) + 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' + } + + def "should add mock without history"() { + expect: + remoteMockServer.addMock(new AddMock( + name: 'testRest', + path: 'testEndpoint', + port: 9999, + predicate: '''{req -> req.xml.name() == 'request'}''', + response: '''{req -> ""}''', + soap: false, + preserveHistory: false + )) + when: + HttpPost restPost = new HttpPost('http://localhost:9999/testEndpoint') + restPost.entity = new StringEntity('', ContentType.create("text/xml", "UTF-8")) + CloseableHttpResponse response = client.execute(restPost) + then: + GPathResult restPostResponse = Util.extractXmlResponse(response) + restPostResponse.name() == 'goodResponseRest-request' + expect: + remoteMockServer.removeMock('testRest')?.size() == 0 + } + + def "should handle empty post"() { + expect: + remoteMockServer.addMock(new AddMock( + name: 'testRest', + path: 'testEndpoint', + port: 9999, + statusCode: 201, + soap: false + )) + when: + HttpPost restPost = new HttpPost('http://localhost:9999/testEndpoint') + CloseableHttpResponse response = client.execute(restPost) + then: + response.statusLine.statusCode == 201 + Util.consumeResponse(response) + expect: + remoteMockServer.removeMock('testRest')?.size() == 1 + } + + def 'should handle leading slash'() { + given: + String name = "testRest-${UUID.randomUUID().toString()}" + expect: + remoteMockServer.addMock(new AddMock( + name: name, + path: mockPath, + port: 9999, + statusCode: 201, + soap: false + )) + when: + HttpPost restPost = new HttpPost("http://localhost:9999/$urlPath") + CloseableHttpResponse response = client.execute(restPost) + then: + response.statusLine.statusCode == 201 + Util.consumeResponse(response) + expect: + remoteMockServer.removeMock(name)?.size() == 1 + where: + mockPath | urlPath + '' | '' + '/' | '' + 'test' | 'test' + '/test' | 'test' + 'test/other' | 'test/other' + '/test/other' | 'test/other' + } + + def 'should match any method'() { + given: + String name = "testRest-${UUID.randomUUID().toString()}" + remoteMockServer.addMock(new AddMock( + name: name, + path: 'any-method', + port: 9999, + statusCode: 201, + soap: false, + method: Method.ANY_METHOD + )) + when: + CloseableHttpResponse response = client.execute(req) + then: + response.statusLine.statusCode == 201 + Util.consumeResponse(response) + cleanup: + remoteMockServer.removeMock(name) + where: + req << [ + new HttpGet('http://localhost:9999/any-method'), + new HttpPost('http://localhost:9999/any-method'), + new HttpPatch('http://localhost:9999/any-method') + ] + } } diff --git a/mockserver-tests/src/test/groovy/eu/ztsh/mockserver/tests/MockServerMaxUsesTest.groovy b/mockserver-tests/src/test/groovy/eu/ztsh/mockserver/tests/MockServerMaxUsesTest.groovy new file mode 100644 index 0000000..eda365c --- /dev/null +++ b/mockserver-tests/src/test/groovy/eu/ztsh/mockserver/tests/MockServerMaxUsesTest.groovy @@ -0,0 +1,239 @@ +package eu.ztsh.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 eu.ztsh.mockserver.api.request.AddMock +import eu.ztsh.mockserver.client.RemoteMockServer +import eu.ztsh.mockserver.server.HttpMockServer +import spock.lang.AutoCleanup +import spock.lang.Specification + +class MockServerMaxUsesTest extends Specification { + + RemoteMockServer remoteMockServer + + @AutoCleanup('stop') + HttpMockServer httpMockServer + + 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 + and:'mock should exist' + remoteMockServer.listMocks().find { it.name == 'mock1' } != null + } + + 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-tests/src/test/groovy/pl/touk/mockserver/tests/ServerMockPT.groovy b/mockserver-tests/src/test/groovy/pl/touk/mockserver/tests/ServerMockPT.groovy deleted file mode 100644 index 2ee891c..0000000 --- a/mockserver-tests/src/test/groovy/pl/touk/mockserver/tests/ServerMockPT.groovy +++ /dev/null @@ -1,53 +0,0 @@ -package pl.touk.mockserver.tests - -import groovy.util.slurpersupport.GPathResult -import org.apache.http.client.HttpClient -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.HttpClients -import pl.touk.mockserver.client.AddMockRequestData -import pl.touk.mockserver.client.RemoteMockServer -import pl.touk.mockserver.client.Util -import pl.touk.mockserver.server.HttpMockServer -import spock.lang.Specification - -class ServerMockPT extends Specification { - - def "should handle many request simultaneously"() { - given: - HttpMockServer httpMockServer = new HttpMockServer() - RemoteMockServer controlServerClient = new RemoteMockServer("localhost", 9999) - HttpClient client = HttpClients.createDefault() - int requestAmount = 1000 - GPathResult[] responses = new GPathResult[requestAmount] - Thread[] threads = new Thread[requestAmount] - for (int i = 0; i < requestAmount; ++i) { - int current = i - threads[i] = new Thread({ - int endpointNumber = current % 10 - int port = 9000 + (current % 7) - controlServerClient.addMock(new AddMockRequestData( - name: "testRest$current", - path: "testEndpoint$endpointNumber", - port: port, - predicate: """{req -> req.xml.name() == 'request$current'}""", - response: """{req -> ""}""" - )) - HttpPost restPost = new HttpPost("http://localhost:$port/testEndpoint$endpointNumber") - restPost.entity = new StringEntity("", ContentType.create("text/xml", "UTF-8")) - CloseableHttpResponse response = client.execute(restPost) - responses[current] = Util.extractXmlResponse(response) - assert controlServerClient.removeMock("testRest$current").size() == 1 - }) - } - when: - threads*.start() - Thread.sleep(60000) - then: - responses.eachWithIndex { res, i -> assert res.name() == "goodResponse$i" } - cleanup: - httpMockServer.stop() - } -} diff --git a/mockserver-tests/src/test/resources/keystore.jks b/mockserver-tests/src/test/resources/keystore.jks new file mode 100644 index 0000000..d5e35d1 Binary files /dev/null and b/mockserver-tests/src/test/resources/keystore.jks differ diff --git a/mockserver-tests/src/test/resources/schema1.xsd b/mockserver-tests/src/test/resources/schema1.xsd new file mode 100644 index 0000000..4ca340a --- /dev/null +++ b/mockserver-tests/src/test/resources/schema1.xsd @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + diff --git a/mockserver-tests/src/test/resources/schema2.xsd b/mockserver-tests/src/test/resources/schema2.xsd new file mode 100644 index 0000000..611d010 --- /dev/null +++ b/mockserver-tests/src/test/resources/schema2.xsd @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/mockserver-tests/src/test/resources/trusted.jks b/mockserver-tests/src/test/resources/trusted.jks new file mode 100644 index 0000000..e6fa704 Binary files /dev/null and b/mockserver-tests/src/test/resources/trusted.jks differ diff --git a/mockserver-tests/src/test/resources/truststore.jks b/mockserver-tests/src/test/resources/truststore.jks new file mode 100644 index 0000000..27a8332 Binary files /dev/null and b/mockserver-tests/src/test/resources/truststore.jks differ diff --git a/mockserver-tests/src/test/resources/untrusted.jks b/mockserver-tests/src/test/resources/untrusted.jks new file mode 100644 index 0000000..ca94b45 Binary files /dev/null and b/mockserver-tests/src/test/resources/untrusted.jks differ diff --git a/mockserver/pom.xml b/mockserver/pom.xml index db754f2..d097048 100644 --- a/mockserver/pom.xml +++ b/mockserver/pom.xml @@ -1,49 +1,95 @@ - - - http-mock-server - pl.touk.mockserver - 1.1.0 - + 4.0.0 + + eu.ztsh.mockserver + http-mock-server + 3.0.0-SNAPSHOT + + mockserver - org.codehaus.groovy - groovy-all + eu.ztsh.mockserver + mockserver-api + + + org.apache.groovy + groovy + + + org.apache.groovy + groovy-json + + + org.apache.groovy + groovy-xml + + + + org.glassfish.jaxb + jaxb-core + + + org.glassfish.jaxb + jaxb-runtime + + + + org.apache.commons + commons-lang3 + + org.slf4j slf4j-api ch.qos.logback - logback-classic + logback-core - org.apache.commons - commons-lang3 + ch.qos.logback + logback-classic clean package assembly:single install + + org.codehaus.gmavenplus + gmavenplus-plugin + maven-assembly-plugin - pl.touk.mockserver.server.Main + eu.ztsh.mockserver.server.Main jar-with-dependencies + mockserver-full + false + + + create-archive + package + + single + + + + diff --git a/mockserver/src/main/groovy/eu/ztsh/mockserver/server/ContextExecutor.groovy b/mockserver/src/main/groovy/eu/ztsh/mockserver/server/ContextExecutor.groovy new file mode 100644 index 0000000..6023a1c --- /dev/null +++ b/mockserver/src/main/groovy/eu/ztsh/mockserver/server/ContextExecutor.groovy @@ -0,0 +1,112 @@ +package eu.ztsh.mockserver.server + +import com.sun.net.httpserver.HttpExchange +import groovy.transform.PackageScope +import groovy.util.logging.Slf4j +import eu.ztsh.mockserver.api.common.Method + +import java.util.concurrent.CopyOnWriteArrayList + +@Slf4j +@PackageScope +class ContextExecutor { + private final HttpServerWrapper httpServerWrapper + final String path + private final List mocks + + ContextExecutor(HttpServerWrapper httpServerWrapper, Mock initialMock) { + this.httpServerWrapper = httpServerWrapper + this.path = "/${initialMock.path}" + this.mocks = new CopyOnWriteArrayList<>([initialMock]) + httpServerWrapper.createContext(path) { + HttpExchange ex -> + try { + applyMocks(ex) + } catch (Exception e) { + log.error("Exceptiony occured handling request", e) + throw e + } finally { + ex.close() + } + } + } + + private void applyMocks(HttpExchange ex) { + MockRequest request = new MockRequest(ex.requestBody.text, ex.requestHeaders, ex.requestURI) + log.info('Mock received input') + log.debug("Request: ${request.text}") + for (Mock mock : mocks) { + 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}") + return + } + log.debug("Mock ${mock.name} does not match request") + } catch (Exception e) { + log.warn("An exception occured when matching or applying mock ${mock.name}", e) + } + } + log.warn("Any mock does not match request ${request.text}") + Util.createResponse(ex, request.text, 404) + } + + String getPath() { + return path.substring(1) + } + + String getContextPath() { + return path + } + + private static void fillExchange(HttpExchange httpExchange, MockResponse response) { + response.headers.each { + httpExchange.responseHeaders.add(it.key, it.value) + } + Util.createResponse(httpExchange, response.text, response.statusCode) + } + + List removeMock(String name) { + Mock mock = mocks.find { it.name == name } + if (mock) { + mocks.remove(mock) + return mock.history + } + return [] + } + + List peekMock(String name) { + Mock mock = mocks.find { it.name == name } + if (mock) { + return mock.history + } + return [] + } + + void addMock(Mock mock) { + mocks << mock + } + + List getMocks() { + return mocks + } + + private synchronized void handleMaxUses(Mock mock) { + if (mock.hasLimitedUses()) { + mock.decrementUses() + resetIfNeeded(mock) + log.debug("Uses left ${mock.usesLeft} of ${mock.maxUses} (is cyclic: ${mock.cyclic})") + } + } + + private void resetIfNeeded(Mock mock) { + if (mock.shouldUsesBeReset()) { + mock.resetUses() + mocks.remove(mock) + mocks.add(mock) + } + } +} diff --git a/mockserver/src/main/groovy/eu/ztsh/mockserver/server/HttpMockServer.groovy b/mockserver/src/main/groovy/eu/ztsh/mockserver/server/HttpMockServer.groovy new file mode 100644 index 0000000..eaae94d --- /dev/null +++ b/mockserver/src/main/groovy/eu/ztsh/mockserver/server/HttpMockServer.groovy @@ -0,0 +1,271 @@ +package eu.ztsh.mockserver.server + +import com.sun.net.httpserver.HttpExchange +import groovy.util.logging.Slf4j +import eu.ztsh.mockserver.api.common.Https +import eu.ztsh.mockserver.api.common.ImportAlias +import eu.ztsh.mockserver.api.common.Method +import eu.ztsh.mockserver.api.request.AddMock +import eu.ztsh.mockserver.api.request.MockServerRequest +import eu.ztsh.mockserver.api.request.PeekMock +import eu.ztsh.mockserver.api.request.RemoveMock +import eu.ztsh.mockserver.api.response.ExceptionOccured +import eu.ztsh.mockserver.api.response.MockAdded +import eu.ztsh.mockserver.api.response.MockEventReport +import eu.ztsh.mockserver.api.response.MockPeeked +import eu.ztsh.mockserver.api.response.MockRemoved +import eu.ztsh.mockserver.api.response.MockReport +import eu.ztsh.mockserver.api.response.MockRequestReport +import eu.ztsh.mockserver.api.response.MockResponseReport +import eu.ztsh.mockserver.api.response.Mocks +import eu.ztsh.mockserver.api.response.Parameter + +import jakarta.xml.bind.JAXBContext +import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.CopyOnWriteArraySet +import java.util.concurrent.Executor +import java.util.concurrent.Executors + +import static eu.ztsh.mockserver.server.Util.createResponse + +@Slf4j +class HttpMockServer { + + private final HttpServerWrapper httpServerWrapper + private final Map childServers = new ConcurrentHashMap<>() + private final Set mockNames = new CopyOnWriteArraySet<>() + private final ConfigObject configuration = new ConfigObject() + private final Executor executor + + private static + final JAXBContext requestJaxbContext = JAXBContext.newInstance(AddMock.package.name, AddMock.classLoader) + + HttpMockServer(int port = 9999, ConfigObject initialConfiguration = new ConfigObject(), int threads = 10) { + executor = Executors.newFixedThreadPool(threads) + httpServerWrapper = new HttpServerWrapper(port, executor) + + initialConfiguration.values()?.each { ConfigObject co -> + addMock(co) + } + + httpServerWrapper.createContext('/serverControl', { + HttpExchange ex -> + try { + if (ex.requestMethod == 'GET') { + 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) { + addMock(request, ex) + } else if (request instanceof RemoveMock) { + removeMock(request, ex) + } else if (request instanceof PeekMock) { + peekMock(request, ex) + } else { + throw new RuntimeException('Unknown request') + } + } else { + throw new RuntimeException('Unknown request') + } + } catch (Exception e) { + createErrorResponse(ex, e) + } + }) + } + + void listMocks(HttpExchange ex) { + Mocks mockListing = new Mocks( + mocks: listMocks().collect { + new MockReport( + name: it.name, + path: it.path, + port: it.port, + predicate: it.predicateClosureText, + response: it.responseClosureText, + responseHeaders: it.responseHeadersClosureText, + soap: it.soap, + method: it.method, + statusCode: it.statusCode as int, + schema: it.schema, + imports: it.imports.collect { new ImportAlias(alias: it.key, fullClassName: it.value) }, + preserveHistory: it.preserveHistory + ) + } + ) + createResponse(ex, mockListing, 200) + } + + Set listMocks() { + return childServers.values().collect { it.mocks }.flatten() as TreeSet + } + + private void addMock(AddMock request, HttpExchange ex) { + String name = request.name + 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) + 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') + } + 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) + 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] } ?: [:] + mock.predicate = request.predicate + mock.response = request.response + mock.soap = request.soap + mock.statusCode = request.statusCode + mock.method = request.method + mock.responseHeaders = request.responseHeaders + mock.schema = request.schema + mock.preserveHistory = request.preserveHistory != false + mock.https = request.https + mock.maxUses = request.maxUses + mock.cyclic = request.cyclic + 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 + mock.preserveHistory = co.preserveHistory != false + if (co.https) { + mock.https = new Https( + keystorePath: co.https.keystorePath ?: null, + keystorePassword: co.https.keystorePassword, + keyPassword: co.https.keyPassword, + truststorePath: co.https.truststorePath, + truststorePassword: co.https.truststorePassword, + requireClientAuth: co.https?.requireClientAuth?.asBoolean() ?: false + ) + } + mock.maxUses = co.maxUses ?: null + mock.cyclic = co.cyclic ?: null + return mock + } + + private HttpServerWrapper getOrCreateChildServer(int mockPort, Https https) { + HttpServerWrapper child = childServers[mockPort] + if (!child) { + child = new HttpServerWrapper(mockPort, executor, https) + childServers.put(mockPort, child) + } + return child + } + + private void removeMock(RemoveMock request, HttpExchange ex) { + String name = request.name + boolean skipReport = request.skipReport ?: false + if (!(name in mockNames)) { + throw new RuntimeException('mock not registered') + } + log.info("Removing mock $name") + List mockEvents = skipReport ? [] : childServers.values().collect { + it.removeMock(name) + }.flatten() as List + mockNames.remove(name) + configuration.remove(name) + MockRemoved mockRemoved = new MockRemoved( + mockEvents: createMockEventReports(mockEvents) + ) + createResponse(ex, mockRemoved, 200) + } + + private static List createMockEventReports(List mockEvents) { + return mockEvents.collect { + new MockEventReport( + request: new MockRequestReport( + text: it.request.text, + headers: new MockRequestReport.Headers(headers: it.request.headers.collect { + new Parameter(name: it.key, value: it.value) + }), + queryParams: new MockRequestReport.QueryParams(queryParams: it.request.query.collect { + new Parameter(name: it.key, value: it.value) + }), + path: new MockRequestReport.Path(pathParts: it.request.path) + ), + response: new MockResponseReport( + statusCode: it.response.statusCode, + text: it.response.text, + headers: new MockResponseReport.Headers(headers: it.response.headers.collect { + new Parameter(name: it.key, value: it.value) + }) + ) + ) + } + } + + private void peekMock(PeekMock request, HttpExchange ex) { + String name = request.name + if (!(name in mockNames)) { + throw new RuntimeException('mock not registered') + } + log.trace("Peeking mock $name") + List mockEvents = childServers.values().collect { it.peekMock(name) }.flatten() as List + MockPeeked mockPeeked = new MockPeeked( + mockEvents: createMockEventReports(mockEvents) + ) + createResponse(ex, mockPeeked, 200) + } + + private static void createErrorResponse(HttpExchange ex, Exception e) { + log.warn('Exception occured', e) + createResponse(ex, new ExceptionOccured(value: e.message), 400) + } + + void stop() { + childServers.values().each { it.stop() } + httpServerWrapper.stop() + } +} diff --git a/mockserver/src/main/groovy/eu/ztsh/mockserver/server/HttpServerWrapper.groovy b/mockserver/src/main/groovy/eu/ztsh/mockserver/server/HttpServerWrapper.groovy new file mode 100644 index 0000000..5d33156 --- /dev/null +++ b/mockserver/src/main/groovy/eu/ztsh/mockserver/server/HttpServerWrapper.groovy @@ -0,0 +1,106 @@ +package eu.ztsh.mockserver.server + +import com.sun.net.httpserver.HttpHandler +import com.sun.net.httpserver.HttpServer +import com.sun.net.httpserver.HttpsServer +import groovy.transform.PackageScope +import groovy.util.logging.Slf4j +import eu.ztsh.mockserver.api.common.Https + +import javax.net.ssl.KeyManager +import javax.net.ssl.KeyManagerFactory +import javax.net.ssl.SSLContext +import javax.net.ssl.TrustManager +import javax.net.ssl.TrustManagerFactory +import java.security.KeyStore +import java.security.SecureRandom +import java.util.concurrent.Executor + +@Slf4j +@PackageScope +class HttpServerWrapper { + private final HttpServer httpServer + final int port + + private List executors = [] + + HttpServerWrapper(int port, Executor executor, Https https = null) { + this.port = port + InetSocketAddress addr = new InetSocketAddress(Inet4Address.getByName("0.0.0.0"), port) + httpServer = buildServer(addr, https) + httpServer.executor = executor + log.info("Http server starting on port $port...") + httpServer.start() + log.info('Http server is started') + } + + private HttpServer buildServer(InetSocketAddress addr, Https https) { + if (https) { + HttpsServer httpsServer = HttpsServer.create(addr, 0) + httpsServer.httpsConfigurator = new HttpsConfig(buildSslContext(https), https) + return httpsServer + } else { + return HttpServer.create(addr, 0) + } + } + + private SSLContext buildSslContext(Https https) { + KeyManager[] keyManagers = buildKeyManager(https) + TrustManager[] trustManagers = buildTrustManager(https) + + SSLContext ssl = SSLContext.getInstance('TLSv1') + ssl.init(keyManagers, trustManagers, new SecureRandom()) + return ssl + } + + private KeyManager[] buildKeyManager(Https https) { + KeyStore keyStore = KeyStore.getInstance('jks') + keyStore.load(new FileInputStream(https.keystorePath), https.keystorePassword.toCharArray()) + KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.defaultAlgorithm) + kmf.init(keyStore, https.keyPassword.toCharArray()) + return kmf.keyManagers + } + + private TrustManager[] buildTrustManager(Https https) { + if (https.requireClientAuth) { + KeyStore trustStore = KeyStore.getInstance('jks') + trustStore.load(new FileInputStream(https.truststorePath), https.truststorePassword.toCharArray()) + TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.defaultAlgorithm) + tmf.init(trustStore) + return tmf.trustManagers + } else { + return [] + } + } + + void createContext(String context, HttpHandler handler) { + httpServer.createContext(context, handler) + } + + void addMock(Mock mock) { + ContextExecutor executor = executors.find { it.path == mock.path } + if (executor) { + executor.addMock(mock) + } else { + executors << new ContextExecutor(this, mock) + } + log.info("Added mock ${mock.name}") + } + + void stop() { + executors.each { httpServer.removeContext(it.contextPath) } + httpServer.stop(0) + } + + List removeMock(String name) { + return executors.collect { it.removeMock(name) }.flatten() as List + } + + List peekMock(String name) { + return executors.collect { it.peekMock(name) }.flatten() as List + } + + List getMocks() { + return executors.collect { it.mocks }.flatten() as List + } +} diff --git a/mockserver/src/main/groovy/eu/ztsh/mockserver/server/HttpsConfig.groovy b/mockserver/src/main/groovy/eu/ztsh/mockserver/server/HttpsConfig.groovy new file mode 100644 index 0000000..9d00714 --- /dev/null +++ b/mockserver/src/main/groovy/eu/ztsh/mockserver/server/HttpsConfig.groovy @@ -0,0 +1,28 @@ +package eu.ztsh.mockserver.server + +import com.sun.net.httpserver.HttpsConfigurator +import com.sun.net.httpserver.HttpsParameters +import groovy.transform.CompileStatic +import eu.ztsh.mockserver.api.common.Https + +import javax.net.ssl.SSLContext +import javax.net.ssl.SSLParameters + +@CompileStatic +class HttpsConfig extends HttpsConfigurator { + private final Https https + + HttpsConfig(SSLContext sslContext, Https https) { + super(sslContext) + this.https = https + } + + @Override + void configure(HttpsParameters httpsParameters) { + SSLContext sslContext = getSSLContext() + SSLParameters sslParameters = sslContext.defaultSSLParameters + sslParameters.needClientAuth = https.requireClientAuth + httpsParameters.needClientAuth = https.requireClientAuth + httpsParameters.SSLParameters = sslParameters + } +} diff --git a/mockserver/src/main/groovy/eu/ztsh/mockserver/server/Main.groovy b/mockserver/src/main/groovy/eu/ztsh/mockserver/server/Main.groovy new file mode 100644 index 0000000..916569e --- /dev/null +++ b/mockserver/src/main/groovy/eu/ztsh/mockserver/server/Main.groovy @@ -0,0 +1,33 @@ +package eu.ztsh.mockserver.server + +import groovy.util.logging.Slf4j + +@Slf4j +class Main { + static void main(String[] args) { + HttpMockServer httpMockServer = startMockServer(args) + + Runtime.runtime.addShutdownHook(new Thread({ + log.info('Http server is stopping...') + httpMockServer.stop() + log.info('Http server is stopped') + } as Runnable)) + + while (true) { + Thread.sleep(10000) + } + } + + private static HttpMockServer startMockServer(String... args) { + switch (args.length) { + case 1: + return new HttpMockServer(args[0] as int, new ConfigObject()) + case 2: + return new HttpMockServer(args[0] as int, new ConfigSlurper().parse(new File(args[1]).toURI().toURL())) + case 3: + return new HttpMockServer(args[0] as int, new ConfigSlurper().parse(new File(args[1]).toURI().toURL()), args[2] as int) + default: + return new HttpMockServer() + } + } +} diff --git a/mockserver/src/main/groovy/eu/ztsh/mockserver/server/Mock.groovy b/mockserver/src/main/groovy/eu/ztsh/mockserver/server/Mock.groovy new file mode 100644 index 0000000..821471c --- /dev/null +++ b/mockserver/src/main/groovy/eu/ztsh/mockserver/server/Mock.groovy @@ -0,0 +1,199 @@ +package eu.ztsh.mockserver.server + +import groovy.transform.EqualsAndHashCode +import groovy.transform.PackageScope +import groovy.util.logging.Slf4j +import org.codehaus.groovy.control.CompilerConfiguration +import org.codehaus.groovy.control.customizers.ImportCustomizer +import eu.ztsh.mockserver.api.common.Https +import eu.ztsh.mockserver.api.common.Method + +import javax.xml.XMLConstants +import javax.xml.transform.stream.StreamSource +import javax.xml.validation.SchemaFactory +import javax.xml.validation.Validator +import java.util.concurrent.CopyOnWriteArrayList + +@PackageScope +@EqualsAndHashCode(excludes = ["counter"]) +@Slf4j +class Mock implements Comparable { + final String name + final String path + final int port + String predicateClosureText = '{ _ -> true }' + String responseClosureText = '''{ _ -> '' }''' + String responseHeadersClosureText = '{ _ -> [:] }' + Closure predicate = toClosure(predicateClosureText) + Closure response = toClosure(responseClosureText) + Closure responseHeaders = toClosure(responseHeadersClosureText) + boolean soap = false + int statusCode = 200 + Method method = Method.POST + int counter = 0 + final List history = new CopyOnWriteArrayList<>() + String schema + private Validator validator + Map imports = [:] + boolean preserveHistory = true + Https https + int maxUses = -1 + int usesLeft + boolean cyclic + + Mock(String name, String path, int port) { + if (!(name)) { + throw new RuntimeException("Mock name must be given") + } + this.name = name + this.path = stripLeadingSlash(path) + this.port = port + } + + private static String stripLeadingSlash(String path) { + if (path?.startsWith('/')) { + return path - '/' + } else { + return path + } + } + + boolean match(Method method, MockRequest request) { + boolean usesCondition = hasLimitedUses() ? usesLeft > 0 : true + return usesCondition && (this.method == method || this.method == Method.ANY_METHOD) && predicate(request) + } + + MockResponse apply(MockRequest request) { + log.debug("Mock $name invoked") + if (validator) { + try { + log.debug('Validating...') + if (soap) { + validator.validate(new StreamSource(new StringReader(request.textWithoutSoap))) + } else { + validator.validate(new StreamSource(new StringReader(request.text))) + } + } catch (Exception e) { + MockResponse response = new MockResponse(400, e.message, [:]) + if(preserveHistory) { + history << new MockEvent(request, response) + } + return response + } + } + ++counter + String responseText = response(request) + String response = soap ? wrapSoap(responseText) : responseText + Map headers = responseHeaders(request) + MockResponse mockResponse = new MockResponse(statusCode, response, headers) + if(preserveHistory) { + history << new MockEvent(request, mockResponse) + } + return mockResponse + } + + private static String wrapSoap(String request) { + """ + + ${request} + """ + } + + void setPredicate(String predicate) { + if (predicate) { + this.predicateClosureText = predicate + this.predicate = toClosure(predicate) + } + } + + private Closure toClosure(String predicate) { + if (predicate ==~ /(?m).*System\s*\.\s*exit\s*\(.*/) { + throw new RuntimeException('System.exit is forbidden') + } + CompilerConfiguration compilerConfiguration = new CompilerConfiguration() + ImportCustomizer customizer = new ImportCustomizer() + imports.each { + customizer.addImport(it.key, it.value) + } + compilerConfiguration.addCompilationCustomizers(customizer) + GroovyShell sh = new GroovyShell(this.class.classLoader, compilerConfiguration); + Closure closure = sh.evaluate(predicate) as Closure + sh.resetLoadedClasses() + return closure + } + + void setResponse(String response) { + if (response) { + this.responseClosureText = response + this.response = toClosure(response) + } + } + + void setSoap(Boolean soap) { + this.soap = soap ?: false + } + + void setStatusCode(String statusCode) { + if (statusCode) { + this.statusCode = Integer.valueOf(statusCode) + } + } + + void setMethod(Method method) { + if (method) { + this.method = method + } + } + + void setResponseHeaders(String responseHeaders) { + if (responseHeaders) { + this.responseHeadersClosureText = responseHeaders + this.responseHeaders = toClosure(responseHeaders) + } + } + + 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) + } + + void setSchema(String schema) { + this.schema = schema + if (schema) { + try { + validator = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI) + .newSchema(this.class.getResource("/$schema")) + .newValidator() + } catch (Exception e) { + throw new RuntimeException('mock request schema is invalid schema', e) + } + } + } + + boolean hasLimitedUses() { + return maxUses > 0 + } + + void decrementUses() { + usesLeft-- + } + + boolean shouldUsesBeReset() { + return hasLimitedUses() && usesLeft <= 0 && cyclic + } + + void resetUses() { + setMaxUses(maxUses) + } +} diff --git a/mockserver/src/main/groovy/pl/touk/mockserver/server/MockEvent.groovy b/mockserver/src/main/groovy/eu/ztsh/mockserver/server/MockEvent.groovy similarity index 88% rename from mockserver/src/main/groovy/pl/touk/mockserver/server/MockEvent.groovy rename to mockserver/src/main/groovy/eu/ztsh/mockserver/server/MockEvent.groovy index 97a46a2..9c429f1 100644 --- a/mockserver/src/main/groovy/pl/touk/mockserver/server/MockEvent.groovy +++ b/mockserver/src/main/groovy/eu/ztsh/mockserver/server/MockEvent.groovy @@ -1,4 +1,4 @@ -package pl.touk.mockserver.server +package eu.ztsh.mockserver.server import groovy.transform.PackageScope diff --git a/mockserver/src/main/groovy/pl/touk/mockserver/server/MockRequest.groovy b/mockserver/src/main/groovy/eu/ztsh/mockserver/server/MockRequest.groovy similarity index 77% rename from mockserver/src/main/groovy/pl/touk/mockserver/server/MockRequest.groovy rename to mockserver/src/main/groovy/eu/ztsh/mockserver/server/MockRequest.groovy index 5e6ddc8..a714afe 100644 --- a/mockserver/src/main/groovy/pl/touk/mockserver/server/MockRequest.groovy +++ b/mockserver/src/main/groovy/eu/ztsh/mockserver/server/MockRequest.groovy @@ -1,9 +1,11 @@ -package pl.touk.mockserver.server +package eu.ztsh.mockserver.server import com.sun.net.httpserver.Headers import groovy.json.JsonSlurper import groovy.transform.PackageScope -import groovy.util.slurpersupport.GPathResult +import groovy.xml.XmlSlurper +import groovy.xml.slurpersupport.GPathResult +import groovy.xml.XmlUtil @PackageScope class MockRequest { @@ -26,6 +28,9 @@ class MockRequest { } private static GPathResult inputToXml(String text) { + if (!text.startsWith('<')) { + return null + } try { return new XmlSlurper().parseText(text) } catch (Exception _) { @@ -35,7 +40,7 @@ class MockRequest { private static GPathResult inputToSoap(GPathResult xml) { try { - if (xml.name() == 'Envelope' && xml.Body.size() > 0) { + if (xml != null && xml.name() == 'Envelope' && xml.Body.size() > 0) { return getSoapBodyContent(xml) } else { return null @@ -46,10 +51,13 @@ class MockRequest { } private static GPathResult getSoapBodyContent(GPathResult xml) { - return xml.Body.'**'[1] + return xml.Body.'**'[1] as GPathResult } private static Object inputToJson(String text) { + if (!text.startsWith('[') && !text.startsWith('{')) { + return null + } try { return new JsonSlurper().parseText(text) } catch (Exception _) { @@ -66,7 +74,10 @@ class MockRequest { private static Map headersToMap(Headers headers) { return headers.collectEntries { [it.key.toLowerCase(), it.value.join(',')] - } + } as Map } + String getTextWithoutSoap() { + return XmlUtil.serialize(soap) + } } diff --git a/mockserver/src/main/groovy/pl/touk/mockserver/server/MockResponse.groovy b/mockserver/src/main/groovy/eu/ztsh/mockserver/server/MockResponse.groovy similarity index 90% rename from mockserver/src/main/groovy/pl/touk/mockserver/server/MockResponse.groovy rename to mockserver/src/main/groovy/eu/ztsh/mockserver/server/MockResponse.groovy index 913f9f3..260acce 100644 --- a/mockserver/src/main/groovy/pl/touk/mockserver/server/MockResponse.groovy +++ b/mockserver/src/main/groovy/eu/ztsh/mockserver/server/MockResponse.groovy @@ -1,4 +1,4 @@ -package pl.touk.mockserver.server +package eu.ztsh.mockserver.server import groovy.transform.PackageScope diff --git a/mockserver/src/main/groovy/eu/ztsh/mockserver/server/Util.groovy b/mockserver/src/main/groovy/eu/ztsh/mockserver/server/Util.groovy new file mode 100644 index 0000000..c51d1f0 --- /dev/null +++ b/mockserver/src/main/groovy/eu/ztsh/mockserver/server/Util.groovy @@ -0,0 +1,32 @@ +package eu.ztsh.mockserver.server + +import com.sun.net.httpserver.HttpExchange +import eu.ztsh.mockserver.api.response.MockAdded + +import jakarta.xml.bind.JAXBContext + +class Util { + + private static + final JAXBContext responseJaxbContext = JAXBContext.newInstance(MockAdded.package.name, MockAdded.classLoader) + + static void createResponse(HttpExchange ex, Object response, int statusCode) { + String responseString = marshall(response) + createResponse(ex, responseString, statusCode) + } + + static void createResponse(HttpExchange ex, String responseString, int statusCode) { + byte[] responseBytes = responseString ? responseString.getBytes('UTF-8') : new byte[0] + ex.sendResponseHeaders(statusCode, responseBytes.length ?: -1) + if (responseString) { + ex.responseBody << responseBytes + ex.responseBody.close() + } + } + + private static String marshall(Object response) { + StringWriter sw = new StringWriter() + responseJaxbContext.createMarshaller().marshal(response, sw) + return sw.toString() + } +} diff --git a/mockserver/src/main/groovy/pl/touk/mockserver/server/ContextExecutor.groovy b/mockserver/src/main/groovy/pl/touk/mockserver/server/ContextExecutor.groovy deleted file mode 100644 index d5a8230..0000000 --- a/mockserver/src/main/groovy/pl/touk/mockserver/server/ContextExecutor.groovy +++ /dev/null @@ -1,83 +0,0 @@ -package pl.touk.mockserver.server - -import com.sun.net.httpserver.HttpExchange -import groovy.transform.PackageScope -import groovy.util.logging.Slf4j - -import java.util.concurrent.CopyOnWriteArrayList - -@Slf4j -@PackageScope -class ContextExecutor { - private final HttpServerWraper httpServerWraper - final String path - private final List mocks - - ContextExecutor(HttpServerWraper httpServerWraper, Mock initialMock) { - this.httpServerWraper = httpServerWraper - this.path = '/' + initialMock.path - this.mocks = new CopyOnWriteArrayList<>([initialMock]) - httpServerWraper.createContext(path) { - HttpExchange ex -> - MockRequest request = new MockRequest(ex.requestBody.text, ex.requestHeaders, ex.requestURI) - log.info("Mock received input") - log.debug("Request: ${request.text}") - for (Mock mock : mocks) { - try { - if (mock.match(ex.requestMethod, request)) { - log.debug("Mock ${mock.name} match request ${request.text}") - MockResponse httpResponse = mock.apply(request) - fillExchange(ex, httpResponse) - log.trace("Mock ${mock.name} response with body ${httpResponse.text}") - return - } - log.debug("Mock ${mock.name} does not match request") - } catch (Exception e) { - log.warn("An exception occured when matching or applying mock ${mock.name}", e) - } - } - log.warn("Any mock does not match request ${request.text}") - Util.createResponse(ex, request.text, 404) - } - } - - String getPath() { - return path.substring(1) - } - - String getContextPath() { - return path - } - - private static void fillExchange(HttpExchange httpExchange, MockResponse response) { - response.headers.each { - httpExchange.responseHeaders.add(it.key, it.value) - } - Util.createResponse(httpExchange, response.text, response.statusCode) - } - - List removeMock(String name) { - Mock mock = mocks.find { it.name == name } - if (mock) { - mocks.remove(mock) - return mock.history - } - return [] - } - - List peekMock(String name) { - Mock mock = mocks.find { it.name == name } - if (mock) { - return mock.history - } - return [] - } - - void addMock(Mock mock) { - mocks << mock - } - - List getMocks() { - return mocks - } -} diff --git a/mockserver/src/main/groovy/pl/touk/mockserver/server/HttpMockServer.groovy b/mockserver/src/main/groovy/pl/touk/mockserver/server/HttpMockServer.groovy deleted file mode 100644 index eae893d..0000000 --- a/mockserver/src/main/groovy/pl/touk/mockserver/server/HttpMockServer.groovy +++ /dev/null @@ -1,191 +0,0 @@ -package pl.touk.mockserver.server - -import com.sun.net.httpserver.HttpExchange -import groovy.util.logging.Slf4j -import groovy.util.slurpersupport.GPathResult -import groovy.xml.MarkupBuilder - -import java.util.concurrent.CopyOnWriteArrayList -import java.util.concurrent.CopyOnWriteArraySet - -import static pl.touk.mockserver.server.Util.createResponse - -@Slf4j -class HttpMockServer { - - private final HttpServerWraper httpServerWraper - private final List childServers = new CopyOnWriteArrayList<>() - private final Set mockNames = new CopyOnWriteArraySet<>() - - HttpMockServer(int port = 9999) { - httpServerWraper = new HttpServerWraper(port) - - httpServerWraper.createContext('/serverControl', { - HttpExchange ex -> - try { - if (ex.requestMethod == 'GET') { - listMocks(ex) - } else if (ex.requestMethod == 'POST') { - GPathResult request = new XmlSlurper().parse(ex.requestBody) - if (request.name() == 'addMock') { - addMock(request, ex) - } else if (request.name() == 'removeMock') { - removeMock(request, ex) - } else if (request.name() == 'peekMock') { - peekMock(request, ex) - } else { - throw new RuntimeException('Unknown request') - } - } else { - throw new RuntimeException('Unknown request') - } - } catch (Exception e) { - createErrorResponse(ex, e) - } - }) - } - - void listMocks(HttpExchange ex) { - StringWriter sw = new StringWriter() - MarkupBuilder builder = new MarkupBuilder(sw) - builder.mocks { - listMocks().each { - Mock mock -> - builder.mock { - name mock.name - path mock.path - port mock.port - predicate mock.predicateClosureText - response mock.responseClosureText - responseHeaders mock.responseHeadersClosureText - } - } - } - createResponse(ex, sw.toString(), 200) - } - - Set listMocks() { - return childServers.collect { it.mocks }.flatten() as TreeSet - } - - private void addMock(GPathResult request, HttpExchange ex) { - String name = request.name - if (name in mockNames) { - throw new RuntimeException('mock already registered') - } - Mock mock = mockFromRequest(request) - HttpServerWraper child = getOrCreateChildServer(mock.port) - child.addMock(mock) - mockNames << name - createResponse(ex, '', 200) - } - - private static Mock mockFromRequest(GPathResult request) { - String name = request.name - String mockPath = request.path - int mockPort = Integer.valueOf(request.port as String) - Mock mock = new Mock(name, mockPath, mockPort) - mock.predicate = request.predicate - mock.response = request.response - mock.soap = request.soap - mock.statusCode = request.statusCode - mock.method = request.method - mock.responseHeaders = request.responseHeaders - return mock - } - - private HttpServerWraper getOrCreateChildServer(int mockPort) { - HttpServerWraper child = childServers.find { it.port == mockPort } - if (!child) { - child = new HttpServerWraper(mockPort) - childServers << child - } - return child - } - - private void removeMock(GPathResult request, HttpExchange ex) { - String name = request.name - boolean skipReport = Boolean.parseBoolean(request.skipReport?.toString() ?: 'false') - if (!(name in mockNames)) { - throw new RuntimeException('mock not registered') - } - log.info("Removing mock $name") - List mockEvents = skipReport ? [] : childServers.collect { it.removeMock(name) }.flatten() - mockNames.remove(name) - createResponse(ex, createMockRemovedResponse(mockEvents), 200) - } - - private void peekMock(GPathResult request, HttpExchange ex) { - String name = request.name - if (!(name in mockNames)) { - throw new RuntimeException('mock not registered') - } - log.trace("Peeking mock $name") - List mockEvents = childServers.collect { it.peekMock(name) }.flatten() - createResponse(ex, createMockPeekedResponse(mockEvents), 200) - } - - private static String createMockRemovedResponse(List mockEvents) { - StringWriter sw = new StringWriter() - MarkupBuilder builder = new MarkupBuilder(sw) - builder.mockRemoved { - mockEventsToXml(mockEvents, builder) - } - return sw.toString() - } - - private static String createMockPeekedResponse(List mockEvents) { - StringWriter sw = new StringWriter() - MarkupBuilder builder = new MarkupBuilder(sw) - builder.mockPeeked { - mockEventsToXml(mockEvents, builder) - } - return sw.toString() - } - - private static void mockEventsToXml(List events, MarkupBuilder builder) { - events.each { MockEvent event -> - builder.mockEvent { - builder.request { - text event.request.text - headers { - event.request.headers.each { - builder.param(name: it.key, it.value) - } - } - query { - event.request.query.each { - builder.param(name: it.key, it.value) - } - } - path { - event.request.path.each { - builder.elem it - } - } - } - builder.response { - text event.response.text - headers { - event.response.headers.each { - builder.param(name: it.key, it.value) - } - } - statusCode event.response.statusCode - } - } - } - } - - private static void createErrorResponse(HttpExchange ex, Exception e) { - StringWriter sw = new StringWriter() - MarkupBuilder builder = new MarkupBuilder(sw) - builder.exceptionOccured e.message - createResponse(ex, sw.toString(), 400) - } - - void stop() { - childServers.each { it.stop() } - httpServerWraper.stop() - } -} diff --git a/mockserver/src/main/groovy/pl/touk/mockserver/server/HttpServerWraper.groovy b/mockserver/src/main/groovy/pl/touk/mockserver/server/HttpServerWraper.groovy deleted file mode 100644 index 43692f6..0000000 --- a/mockserver/src/main/groovy/pl/touk/mockserver/server/HttpServerWraper.groovy +++ /dev/null @@ -1,58 +0,0 @@ -package pl.touk.mockserver.server - -import com.sun.net.httpserver.HttpHandler -import com.sun.net.httpserver.HttpServer -import groovy.transform.PackageScope -import groovy.util.logging.Slf4j - -import java.util.concurrent.Executors - -@Slf4j -@PackageScope -class HttpServerWraper { - private final HttpServer httpServer - final int port - - private List executors = [] - - HttpServerWraper(int port) { - this.port = port - InetSocketAddress addr = new InetSocketAddress(Inet4Address.getByName("0.0.0.0"), port) - httpServer = HttpServer.create(addr, 0) - httpServer.executor = Executors.newCachedThreadPool() - log.info("Http server starting on port $port...") - httpServer.start() - log.info('Http server is started') - } - - void createContext(String context, HttpHandler handler) { - httpServer.createContext(context, handler) - } - - void addMock(Mock mock) { - ContextExecutor executor = executors.find { it.path == mock.path } - if (executor) { - executor.addMock(mock) - } else { - executors << new ContextExecutor(this, mock) - } - log.info("Added mock ${mock.name}") - } - - void stop() { - executors.each { httpServer.removeContext(it.contextPath) } - httpServer.stop(0) - } - - List removeMock(String name) { - return executors.collect { it.removeMock(name) }.flatten() - } - - List peekMock(String name) { - return executors.collect { it.peekMock(name) }.flatten() - } - - List getMocks() { - return executors.collect { it.mocks }.flatten() - } -} diff --git a/mockserver/src/main/groovy/pl/touk/mockserver/server/Main.groovy b/mockserver/src/main/groovy/pl/touk/mockserver/server/Main.groovy deleted file mode 100644 index 759a11a..0000000 --- a/mockserver/src/main/groovy/pl/touk/mockserver/server/Main.groovy +++ /dev/null @@ -1,20 +0,0 @@ -package pl.touk.mockserver.server - -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() - - Runtime.runtime.addShutdownHook(new Thread({ - log.info('Http server is stopping...') - httpMockServer.stop() - log.info('Http server is stopped') - } as Runnable)) - - while (true) { - Thread.sleep(10000) - } - } -} diff --git a/mockserver/src/main/groovy/pl/touk/mockserver/server/Mock.groovy b/mockserver/src/main/groovy/pl/touk/mockserver/server/Mock.groovy deleted file mode 100644 index 61a8132..0000000 --- a/mockserver/src/main/groovy/pl/touk/mockserver/server/Mock.groovy +++ /dev/null @@ -1,107 +0,0 @@ -package pl.touk.mockserver.server - -import groovy.transform.EqualsAndHashCode -import groovy.transform.PackageScope -import groovy.util.logging.Slf4j - -import java.util.concurrent.CopyOnWriteArrayList - -@PackageScope -@EqualsAndHashCode(excludes = ["counter"]) -@Slf4j -class Mock implements Comparable { - final String name - final String path - final int port - String predicateClosureText = '{ _ -> true }' - String responseClosureText = '''{ _ -> '' }''' - String responseHeadersClosureText = '{ _ -> [:] }' - Closure predicate = toClosure(predicateClosureText) - Closure response = toClosure(responseClosureText) - Closure responseHeaders =toClosure(responseHeadersClosureText) - boolean soap = false - int statusCode = 200 - String method = 'POST' - int counter = 0 - final List history = new CopyOnWriteArrayList<>() - - Mock(String name, String path, int port) { - if (!(name)) { - throw new RuntimeException("Mock name must be given") - } - this.name = name - this.path = path - this.port = port - } - - boolean match(String method, MockRequest request) { - return this.method == method && predicate(request) - } - - MockResponse apply(MockRequest request) { - log.debug("Mock $name invoked") - ++counter - String responseText = response(request) - String response = soap ? wrapSoap(responseText) : responseText - Map headers = responseHeaders(request) - MockResponse mockResponse = new MockResponse(statusCode, response, headers) - history << new MockEvent(request, mockResponse) - return mockResponse - } - - private static String wrapSoap(String request) { - """ - - ${request} - """ - } - - void setPredicate(String predicate) { - if (predicate) { - this.predicateClosureText = predicate - this.predicate = toClosure(predicate) - } - } - - private Closure toClosure(String predicate) { - GroovyShell sh = new GroovyShell(this.class.classLoader); - return sh.evaluate(predicate) as Closure - } - - void setResponse(String response) { - if (response) { - this.responseClosureText = response - this.response = toClosure(response) - } - } - - void setSoap(String soap) { - if (soap) { - this.soap = Boolean.valueOf(soap) - } - } - - void setStatusCode(String statusCode) { - if (statusCode) { - this.statusCode = Integer.valueOf(statusCode) - } - } - - void setMethod(String method) { - if (method) { - this.method = method - } - } - - void setResponseHeaders(String responseHeaders) { - if (responseHeaders) { - this.responseHeadersClosureText = responseHeaders - this.responseHeaders = toClosure(responseHeaders) - } - } - - @Override - int compareTo(Mock o) { - return name.compareTo(o.name) - } -} diff --git a/mockserver/src/main/groovy/pl/touk/mockserver/server/Util.groovy b/mockserver/src/main/groovy/pl/touk/mockserver/server/Util.groovy deleted file mode 100644 index 93ab7a5..0000000 --- a/mockserver/src/main/groovy/pl/touk/mockserver/server/Util.groovy +++ /dev/null @@ -1,14 +0,0 @@ -package pl.touk.mockserver.server - -import com.sun.net.httpserver.HttpExchange - -class Util { - static void createResponse(HttpExchange ex, String response, int statusCode) { - byte[] responseBytes = response ? response.getBytes('UTF-8') : new byte[0] - ex.sendResponseHeaders(statusCode, responseBytes.length ?: -1) - if (response) { - ex.responseBody << responseBytes - ex.responseBody.close() - } - } -} diff --git a/mvnw b/mvnw new file mode 100755 index 0000000..8d937f4 --- /dev/null +++ b/mvnw @@ -0,0 +1,308 @@ +#!/bin/sh +# ---------------------------------------------------------------------------- +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# ---------------------------------------------------------------------------- + +# ---------------------------------------------------------------------------- +# Apache Maven Wrapper startup batch script, version 3.2.0 +# +# Required ENV vars: +# ------------------ +# JAVA_HOME - location of a JDK home dir +# +# Optional ENV vars +# ----------------- +# MAVEN_OPTS - parameters passed to the Java VM when running Maven +# e.g. to debug Maven itself, use +# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +# MAVEN_SKIP_RC - flag to disable loading of mavenrc files +# ---------------------------------------------------------------------------- + +if [ -z "$MAVEN_SKIP_RC" ] ; then + + if [ -f /usr/local/etc/mavenrc ] ; then + . /usr/local/etc/mavenrc + fi + + if [ -f /etc/mavenrc ] ; then + . /etc/mavenrc + fi + + if [ -f "$HOME/.mavenrc" ] ; then + . "$HOME/.mavenrc" + fi + +fi + +# OS specific support. $var _must_ be set to either true or false. +cygwin=false; +darwin=false; +mingw=false +case "$(uname)" in + CYGWIN*) cygwin=true ;; + MINGW*) mingw=true;; + Darwin*) darwin=true + # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home + # See https://developer.apple.com/library/mac/qa/qa1170/_index.html + if [ -z "$JAVA_HOME" ]; then + if [ -x "/usr/libexec/java_home" ]; then + JAVA_HOME="$(/usr/libexec/java_home)"; export JAVA_HOME + else + JAVA_HOME="/Library/Java/Home"; export JAVA_HOME + fi + fi + ;; +esac + +if [ -z "$JAVA_HOME" ] ; then + if [ -r /etc/gentoo-release ] ; then + JAVA_HOME=$(java-config --jre-home) + fi +fi + +# For Cygwin, ensure paths are in UNIX format before anything is touched +if $cygwin ; then + [ -n "$JAVA_HOME" ] && + JAVA_HOME=$(cygpath --unix "$JAVA_HOME") + [ -n "$CLASSPATH" ] && + CLASSPATH=$(cygpath --path --unix "$CLASSPATH") +fi + +# For Mingw, ensure paths are in UNIX format before anything is touched +if $mingw ; then + [ -n "$JAVA_HOME" ] && [ -d "$JAVA_HOME" ] && + JAVA_HOME="$(cd "$JAVA_HOME" || (echo "cannot cd into $JAVA_HOME."; exit 1); pwd)" +fi + +if [ -z "$JAVA_HOME" ]; then + javaExecutable="$(which javac)" + if [ -n "$javaExecutable" ] && ! [ "$(expr "\"$javaExecutable\"" : '\([^ ]*\)')" = "no" ]; then + # readlink(1) is not available as standard on Solaris 10. + readLink=$(which readlink) + if [ ! "$(expr "$readLink" : '\([^ ]*\)')" = "no" ]; then + if $darwin ; then + javaHome="$(dirname "\"$javaExecutable\"")" + javaExecutable="$(cd "\"$javaHome\"" && pwd -P)/javac" + else + javaExecutable="$(readlink -f "\"$javaExecutable\"")" + fi + javaHome="$(dirname "\"$javaExecutable\"")" + javaHome=$(expr "$javaHome" : '\(.*\)/bin') + JAVA_HOME="$javaHome" + export JAVA_HOME + fi + fi +fi + +if [ -z "$JAVACMD" ] ; then + if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + else + JAVACMD="$(\unset -f command 2>/dev/null; \command -v java)" + fi +fi + +if [ ! -x "$JAVACMD" ] ; then + echo "Error: JAVA_HOME is not defined correctly." >&2 + echo " We cannot execute $JAVACMD" >&2 + exit 1 +fi + +if [ -z "$JAVA_HOME" ] ; then + echo "Warning: JAVA_HOME environment variable is not set." +fi + +# traverses directory structure from process work directory to filesystem root +# first directory with .mvn subdirectory is considered project base directory +find_maven_basedir() { + if [ -z "$1" ] + then + echo "Path not specified to find_maven_basedir" + return 1 + fi + + basedir="$1" + wdir="$1" + while [ "$wdir" != '/' ] ; do + if [ -d "$wdir"/.mvn ] ; then + basedir=$wdir + break + fi + # workaround for JBEAP-8937 (on Solaris 10/Sparc) + if [ -d "${wdir}" ]; then + wdir=$(cd "$wdir/.." || exit 1; pwd) + fi + # end of workaround + done + printf '%s' "$(cd "$basedir" || exit 1; pwd)" +} + +# concatenates all lines of a file +concat_lines() { + if [ -f "$1" ]; then + # Remove \r in case we run on Windows within Git Bash + # and check out the repository with auto CRLF management + # enabled. Otherwise, we may read lines that are delimited with + # \r\n and produce $'-Xarg\r' rather than -Xarg due to word + # splitting rules. + tr -s '\r\n' ' ' < "$1" + fi +} + +log() { + if [ "$MVNW_VERBOSE" = true ]; then + printf '%s\n' "$1" + fi +} + +BASE_DIR=$(find_maven_basedir "$(dirname "$0")") +if [ -z "$BASE_DIR" ]; then + exit 1; +fi + +MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"}; export MAVEN_PROJECTBASEDIR +log "$MAVEN_PROJECTBASEDIR" + +########################################################################################## +# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +# This allows using the maven wrapper in projects that prohibit checking in binary data. +########################################################################################## +wrapperJarPath="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" +if [ -r "$wrapperJarPath" ]; then + log "Found $wrapperJarPath" +else + log "Couldn't find $wrapperJarPath, downloading it ..." + + if [ -n "$MVNW_REPOURL" ]; then + wrapperUrl="$MVNW_REPOURL/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + else + wrapperUrl="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + fi + while IFS="=" read -r key value; do + # Remove '\r' from value to allow usage on windows as IFS does not consider '\r' as a separator ( considers space, tab, new line ('\n'), and custom '=' ) + safeValue=$(echo "$value" | tr -d '\r') + case "$key" in (wrapperUrl) wrapperUrl="$safeValue"; break ;; + esac + done < "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties" + log "Downloading from: $wrapperUrl" + + if $cygwin; then + wrapperJarPath=$(cygpath --path --windows "$wrapperJarPath") + fi + + if command -v wget > /dev/null; then + log "Found wget ... using wget" + [ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--quiet" + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + wget $QUIET "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" + else + wget $QUIET --http-user="$MVNW_USERNAME" --http-password="$MVNW_PASSWORD" "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" + fi + elif command -v curl > /dev/null; then + log "Found curl ... using curl" + [ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--silent" + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + curl $QUIET -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath" + else + curl $QUIET --user "$MVNW_USERNAME:$MVNW_PASSWORD" -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath" + fi + else + log "Falling back to using Java to download" + javaSource="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.java" + javaClass="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.class" + # For Cygwin, switch paths to Windows format before running javac + if $cygwin; then + javaSource=$(cygpath --path --windows "$javaSource") + javaClass=$(cygpath --path --windows "$javaClass") + fi + if [ -e "$javaSource" ]; then + if [ ! -e "$javaClass" ]; then + log " - Compiling MavenWrapperDownloader.java ..." + ("$JAVA_HOME/bin/javac" "$javaSource") + fi + if [ -e "$javaClass" ]; then + log " - Running MavenWrapperDownloader.java ..." + ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$wrapperUrl" "$wrapperJarPath") || rm -f "$wrapperJarPath" + fi + fi + fi +fi +########################################################################################## +# End of extension +########################################################################################## + +# If specified, validate the SHA-256 sum of the Maven wrapper jar file +wrapperSha256Sum="" +while IFS="=" read -r key value; do + case "$key" in (wrapperSha256Sum) wrapperSha256Sum=$value; break ;; + esac +done < "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties" +if [ -n "$wrapperSha256Sum" ]; then + wrapperSha256Result=false + if command -v sha256sum > /dev/null; then + if echo "$wrapperSha256Sum $wrapperJarPath" | sha256sum -c > /dev/null 2>&1; then + wrapperSha256Result=true + fi + elif command -v shasum > /dev/null; then + if echo "$wrapperSha256Sum $wrapperJarPath" | shasum -a 256 -c > /dev/null 2>&1; then + wrapperSha256Result=true + fi + else + echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." + echo "Please install either command, or disable validation by removing 'wrapperSha256Sum' from your maven-wrapper.properties." + exit 1 + fi + if [ $wrapperSha256Result = false ]; then + echo "Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised." >&2 + echo "Investigate or delete $wrapperJarPath to attempt a clean download." >&2 + echo "If you updated your Maven version, you need to update the specified wrapperSha256Sum property." >&2 + exit 1 + fi +fi + +MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" + +# For Cygwin, switch paths to Windows format before running java +if $cygwin; then + [ -n "$JAVA_HOME" ] && + JAVA_HOME=$(cygpath --path --windows "$JAVA_HOME") + [ -n "$CLASSPATH" ] && + CLASSPATH=$(cygpath --path --windows "$CLASSPATH") + [ -n "$MAVEN_PROJECTBASEDIR" ] && + MAVEN_PROJECTBASEDIR=$(cygpath --path --windows "$MAVEN_PROJECTBASEDIR") +fi + +# Provide a "standardized" way to retrieve the CLI args that will +# work with both Windows and non-Windows executions. +MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $*" +export MAVEN_CMD_LINE_ARGS + +WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +# shellcheck disable=SC2086 # safe args +exec "$JAVACMD" \ + $MAVEN_OPTS \ + $MAVEN_DEBUG_OPTS \ + -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ + "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ + ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" diff --git a/mvnw.cmd b/mvnw.cmd new file mode 100644 index 0000000..f80fbad --- /dev/null +++ b/mvnw.cmd @@ -0,0 +1,205 @@ +@REM ---------------------------------------------------------------------------- +@REM Licensed to the Apache Software Foundation (ASF) under one +@REM or more contributor license agreements. See the NOTICE file +@REM distributed with this work for additional information +@REM regarding copyright ownership. The ASF licenses this file +@REM to you under the Apache License, Version 2.0 (the +@REM "License"); you may not use this file except in compliance +@REM with the License. You may obtain a copy of the License at +@REM +@REM http://www.apache.org/licenses/LICENSE-2.0 +@REM +@REM Unless required by applicable law or agreed to in writing, +@REM software distributed under the License is distributed on an +@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +@REM KIND, either express or implied. See the License for the +@REM specific language governing permissions and limitations +@REM under the License. +@REM ---------------------------------------------------------------------------- + +@REM ---------------------------------------------------------------------------- +@REM Apache Maven Wrapper startup batch script, version 3.2.0 +@REM +@REM Required ENV vars: +@REM JAVA_HOME - location of a JDK home dir +@REM +@REM Optional ENV vars +@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands +@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending +@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven +@REM e.g. to debug Maven itself, use +@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files +@REM ---------------------------------------------------------------------------- + +@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' +@echo off +@REM set title of command window +title %0 +@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on' +@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% + +@REM set %HOME% to equivalent of $HOME +if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") + +@REM Execute a user defined script before this one +if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre +@REM check for pre script, once with legacy .bat ending and once with .cmd ending +if exist "%USERPROFILE%\mavenrc_pre.bat" call "%USERPROFILE%\mavenrc_pre.bat" %* +if exist "%USERPROFILE%\mavenrc_pre.cmd" call "%USERPROFILE%\mavenrc_pre.cmd" %* +:skipRcPre + +@setlocal + +set ERROR_CODE=0 + +@REM To isolate internal variables from possible post scripts, we use another setlocal +@setlocal + +@REM ==== START VALIDATION ==== +if not "%JAVA_HOME%" == "" goto OkJHome + +echo. +echo Error: JAVA_HOME not found in your environment. >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +:OkJHome +if exist "%JAVA_HOME%\bin\java.exe" goto init + +echo. +echo Error: JAVA_HOME is set to an invalid directory. >&2 +echo JAVA_HOME = "%JAVA_HOME%" >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +@REM ==== END VALIDATION ==== + +:init + +@REM Find the project base dir, i.e. the directory that contains the folder ".mvn". +@REM Fallback to current working directory if not found. + +set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% +IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir + +set EXEC_DIR=%CD% +set WDIR=%EXEC_DIR% +:findBaseDir +IF EXIST "%WDIR%"\.mvn goto baseDirFound +cd .. +IF "%WDIR%"=="%CD%" goto baseDirNotFound +set WDIR=%CD% +goto findBaseDir + +:baseDirFound +set MAVEN_PROJECTBASEDIR=%WDIR% +cd "%EXEC_DIR%" +goto endDetectBaseDir + +:baseDirNotFound +set MAVEN_PROJECTBASEDIR=%EXEC_DIR% +cd "%EXEC_DIR%" + +:endDetectBaseDir + +IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig + +@setlocal EnableExtensions EnableDelayedExpansion +for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a +@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% + +:endReadAdditionalConfig + +SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" +set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" +set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +set WRAPPER_URL="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + +FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( + IF "%%A"=="wrapperUrl" SET WRAPPER_URL=%%B +) + +@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +@REM This allows using the maven wrapper in projects that prohibit checking in binary data. +if exist %WRAPPER_JAR% ( + if "%MVNW_VERBOSE%" == "true" ( + echo Found %WRAPPER_JAR% + ) +) else ( + if not "%MVNW_REPOURL%" == "" ( + SET WRAPPER_URL="%MVNW_REPOURL%/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + ) + if "%MVNW_VERBOSE%" == "true" ( + echo Couldn't find %WRAPPER_JAR%, downloading it ... + echo Downloading from: %WRAPPER_URL% + ) + + powershell -Command "&{"^ + "$webclient = new-object System.Net.WebClient;"^ + "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ + "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ + "}"^ + "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%WRAPPER_URL%', '%WRAPPER_JAR%')"^ + "}" + if "%MVNW_VERBOSE%" == "true" ( + echo Finished downloading %WRAPPER_JAR% + ) +) +@REM End of extension + +@REM If specified, validate the SHA-256 sum of the Maven wrapper jar file +SET WRAPPER_SHA_256_SUM="" +FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( + IF "%%A"=="wrapperSha256Sum" SET WRAPPER_SHA_256_SUM=%%B +) +IF NOT %WRAPPER_SHA_256_SUM%=="" ( + powershell -Command "&{"^ + "$hash = (Get-FileHash \"%WRAPPER_JAR%\" -Algorithm SHA256).Hash.ToLower();"^ + "If('%WRAPPER_SHA_256_SUM%' -ne $hash){"^ + " Write-Output 'Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised.';"^ + " Write-Output 'Investigate or delete %WRAPPER_JAR% to attempt a clean download.';"^ + " Write-Output 'If you updated your Maven version, you need to update the specified wrapperSha256Sum property.';"^ + " exit 1;"^ + "}"^ + "}" + if ERRORLEVEL 1 goto error +) + +@REM Provide a "standardized" way to retrieve the CLI args that will +@REM work with both Windows and non-Windows executions. +set MAVEN_CMD_LINE_ARGS=%* + +%MAVEN_JAVA_EXE% ^ + %JVM_CONFIG_MAVEN_PROPS% ^ + %MAVEN_OPTS% ^ + %MAVEN_DEBUG_OPTS% ^ + -classpath %WRAPPER_JAR% ^ + "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" ^ + %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* +if ERRORLEVEL 1 goto error +goto end + +:error +set ERROR_CODE=1 + +:end +@endlocal & set ERROR_CODE=%ERROR_CODE% + +if not "%MAVEN_SKIP_RC%"=="" goto skipRcPost +@REM check for post script, once with legacy .bat ending and once with .cmd ending +if exist "%USERPROFILE%\mavenrc_post.bat" call "%USERPROFILE%\mavenrc_post.bat" +if exist "%USERPROFILE%\mavenrc_post.cmd" call "%USERPROFILE%\mavenrc_post.cmd" +:skipRcPost + +@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' +if "%MAVEN_BATCH_PAUSE%"=="on" pause + +if "%MAVEN_TERMINATE_CMD%"=="on" exit %ERROR_CODE% + +cmd /C exit /B %ERROR_CODE% diff --git a/performance-tests/pom.xml b/performance-tests/pom.xml new file mode 100644 index 0000000..e73db5c --- /dev/null +++ b/performance-tests/pom.xml @@ -0,0 +1,70 @@ + + + 4.0.0 + + + eu.ztsh.mockserver + http-mock-server + 3.0.0-SNAPSHOT + + + mockserver-performance-tests + + + + eu.ztsh.mockserver + mockserver + + + eu.ztsh.mockserver + mockserver-client + + + + org.openjdk.jmh + jmh-core + + + org.openjdk.jmh + jmh-generator-annprocess + + + + 1.4.0 + + + + + performance-test + + + + org.codehaus.mojo + exec-maven-plugin + ${exec-maven-plugin.version} + + + run-benchmarks + integration-test + + exec + + + test + java + + -classpath + + org.openjdk.jmh.Main + .* + + + + + + + + + + + \ No newline at end of file diff --git a/performance-tests/src/test/java/eu/ztsh/mockserver/client/MockserverTest.java b/performance-tests/src/test/java/eu/ztsh/mockserver/client/MockserverTest.java new file mode 100644 index 0000000..2a3b943 --- /dev/null +++ b/performance-tests/src/test/java/eu/ztsh/mockserver/client/MockserverTest.java @@ -0,0 +1,82 @@ +package eu.ztsh.mockserver.client; + +import org.apache.http.client.HttpClient; +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.HttpClients; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.TearDown; +import org.openjdk.jmh.annotations.Warmup; +import org.openjdk.jmh.infra.Blackhole; +import org.openjdk.jmh.infra.ThreadParams; +import eu.ztsh.mockserver.api.request.AddMock; +import eu.ztsh.mockserver.server.HttpMockServer; + +import java.io.IOException; +import java.util.concurrent.TimeUnit; + +@State(Scope.Benchmark) +@OutputTimeUnit(TimeUnit.SECONDS) +public class MockserverTest { + HttpMockServer httpMockServer; + + int initialPort = 9000; + + @Setup + public void prepareMockServer() { + httpMockServer = new HttpMockServer(9999); + } + + @TearDown + public void stopMockServer() { + httpMockServer.stop(); + } + + @State(Scope.Thread) + public static class TestState { + RemoteMockServer remoteMockServer; + HttpClient httpClient; + int current; + + @Setup + public void prepareMockServer(ThreadParams params) { + remoteMockServer = new RemoteMockServer("localhost", 9999); + httpClient = HttpClients.createDefault(); + current = params.getThreadIndex(); + } + } + + @Benchmark + @Measurement(iterations = 20) + @BenchmarkMode({Mode.AverageTime, Mode.Throughput, Mode.SampleTime}) + @Warmup(iterations = 10) + public void shouldHandleManyRequestsSimultaneously(TestState testState, Blackhole bh) throws IOException { + int current = testState.current; + int endpointNumber = current % 10; + int port = initialPort + (current % 7); + AddMock addMock = new AddMock(); + addMock.setName("testRest" + current); + addMock.setPath("testEndpoint" + endpointNumber); + addMock.setPort(port); + addMock.setPredicate("{req -> req.xml.name() == 'request" + current + "' }"); + addMock.setResponse("{req -> ''}"); + testState.remoteMockServer.addMock(addMock); + HttpPost restPost = new HttpPost("http://localhost:" + port + "/testEndpoint" + endpointNumber); + restPost.setEntity(new StringEntity("", ContentType.create("text/xml", "UTF-8"))); + CloseableHttpResponse response = (CloseableHttpResponse) testState.httpClient.execute(restPost); + String stringResponse = Util.extractStringResponse(response); + testState.remoteMockServer.removeMock("testRest" + current, true); + assert stringResponse.equals(""); + bh.consume(stringResponse); + } + +} diff --git a/performance-tests/src/test/resources/logback.xml b/performance-tests/src/test/resources/logback.xml new file mode 100644 index 0000000..b37e533 --- /dev/null +++ b/performance-tests/src/test/resources/logback.xml @@ -0,0 +1,15 @@ + + + + %highlight(%d{HH:mm:ss.SSS} [%thread] %-5level %logger - %msg%n) + + + + + %msg%n + + + + + + \ No newline at end of file diff --git a/pom.xml b/pom.xml index 48ad9ac..a981985 100644 --- a/pom.xml +++ b/pom.xml @@ -1,53 +1,90 @@ - + 4.0.0 - pl.touk.mockserver + eu.ztsh.mockserver http-mock-server pom - 1.1.0 + 3.0.0-SNAPSHOT + mockserver-client mockserver mockserver-tests + mockserver-api + performance-tests + 11 + ${java.version} + ${java.version} UTF-8 - UTF-8 - 3.1 - 2.4.1 - 4.3.5 - 1.0-groovy-2.4 - 3.3.2 - 1.7.7 - 1.0.13 - - - scm:git:ssh://gerrit.touk.pl:29418/integracja/http-mock-server - scm:git:ssh://gerrit.touk.pl:29418/integracja/http-mock-server - HEAD - + UTF-8 + 4.0.12 + 4.5.13 + 2.2-groovy-4.0 + 3.3.2 + 1.7.30 + 1.3.12 + 1.18.26 + 4.0.4 + + true + 1.37 + 3.0.2 + 3.1.0 + - org.codehaus.groovy - groovy-all + eu.ztsh.mockserver + mockserver-api + ${project.version} + + + eu.ztsh.mockserver + mockserver + ${project.version} + + + eu.ztsh.mockserver + mockserver-client + ${project.version} + + + + org.glassfish.jaxb + jaxb-bom + ${jaxb.version} + pom + import + + + + org.apache.groovy + groovy ${groovy.version} + + org.apache.groovy + groovy-json + ${groovy.version} + + + org.apache.groovy + groovy-xml + ${groovy.version} + + org.apache.httpcomponents httpclient ${httpclient.version} - - org.spockframework - spock-core - ${spock-core.version} - test - org.apache.commons commons-lang3 @@ -58,44 +95,66 @@ slf4j-api ${slf4j-api.version} + + ch.qos.logback + logback-core + ${logback.version} + ch.qos.logback logback-classic - ${logback-classic.version} + ${logback.version} + + + org.projectlombok + lombok + ${lombok.version} + + + + org.spockframework + spock-core + ${spock-core.version} + test + + + org.openjdk.jmh + jmh-core + ${jmh.version} + test + + + org.openjdk.jmh + jmh-generator-annprocess + ${jmh.version} + test - clean install - - - org.codehaus.gmavenplus - gmavenplus-plugin - 1.4 - - - - compile - testCompile - - - - - + + + + org.codehaus.mojo + jaxb2-maven-plugin + ${jaxb2-maven-plugin.version} + + + org.codehaus.gmavenplus + gmavenplus-plugin + ${gmavenplus-plugin.version} + + + + compile + compileTests + + + + + + - - - touk.nexus.release - TouK Virtual Repository - http://nexus.touk.pl/nexus/content/repositories/releases - - - touk.nexus.snapshots - TouK Virtual Repository - http://nexus.touk.pl/nexus/content/repositories/snapshots - - -