Add request validation against xsd

Change-Id: Iadf618e50d01dd92bf9eed5b8ed98138b56c20c9
This commit is contained in:
Dominik Adam Przybysz 2015-10-24 13:46:52 +02:00
parent aabc9d75f2
commit 9a14f9bfab
10 changed files with 205 additions and 10 deletions

View file

@ -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>

View file

@ -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>

View file

@ -0,0 +1,9 @@
package pl.touk.mockserver.client
import groovy.transform.CompileStatic
import groovy.transform.TypeChecked
@CompileStatic
@TypeChecked
class InvalidMockRequestSchema extends RuntimeException {
}

View file

@ -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)
} }

View file

@ -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
} }
@ -820,9 +834,9 @@ class MockServerIntegrationTest extends Specification {
mockEvents2.size() == 1 mockEvents2.size() == 1
mockEvents2[0].request.text == '<reqXYZ/>' mockEvents2[0].request.text == '<reqXYZ/>'
!mockEvents2[0].request.headers?.headers?.empty !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].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 == '<goodResponseRest/>' mockEvents2[0].response.text == '<goodResponseRest/>'
mockEvents2[0].response.statusCode == 202 mockEvents2[0].response.statusCode == 202
} }
@ -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
}
} }

View 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>

View 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>

View file

@ -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
} }
@ -135,12 +137,12 @@ class HttpMockServer {
queryParams: new MockRequestReport.QueryParams(queryParams: it.request.query.collect { queryParams: new MockRequestReport.QueryParams(queryParams: it.request.query.collect {
new Parameter(name: it.key, value: it.value) 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( response: new MockResponseReport(
statusCode: it.response.statusCode, statusCode: it.response.statusCode,
text: it.response.text, 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) new Parameter(name: it.key, value: it.value)
}) })
) )
@ -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)
} }

View file

@ -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)
}
}
}
} }

View file

@ -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)
}
} }