diff --git a/mockserver-api/src/main/xsd/pl/touk/mockserver/api/request.xsd b/mockserver-api/src/main/xsd/pl/touk/mockserver/api/request.xsd index fb4afca..10c7190 100644 --- a/mockserver-api/src/main/xsd/pl/touk/mockserver/api/request.xsd +++ b/mockserver-api/src/main/xsd/pl/touk/mockserver/api/request.xsd @@ -21,6 +21,7 @@ + diff --git a/mockserver-api/src/main/xsd/pl/touk/mockserver/api/response.xsd b/mockserver-api/src/main/xsd/pl/touk/mockserver/api/response.xsd index 21ffd9b..9e630dc 100644 --- a/mockserver-api/src/main/xsd/pl/touk/mockserver/api/response.xsd +++ b/mockserver-api/src/main/xsd/pl/touk/mockserver/api/response.xsd @@ -108,6 +108,7 @@ + diff --git a/mockserver-client/src/main/groovy/pl/touk/mockserver/client/InvalidMockRequestSchema.groovy b/mockserver-client/src/main/groovy/pl/touk/mockserver/client/InvalidMockRequestSchema.groovy new file mode 100644 index 0000000..ff3d401 --- /dev/null +++ b/mockserver-client/src/main/groovy/pl/touk/mockserver/client/InvalidMockRequestSchema.groovy @@ -0,0 +1,9 @@ +package pl.touk.mockserver.client + +import groovy.transform.CompileStatic +import groovy.transform.TypeChecked + +@CompileStatic +@TypeChecked +class InvalidMockRequestSchema extends RuntimeException { +} 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 index 966625f..3a427d6 100644 --- a/mockserver-client/src/main/groovy/pl/touk/mockserver/client/Util.groovy +++ b/mockserver-client/src/main/groovy/pl/touk/mockserver/client/Util.groovy @@ -42,6 +42,9 @@ class Util { if (message == 'mock not registered') { throw new MockDoesNotExist() } + if (message == 'mock request schema is invalid schema') { + throw new InvalidMockRequestSchema() + } throw new InvalidMockDefinition(message) } diff --git a/mockserver-tests/src/test/groovy/pl/touk/mockserver/tests/MockServerIntegrationTest.groovy b/mockserver-tests/src/test/groovy/pl/touk/mockserver/tests/MockServerIntegrationTest.groovy index 3dd7ec4..fa61e4a 100644 --- a/mockserver-tests/src/test/groovy/pl/touk/mockserver/tests/MockServerIntegrationTest.groovy +++ b/mockserver-tests/src/test/groovy/pl/touk/mockserver/tests/MockServerIntegrationTest.groovy @@ -7,11 +7,10 @@ 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.api.request.AddMock import pl.touk.mockserver.api.common.Method +import pl.touk.mockserver.api.request.AddMock import pl.touk.mockserver.api.response.MockEventReport import pl.touk.mockserver.api.response.MockReport -import pl.touk.mockserver.api.response.Parameter import pl.touk.mockserver.client.* import pl.touk.mockserver.server.HttpMockServer import spock.lang.Shared @@ -146,6 +145,20 @@ 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 AddMock( @@ -634,21 +647,22 @@ class MockServerIntegrationTest extends Specification { remoteMockServer.addMock(new AddMock( name: 'testRest', path: 'testEndpoint', - port: 9999 + port: 9999, + schema: 'schema2.xsd' )) remoteMockServer.removeMock('testRest5') 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]) + 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]) } - private void assertMockReport( MockReport mockReport, Map props) { + private static void assertMockReport(MockReport mockReport, Map props) { props.each { assert mockReport."${it.key}" == it.value } @@ -820,9 +834,9 @@ class MockServerIntegrationTest extends Specification { mockEvents2.size() == 1 mockEvents2[0].request.text == '' !mockEvents2[0].request.headers?.headers?.empty - mockEvents2[0].request.queryParams.queryParams.find{it.name == 'id'}?.value == '123' + 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.headers.headers.find { it.name == 'aaa' }?.value == '15' mockEvents2[0].response.text == '' mockEvents2[0].response.statusCode == 202 } @@ -881,4 +895,100 @@ class MockServerIntegrationTest extends Specification { '''{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 + } } 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/src/main/groovy/pl/touk/mockserver/server/HttpMockServer.groovy b/mockserver/src/main/groovy/pl/touk/mockserver/server/HttpMockServer.groovy index 0b601ff..8eaab6d 100644 --- a/mockserver/src/main/groovy/pl/touk/mockserver/server/HttpMockServer.groovy +++ b/mockserver/src/main/groovy/pl/touk/mockserver/server/HttpMockServer.groovy @@ -64,7 +64,8 @@ class HttpMockServer { responseHeaders: it.responseHeadersClosureText, soap: it.soap, method: it.method, - statusCode: it.statusCode as int + statusCode: it.statusCode as int, + schema: it.schema ) } ) @@ -95,6 +96,7 @@ class HttpMockServer { mock.statusCode = request.statusCode mock.method = request.method mock.responseHeaders = request.responseHeaders + mock.schema = request.schema return mock } @@ -135,12 +137,12 @@ class HttpMockServer { 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) + 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 { + headers: new MockResponseReport.Headers(headers: it.response.headers.collect { new Parameter(name: it.key, value: it.value) }) ) @@ -162,6 +164,7 @@ class HttpMockServer { } private static void createErrorResponse(HttpExchange ex, Exception e) { + log.warn('Exception occured', e) createResponse(ex, new ExceptionOccured(value: e.message), 400) } diff --git a/mockserver/src/main/groovy/pl/touk/mockserver/server/Mock.groovy b/mockserver/src/main/groovy/pl/touk/mockserver/server/Mock.groovy index 32bad68..15b538f 100644 --- a/mockserver/src/main/groovy/pl/touk/mockserver/server/Mock.groovy +++ b/mockserver/src/main/groovy/pl/touk/mockserver/server/Mock.groovy @@ -5,6 +5,10 @@ import groovy.transform.PackageScope import groovy.util.logging.Slf4j import pl.touk.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 @@ -25,6 +29,8 @@ class Mock implements Comparable { Method method = Method.POST int counter = 0 final List history = new CopyOnWriteArrayList<>() + String schema + private Validator validator Mock(String name, String path, int port) { if (!(name)) { @@ -41,6 +47,20 @@ class Mock implements Comparable { 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, [:]) + history << new MockEvent(request, response) + return response + } + } ++counter String responseText = response(request) String response = soap ? wrapSoap(responseText) : responseText @@ -106,4 +126,17 @@ class Mock implements Comparable { 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(new File(this.class.getResource("/$schema").path)) + .newValidator() + } catch (Exception e) { + throw new RuntimeException('mock request schema is invalid schema', e) + } + } + } } diff --git a/mockserver/src/main/groovy/pl/touk/mockserver/server/MockRequest.groovy b/mockserver/src/main/groovy/pl/touk/mockserver/server/MockRequest.groovy index c4ebe01..f8dc561 100644 --- a/mockserver/src/main/groovy/pl/touk/mockserver/server/MockRequest.groovy +++ b/mockserver/src/main/groovy/pl/touk/mockserver/server/MockRequest.groovy @@ -4,6 +4,7 @@ import com.sun.net.httpserver.Headers import groovy.json.JsonSlurper import groovy.transform.PackageScope import groovy.util.slurpersupport.GPathResult +import groovy.xml.XmlUtil @PackageScope class MockRequest { @@ -69,4 +70,7 @@ class MockRequest { } as Map } + String getTextWithoutSoap() { + return XmlUtil.serialize(soap) + } }