Skip to content

Commit 08fdf7c

Browse files
committed
Response validator
1 parent b0c4141 commit 08fdf7c

File tree

6 files changed

+223
-16
lines changed

6 files changed

+223
-16
lines changed

openapi_core/operations.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,6 @@ def get_response(self, http_status='default'):
3939
if http_status_range in self.responses:
4040
return self.responses[http_status_range]
4141

42-
4342
if 'default' not in self.responses:
4443
raise InvalidResponse(
4544
"Unknown response http status {0}".format(http_status))

openapi_core/validators.py

Lines changed: 69 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
from six import iteritems
33

44
from openapi_core.exceptions import (
5-
OpenAPIMappingError, MissingParameter, MissingBody,
5+
OpenAPIMappingError, MissingParameter, MissingBody, InvalidResponse,
66
)
77

88

@@ -43,6 +43,14 @@ def __init__(self, errors, body=None, parameters=None):
4343
self.parameters = parameters or RequestParameters()
4444

4545

46+
class ResponseValidationResult(BaseValidationResult):
47+
48+
def __init__(self, errors, body=None, headers=None):
49+
super(ResponseValidationResult, self).__init__(errors)
50+
self.body = body
51+
self.headers = headers
52+
53+
4654
class RequestValidator(object):
4755

4856
def __init__(self, spec):
@@ -120,3 +128,63 @@ def _get_raw_body(self, request):
120128
raise MissingBody("Missing required request body")
121129

122130
return request.body
131+
132+
133+
class ResponseValidator(object):
134+
135+
def __init__(self, spec):
136+
self.spec = spec
137+
138+
def validate(self, request, response):
139+
errors = []
140+
body = None
141+
headers = {}
142+
143+
try:
144+
server = self.spec.get_server(request.full_url_pattern)
145+
# don't process if server errors
146+
except OpenAPIMappingError as exc:
147+
errors.append(exc)
148+
return ResponseValidationResult(errors, body, headers)
149+
150+
operation_pattern = request.full_url_pattern.replace(
151+
server.default_url, '')
152+
153+
try:
154+
operation = self.spec.get_operation(
155+
operation_pattern, request.method)
156+
# don't process if operation errors
157+
except OpenAPIMappingError as exc:
158+
errors.append(exc)
159+
return ResponseValidationResult(errors, body, headers)
160+
161+
try:
162+
operation_response = operation.get_response(str(response.status))
163+
# don't process if invalid response status code
164+
except InvalidResponse as exc:
165+
errors.append(exc)
166+
return ResponseValidationResult(errors, body, headers)
167+
168+
if operation_response.content:
169+
try:
170+
media_type = operation_response.content[response.mimetype]
171+
except OpenAPIMappingError as exc:
172+
errors.append(exc)
173+
else:
174+
try:
175+
raw_body = self._get_raw_body(response)
176+
except MissingBody as exc:
177+
errors.append(exc)
178+
else:
179+
try:
180+
body = media_type.unmarshal(raw_body)
181+
except OpenAPIMappingError as exc:
182+
errors.append(exc)
183+
184+
return ResponseValidationResult(errors, body, headers)
185+
186+
def _get_raw_body(self, response):
187+
if not response.body:
188+
raise MissingBody("Missing required response body")
189+
190+
return response.body

openapi_core/wrappers.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,3 +104,38 @@ def body(self):
104104
@property
105105
def mimetype(self):
106106
return self.request.mimetype
107+
108+
109+
class BaseOpenAPIResponse(object):
110+
111+
body = NotImplemented
112+
status = NotImplemented
113+
114+
mimetype = NotImplemented
115+
116+
117+
class MockResponse(BaseOpenAPIRequest):
118+
119+
def __init__(self, body, status=200, mimetype='application/json'):
120+
self.body = body
121+
122+
self.status = status
123+
self.mimetype = mimetype
124+
125+
126+
class FlaskOpenAPIResponse(BaseOpenAPIResponse):
127+
128+
def __init__(self, response):
129+
self.response = response
130+
131+
@property
132+
def body(self):
133+
return self.response.text
134+
135+
@property
136+
def status(self):
137+
return self.response.status
138+
139+
@property
140+
def mimetype(self):
141+
return self.response.mimetype

tests/integration/data/v3.0/petstore.yaml

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -61,12 +61,6 @@ paths:
6161
application/json:
6262
schema:
6363
$ref: "#/components/schemas/Pets"
64-
default:
65-
description: unexpected error
66-
content:
67-
application/json:
68-
schema:
69-
$ref: "#/components/schemas/Error"
7064
post:
7165
summary: Create a pet
7266
operationId: createPets
@@ -164,9 +158,12 @@ components:
164158
position:
165159
$ref: "#/components/schemas/Position"
166160
Pets:
167-
type: array
168-
items:
169-
$ref: "#/components/schemas/Pet"
161+
type: object
162+
properties:
163+
data:
164+
type: array
165+
items:
166+
$ref: "#/components/schemas/Pet"
170167
Error:
171168
type: object
172169
required:

tests/integration/test_validators.py

Lines changed: 93 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,11 @@
33

44
from openapi_core.exceptions import (
55
InvalidServer, InvalidOperation, MissingParameter,
6-
MissingBody, InvalidContentType,
6+
MissingBody, InvalidContentType, InvalidResponse, InvalidMediaTypeValue,
77
)
88
from openapi_core.shortcuts import create_spec
9-
from openapi_core.validators import RequestValidator
10-
from openapi_core.wrappers import MockRequest
9+
from openapi_core.validators import RequestValidator, ResponseValidator
10+
from openapi_core.wrappers import MockRequest, MockResponse
1111

1212

1313
class TestRequestValidator(object):
@@ -155,3 +155,93 @@ def test_get_pet(self, validator):
155155
'petId': 1,
156156
},
157157
}
158+
159+
160+
class TestResponseValidator(object):
161+
162+
host_url = 'http://petstore.swagger.io'
163+
164+
@pytest.fixture
165+
def spec_dict(self, factory):
166+
return factory.spec_from_file("data/v3.0/petstore.yaml")
167+
168+
@pytest.fixture
169+
def spec(self, spec_dict):
170+
return create_spec(spec_dict)
171+
172+
@pytest.fixture
173+
def validator(self, spec):
174+
return ResponseValidator(spec)
175+
176+
def test_invalid_server(self, validator):
177+
request = MockRequest('http://petstore.invalid.net/v1', 'get', '/')
178+
response = MockResponse('Not Found', status=404)
179+
180+
result = validator.validate(request, response)
181+
182+
assert len(result.errors) == 1
183+
assert type(result.errors[0]) == InvalidServer
184+
assert result.body is None
185+
assert result.headers == {}
186+
187+
def test_invalid_operation(self, validator):
188+
request = MockRequest(self.host_url, 'get', '/v1')
189+
response = MockResponse('Not Found', status=404)
190+
191+
result = validator.validate(request, response)
192+
193+
assert len(result.errors) == 1
194+
assert type(result.errors[0]) == InvalidOperation
195+
assert result.body is None
196+
assert result.headers == {}
197+
198+
def test_invalid_response(self, validator):
199+
request = MockRequest(self.host_url, 'get', '/v1/pets')
200+
response = MockResponse('Not Found', status=409)
201+
202+
result = validator.validate(request, response)
203+
204+
assert len(result.errors) == 1
205+
assert type(result.errors[0]) == InvalidResponse
206+
assert result.body is None
207+
assert result.headers == {}
208+
209+
def test_missing_body(self, validator):
210+
request = MockRequest(self.host_url, 'get', '/v1/pets')
211+
response = MockResponse(None)
212+
213+
result = validator.validate(request, response)
214+
215+
assert len(result.errors) == 1
216+
assert type(result.errors[0]) == MissingBody
217+
assert result.body is None
218+
assert result.headers == {}
219+
220+
def test_invalid_media_type_value(self, validator):
221+
request = MockRequest(self.host_url, 'get', '/v1/pets')
222+
response = MockResponse('\{\}')
223+
224+
result = validator.validate(request, response)
225+
226+
assert len(result.errors) == 1
227+
assert type(result.errors[0]) == InvalidMediaTypeValue
228+
assert result.body is None
229+
assert result.headers == {}
230+
231+
def test_get_pets(self, validator):
232+
request = MockRequest(self.host_url, 'get', '/v1/pets')
233+
response_json = {
234+
'data': [
235+
{
236+
'id': 1,
237+
},
238+
],
239+
}
240+
response_data = json.dumps(response_json)
241+
response = MockResponse(response_data)
242+
243+
result = validator.validate(request, response)
244+
245+
assert result.errors == []
246+
assert result.body == response_json
247+
assert result.headers == {}

tests/integration/test_wrappers.py

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
import pytest
22

3-
from flask.wrappers import Request
3+
from flask.wrappers import Request, Response
44
from werkzeug.datastructures import EnvironHeaders, ImmutableMultiDict
55
from werkzeug.routing import Map, Rule, Subdomain
66
from werkzeug.test import create_environ
77

8-
from openapi_core.wrappers import FlaskOpenAPIRequest
8+
from openapi_core.wrappers import FlaskOpenAPIRequest, FlaskOpenAPIResponse
99

1010

1111
class TestFlaskOpenAPIRequest(object):
@@ -90,3 +90,21 @@ def test_url_rule(self, request_factory, environ, request):
9090
assert openapi_request.path_pattern == request.url_rule.rule
9191
assert openapi_request.body == request.data
9292
assert openapi_request.mimetype == request.mimetype
93+
94+
95+
class TetsFlaskOpenAPIResponse(object):
96+
97+
@pytest.fixture
98+
def response_factory(self):
99+
def create_response(body, status=200):
100+
return Response('Not Found', status=404)
101+
return create_response
102+
103+
def test_invalid_server(self, response_factory):
104+
response = response_factory('Not Found', status=404)
105+
106+
openapi_response = FlaskOpenAPIResponse(response)
107+
108+
assert openapi_response.body == response.text
109+
assert openapi_response.status == response.status
110+
assert openapi_response.mimetype == response.mimetype

0 commit comments

Comments
 (0)