Skip to content

Commit b7f3c3d

Browse files
committed
tests: Switch to openapi-core 0.13.x
We've done the necessary work here already so this is a relatively easy switchover. However, we do have to work around an issue whereby the first possible matching route is used rather than the best one [1]. In addition, we have to install from master since there are fixes missing from the latest release, 0.13.3. Hopefully both issues will be resolved in a future release. [1] python-openapi/openapi-core#226 Signed-off-by: Stephen Finucane <[email protected]>
1 parent 90380ad commit b7f3c3d

File tree

2 files changed

+20
-175
lines changed

2 files changed

+20
-175
lines changed

patchwork/tests/api/validator.py

Lines changed: 19 additions & 174 deletions
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,15 @@
77
import re
88

99
from django.urls import resolve
10-
from django.urls.resolvers import get_resolver
1110
import openapi_core
11+
from openapi_core.contrib.django import DjangoOpenAPIResponseFactory
12+
from openapi_core.contrib.django import DjangoOpenAPIRequestFactory
1213
from openapi_core.schema.schemas.models import Format
13-
from openapi_core.wrappers.base import BaseOpenAPIResponse
14-
from openapi_core.wrappers.base import BaseOpenAPIRequest
1514
from openapi_core.validation.request.validators import RequestValidator
1615
from openapi_core.validation.response.validators import ResponseValidator
1716
from openapi_core.schema.parameters.exceptions import OpenAPIParameterError
1817
from openapi_core.schema.media_types.exceptions import OpenAPIMediaTypeError
18+
from openapi_core.templating import util
1919
from rest_framework import status
2020
import yaml
2121

@@ -24,13 +24,23 @@
2424
os.path.dirname(os.path.abspath(__file__)), os.pardir, os.pardir,
2525
os.pardir, 'docs', 'api', 'schemas')
2626

27-
HEADER_REGEXES = (
28-
re.compile(r'^HTTP_.+$'), re.compile(r'^CONTENT_TYPE$'),
29-
re.compile(r'^CONTENT_LENGTH$'))
30-
3127
_LOADED_SPECS = {}
3228

3329

30+
# HACK! Workaround for https://github.com/p1c2u/openapi-core/issues/226
31+
def search(path_pattern, full_url_pattern):
32+
p = util.Parser(path_pattern)
33+
p._expression = p._expression + '$'
34+
result = p.search(full_url_pattern)
35+
if not result or any('/' in arg for arg in result.named.values()):
36+
return None
37+
38+
return result
39+
40+
41+
util.search = search
42+
43+
3444
class RegexValidator(object):
3545

3646
def __init__(self, regex):
@@ -61,113 +71,6 @@ def __call__(self, value):
6171
}
6272

6373

64-
def _extract_headers(request):
65-
request_headers = {}
66-
for header in request.META:
67-
for regex in HEADER_REGEXES:
68-
if regex.match(header):
69-
request_headers[header] = request.META[header]
70-
71-
return request_headers
72-
73-
74-
def _resolve(path, resolver=None):
75-
"""Resolve a given path to its matching regex (Django 2.x).
76-
77-
This is essentially a re-implementation of ``URLResolver.resolve`` that
78-
builds and returns the matched regex instead of the view itself.
79-
80-
>>> _resolve('/api/1.0/patches/1/checks/')
81-
"^api/(?:(?P<version>(1.0|1.1))/)patches/(?P<patch_id>[^/]+)/checks/$"
82-
"""
83-
from django.urls.resolvers import URLResolver # noqa
84-
from django.urls.resolvers import RegexPattern # noqa
85-
86-
resolver = resolver or get_resolver()
87-
match = resolver.pattern.match(path)
88-
89-
# we dont handle any other type of pattern at the moment
90-
assert isinstance(resolver.pattern, RegexPattern)
91-
92-
if not match:
93-
return
94-
95-
if isinstance(resolver, URLResolver):
96-
sub_path, args, kwargs = match
97-
for sub_resolver in resolver.url_patterns:
98-
sub_match = _resolve(sub_path, sub_resolver)
99-
if not sub_match:
100-
continue
101-
102-
kwargs.update(sub_match[2])
103-
args += sub_match[1]
104-
105-
regex = resolver.pattern._regex + sub_match[0].lstrip('^')
106-
107-
return regex, args, kwargs
108-
else:
109-
_, args, kwargs = match
110-
return resolver.pattern._regex, args, kwargs
111-
112-
113-
def _resolve_path_to_kwargs(path):
114-
"""Convert a path to the kwargs used to resolve it.
115-
116-
>>> resolve_path_to_kwargs('/api/1.0/patches/1/checks/')
117-
{"patch_id": 1}
118-
"""
119-
# TODO(stephenfin): Handle definition by args
120-
_, _, kwargs = _resolve(path)
121-
122-
results = {}
123-
for key, value in kwargs.items():
124-
if key == 'version':
125-
continue
126-
127-
if key == 'pk':
128-
key = 'id'
129-
130-
results[key] = value
131-
132-
return results
133-
134-
135-
def _resolve_path_to_template(path):
136-
"""Convert a path to a template string.
137-
138-
>>> resolve_path_to_template('/api/1.0/patches/1/checks/')
139-
"/api/{version}/patches/{patch_id}/checks/"
140-
"""
141-
regex, _, _ = _resolve(path)
142-
regex = re.match(regex, path)
143-
144-
result = ''
145-
prev_index = 0
146-
for index, group in enumerate(regex.groups(), 1):
147-
if not group: # group didn't match anything
148-
continue
149-
150-
result += path[prev_index:regex.start(index)]
151-
prev_index = regex.end(index)
152-
# groupindex keys by name, not index. Switch that.
153-
for name, index_ in regex.re.groupindex.items():
154-
if index_ == (index):
155-
# special-case version group
156-
if name == 'version':
157-
result += group
158-
break
159-
160-
if name == 'pk':
161-
name = 'id'
162-
163-
result += '{%s}' % name
164-
break
165-
166-
result += path[prev_index:]
167-
168-
return result
169-
170-
17174
def _load_spec(version):
17275
global _LOADED_SPECS
17376

@@ -186,72 +89,14 @@ def _load_spec(version):
18689
return _LOADED_SPECS[version]
18790

18891

189-
class DRFOpenAPIRequest(BaseOpenAPIRequest):
190-
191-
def __init__(self, request):
192-
self.request = request
193-
194-
@property
195-
def host_url(self):
196-
return self.request.get_host()
197-
198-
@property
199-
def path(self):
200-
return self.request.path
201-
202-
@property
203-
def method(self):
204-
return self.request.method.lower()
205-
206-
@property
207-
def path_pattern(self):
208-
return _resolve_path_to_template(self.request.path_info)
209-
210-
@property
211-
def parameters(self):
212-
return {
213-
'path': _resolve_path_to_kwargs(self.request.path_info),
214-
'query': self.request.GET,
215-
'header': _extract_headers(self.request),
216-
'cookie': self.request.COOKIES,
217-
}
218-
219-
@property
220-
def body(self):
221-
return self.request.body.decode('utf-8')
222-
223-
@property
224-
def mimetype(self):
225-
return self.request.content_type
226-
227-
228-
class DRFOpenAPIResponse(BaseOpenAPIResponse):
229-
230-
def __init__(self, response):
231-
self.response = response
232-
233-
@property
234-
def data(self):
235-
return self.response.content.decode('utf-8')
236-
237-
@property
238-
def status_code(self):
239-
return self.response.status_code
240-
241-
@property
242-
def mimetype(self):
243-
# TODO(stephenfin): Why isn't this populated?
244-
return 'application/json'
245-
246-
24792
def validate_data(path, request, response, validate_request,
24893
validate_response):
24994
if response.status_code == status.HTTP_405_METHOD_NOT_ALLOWED:
25095
return
25196

25297
spec = _load_spec(resolve(path).kwargs.get('version'))
253-
request = DRFOpenAPIRequest(request)
254-
response = DRFOpenAPIResponse(response)
98+
request = DjangoOpenAPIRequestFactory.create(request)
99+
response = DjangoOpenAPIResponseFactory.create(response)
255100

256101
# request
257102
if validate_request:

requirements-test.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,4 @@ mysqlclient~=1.4.4
22
psycopg2-binary~=2.8.0
33
sqlparse~=0.3.0
44
python-dateutil~=2.8.0
5-
openapi-core~=0.8.0
5+
https://github.com/p1c2u/openapi-core/archive/97ec8c796746f72ef3298fe92078b5f80e1f66f7.tar.gz

0 commit comments

Comments
 (0)