Add request validation against xsd
Change-Id: Iadf618e50d01dd92bf9eed5b8ed98138b56c20c9
This commit is contained in:
parent
aabc9d75f2
commit
9a14f9bfab
10 changed files with 205 additions and 10 deletions
|
@ -21,6 +21,7 @@
|
||||||
<xs:element name="statusCode" type="xs:int" minOccurs="0"/>
|
<xs:element name="statusCode" type="xs:int" minOccurs="0"/>
|
||||||
<xs:element name="method" type="common:method" minOccurs="0"/>
|
<xs:element name="method" type="common:method" minOccurs="0"/>
|
||||||
<xs:element name="responseHeaders" type="xs:string" minOccurs="0"/>
|
<xs:element name="responseHeaders" type="xs:string" minOccurs="0"/>
|
||||||
|
<xs:element name="schema" type="xs:string" minOccurs="0"/>
|
||||||
</xs:sequence>
|
</xs:sequence>
|
||||||
</xs:extension>
|
</xs:extension>
|
||||||
</xs:complexContent>
|
</xs:complexContent>
|
||||||
|
|
|
@ -108,6 +108,7 @@
|
||||||
<xs:element name="soap" type="xs:boolean"/>
|
<xs:element name="soap" type="xs:boolean"/>
|
||||||
<xs:element name="method" type="common:method"/>
|
<xs:element name="method" type="common:method"/>
|
||||||
<xs:element name="statusCode" type="xs:int"/>
|
<xs:element name="statusCode" type="xs:int"/>
|
||||||
|
<xs:element name="schema" type="xs:string" minOccurs="0"/>
|
||||||
</xs:sequence>
|
</xs:sequence>
|
||||||
</xs:complexType>
|
</xs:complexType>
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,9 @@
|
||||||
|
package pl.touk.mockserver.client
|
||||||
|
|
||||||
|
import groovy.transform.CompileStatic
|
||||||
|
import groovy.transform.TypeChecked
|
||||||
|
|
||||||
|
@CompileStatic
|
||||||
|
@TypeChecked
|
||||||
|
class InvalidMockRequestSchema extends RuntimeException {
|
||||||
|
}
|
|
@ -42,6 +42,9 @@ class Util {
|
||||||
if (message == 'mock not registered') {
|
if (message == 'mock not registered') {
|
||||||
throw new MockDoesNotExist()
|
throw new MockDoesNotExist()
|
||||||
}
|
}
|
||||||
|
if (message == 'mock request schema is invalid schema') {
|
||||||
|
throw new InvalidMockRequestSchema()
|
||||||
|
}
|
||||||
throw new InvalidMockDefinition(message)
|
throw new InvalidMockDefinition(message)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -7,11 +7,10 @@ import org.apache.http.entity.StringEntity
|
||||||
import org.apache.http.impl.client.CloseableHttpClient
|
import org.apache.http.impl.client.CloseableHttpClient
|
||||||
import org.apache.http.impl.client.HttpClients
|
import org.apache.http.impl.client.HttpClients
|
||||||
import org.apache.http.util.EntityUtils
|
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.common.Method
|
||||||
|
import pl.touk.mockserver.api.request.AddMock
|
||||||
import pl.touk.mockserver.api.response.MockEventReport
|
import pl.touk.mockserver.api.response.MockEventReport
|
||||||
import pl.touk.mockserver.api.response.MockReport
|
import pl.touk.mockserver.api.response.MockReport
|
||||||
import pl.touk.mockserver.api.response.Parameter
|
|
||||||
import pl.touk.mockserver.client.*
|
import pl.touk.mockserver.client.*
|
||||||
import pl.touk.mockserver.server.HttpMockServer
|
import pl.touk.mockserver.server.HttpMockServer
|
||||||
import spock.lang.Shared
|
import spock.lang.Shared
|
||||||
|
@ -146,6 +145,20 @@ class MockServerIntegrationTest extends Specification {
|
||||||
thrown(MockAlreadyExists)
|
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 -> "<goodResponse/>"}''',
|
||||||
|
soap: false,
|
||||||
|
schema: 'ble.xsd'
|
||||||
|
))
|
||||||
|
then:
|
||||||
|
thrown(InvalidMockRequestSchema)
|
||||||
|
}
|
||||||
|
|
||||||
def "should not add mock with empty name"() {
|
def "should not add mock with empty name"() {
|
||||||
when:
|
when:
|
||||||
remoteMockServer.addMock(new AddMock(
|
remoteMockServer.addMock(new AddMock(
|
||||||
|
@ -634,21 +647,22 @@ class MockServerIntegrationTest extends Specification {
|
||||||
remoteMockServer.addMock(new AddMock(
|
remoteMockServer.addMock(new AddMock(
|
||||||
name: 'testRest',
|
name: 'testRest',
|
||||||
path: 'testEndpoint',
|
path: 'testEndpoint',
|
||||||
port: 9999
|
port: 9999,
|
||||||
|
schema: 'schema2.xsd'
|
||||||
))
|
))
|
||||||
remoteMockServer.removeMock('testRest5')
|
remoteMockServer.removeMock('testRest5')
|
||||||
when:
|
when:
|
||||||
List<MockReport> mockReport = remoteMockServer.listMocks()
|
List<MockReport> mockReport = remoteMockServer.listMocks()
|
||||||
then:
|
then:
|
||||||
mockReport.size() == 5
|
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 -> '<response/>' }''', responseHeaders: '{ _ -> [a: "b"] }', soap: false, statusCode: 200, method: Method.POST])
|
assertMockReport(mockReport[1], [name: 'testRest2', path: 'testEndpoint', port: 9998, predicate: '''{ req -> req.xml.name() == 'request1'}''', response: '''{ req -> '<response/>' }''', responseHeaders: '{ _ -> [a: "b"] }', soap: false, statusCode: 200, method: Method.POST])
|
||||||
assertMockReport(mockReport[2], [name: 'testRest3', path: 'testEndpoint2', port: 9999, predicate: '{ _ -> true }', response: '''{ _ -> '' }''', responseHeaders: '{ _ -> [:] }', soap: false, statusCode: 200, method: Method.POST])
|
assertMockReport(mockReport[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[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])
|
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<String, Object> props) {
|
private static void assertMockReport(MockReport mockReport, Map<String, Object> props) {
|
||||||
props.each {
|
props.each {
|
||||||
assert mockReport."${it.key}" == it.value
|
assert mockReport."${it.key}" == it.value
|
||||||
}
|
}
|
||||||
|
@ -881,4 +895,100 @@ class MockServerIntegrationTest extends Specification {
|
||||||
'''{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 -> '<goodResponseRest/>'}''',
|
||||||
|
soap: false
|
||||||
|
))
|
||||||
|
when:
|
||||||
|
HttpPost restPost = new HttpPost('http://localhost:9999/testEndpoint')
|
||||||
|
restPost.entity = new StringEntity('<request xmlns="http://mockserver/test1"><id>15</id><value>unknown</value></request>', 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('<request xmlns="http://mockserver/test1"><id>15</id><value>test</value></request>', 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 -> '<goodResponse/>'}''',
|
||||||
|
soap: true
|
||||||
|
))
|
||||||
|
when:
|
||||||
|
HttpPost restPost = new HttpPost('http://localhost:9999/testEndpoint')
|
||||||
|
restPost.entity = new StringEntity(Util.soap('<request xmlns="http://mockserver/test1"><id>15</id><value>unknown</value></request>'), 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('<request xmlns="http://mockserver/test1"><id>15</id><value>test</value></request>'), 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 -> '<goodResponse/>'}''',
|
||||||
|
soap: true
|
||||||
|
))
|
||||||
|
when:
|
||||||
|
HttpPost restPost = new HttpPost('http://localhost:9999/testEndpoint')
|
||||||
|
restPost.entity = new StringEntity('''<soap-env:Envelope xmlns:soap-env='http://schemas.xmlsoap.org/soap/envelope/'
|
||||||
|
xmlns:wsa="http://schemas.xmlsoap.org/ws/2004/08/addressing"
|
||||||
|
xmlns:req="http://mockserver/test1">
|
||||||
|
<soap-env:Body>
|
||||||
|
<req:request><req:id>15</req:id><req:value>unknown</req:value></req:request>
|
||||||
|
</soap-env:Body>
|
||||||
|
</soap-env:Envelope>''', 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('''<soap-env:Envelope xmlns:soap-env='http://schemas.xmlsoap.org/soap/envelope/'
|
||||||
|
xmlns:wsa="http://schemas.xmlsoap.org/ws/2004/08/addressing"
|
||||||
|
xmlns:req="http://mockserver/test1">
|
||||||
|
<soap-env:Body>
|
||||||
|
<req:request><req:id>15</req:id><req:value>test</req:value></req:request>
|
||||||
|
</soap-env:Body>
|
||||||
|
</soap-env:Envelope>''', 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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
19
mockserver-tests/src/test/resources/schema1.xsd
Normal file
19
mockserver-tests/src/test/resources/schema1.xsd
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
<xs:schema elementFormDefault="qualified"
|
||||||
|
version="1.0"
|
||||||
|
targetNamespace="http://mockserver/test1"
|
||||||
|
xmlns:tns="http://mockserver/test1"
|
||||||
|
xmlns:xs="http://www.w3.org/2001/XMLSchema"
|
||||||
|
xmlns:test2="http://mockserver/test2">
|
||||||
|
|
||||||
|
<xs:import namespace="http://mockserver/test2" schemaLocation="schema2.xsd"/>
|
||||||
|
|
||||||
|
<xs:element name="request" type="tns:Request"/>
|
||||||
|
|
||||||
|
<xs:complexType name="Request">
|
||||||
|
<xs:sequence>
|
||||||
|
<xs:element name="id" type="xs:int"/>
|
||||||
|
<xs:element name="value" type="test2:Value"/>
|
||||||
|
</xs:sequence>
|
||||||
|
</xs:complexType>
|
||||||
|
</xs:schema>
|
||||||
|
|
12
mockserver-tests/src/test/resources/schema2.xsd
Normal file
12
mockserver-tests/src/test/resources/schema2.xsd
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||||
|
<xs:schema version="1.0" targetNamespace="http://mockserver/test2" xmlns:xs="http://www.w3.org/2001/XMLSchema">
|
||||||
|
|
||||||
|
<xs:simpleType name="Value">
|
||||||
|
<xs:restriction base="xs:string">
|
||||||
|
<xs:enumeration value="test"/>
|
||||||
|
<xs:enumeration value="prod"/>
|
||||||
|
<xs:enumeration value="preprod"/>
|
||||||
|
</xs:restriction>
|
||||||
|
</xs:simpleType>
|
||||||
|
</xs:schema>
|
||||||
|
|
|
@ -64,7 +64,8 @@ class HttpMockServer {
|
||||||
responseHeaders: it.responseHeadersClosureText,
|
responseHeaders: it.responseHeadersClosureText,
|
||||||
soap: it.soap,
|
soap: it.soap,
|
||||||
method: it.method,
|
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.statusCode = request.statusCode
|
||||||
mock.method = request.method
|
mock.method = request.method
|
||||||
mock.responseHeaders = request.responseHeaders
|
mock.responseHeaders = request.responseHeaders
|
||||||
|
mock.schema = request.schema
|
||||||
return mock
|
return mock
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -162,6 +164,7 @@ class HttpMockServer {
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void createErrorResponse(HttpExchange ex, Exception e) {
|
private static void createErrorResponse(HttpExchange ex, Exception e) {
|
||||||
|
log.warn('Exception occured', e)
|
||||||
createResponse(ex, new ExceptionOccured(value: e.message), 400)
|
createResponse(ex, new ExceptionOccured(value: e.message), 400)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -5,6 +5,10 @@ import groovy.transform.PackageScope
|
||||||
import groovy.util.logging.Slf4j
|
import groovy.util.logging.Slf4j
|
||||||
import pl.touk.mockserver.api.common.Method
|
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
|
import java.util.concurrent.CopyOnWriteArrayList
|
||||||
|
|
||||||
@PackageScope
|
@PackageScope
|
||||||
|
@ -25,6 +29,8 @@ class Mock implements Comparable<Mock> {
|
||||||
Method method = Method.POST
|
Method method = Method.POST
|
||||||
int counter = 0
|
int counter = 0
|
||||||
final List<MockEvent> history = new CopyOnWriteArrayList<>()
|
final List<MockEvent> history = new CopyOnWriteArrayList<>()
|
||||||
|
String schema
|
||||||
|
private Validator validator
|
||||||
|
|
||||||
Mock(String name, String path, int port) {
|
Mock(String name, String path, int port) {
|
||||||
if (!(name)) {
|
if (!(name)) {
|
||||||
|
@ -41,6 +47,20 @@ class Mock implements Comparable<Mock> {
|
||||||
|
|
||||||
MockResponse apply(MockRequest request) {
|
MockResponse apply(MockRequest request) {
|
||||||
log.debug("Mock $name invoked")
|
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
|
++counter
|
||||||
String responseText = response(request)
|
String responseText = response(request)
|
||||||
String response = soap ? wrapSoap(responseText) : responseText
|
String response = soap ? wrapSoap(responseText) : responseText
|
||||||
|
@ -106,4 +126,17 @@ class Mock implements Comparable<Mock> {
|
||||||
int compareTo(Mock o) {
|
int compareTo(Mock o) {
|
||||||
return name.compareTo(o.name)
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,6 +4,7 @@ import com.sun.net.httpserver.Headers
|
||||||
import groovy.json.JsonSlurper
|
import groovy.json.JsonSlurper
|
||||||
import groovy.transform.PackageScope
|
import groovy.transform.PackageScope
|
||||||
import groovy.util.slurpersupport.GPathResult
|
import groovy.util.slurpersupport.GPathResult
|
||||||
|
import groovy.xml.XmlUtil
|
||||||
|
|
||||||
@PackageScope
|
@PackageScope
|
||||||
class MockRequest {
|
class MockRequest {
|
||||||
|
@ -69,4 +70,7 @@ class MockRequest {
|
||||||
} as Map<String, String>
|
} as Map<String, String>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String getTextWithoutSoap() {
|
||||||
|
return XmlUtil.serialize(soap)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue