utils: factorize json schema validation (#86422)
It was already used in two places (views.py and photon connector), and wrongly (photon connector was ignoring some errors for no reason). META_SCHEMA manipulation is removed and will be replaced by a normalization of the schema to remove lazy strings in a later commit. A new JSONValidationError subclass of APIError is introduced.
This commit is contained in:
parent
45f6ee9e8d
commit
f2b64b6ebf
|
@ -23,11 +23,11 @@ from django.db.models import JSONField
|
|||
from django.utils.encoding import force_bytes
|
||||
from django.utils.http import urlencode
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from jsonschema import ValidationError, validate, validators
|
||||
from requests import RequestException
|
||||
|
||||
from passerelle.base.models import BaseResource
|
||||
from passerelle.utils.api import endpoint
|
||||
from passerelle.utils.json import JSONValidationError, validate_schema
|
||||
from passerelle.utils.jsonresponse import APIError
|
||||
|
||||
GEOJSON_SCHEMA = {
|
||||
|
@ -136,16 +136,11 @@ class Photon(BaseResource):
|
|||
return result
|
||||
|
||||
def validate_geojson(self, response_json):
|
||||
validator = validators.validator_for(GEOJSON_SCHEMA)
|
||||
validator.META_SCHEMA['properties'].pop('description', None)
|
||||
validator.META_SCHEMA['properties'].pop('title', None)
|
||||
try:
|
||||
validate(response_json, GEOJSON_SCHEMA)
|
||||
except ValidationError as e:
|
||||
error_msg = e.message
|
||||
if e.path:
|
||||
error_msg = '%s: %s' % ('/'.join(map(str, e.path)), error_msg)
|
||||
raise APIError(error_msg)
|
||||
validate_schema(response_json, GEOJSON_SCHEMA)
|
||||
except JSONValidationError as e:
|
||||
# do not return an HTTP 400 status
|
||||
raise APIError(str(e))
|
||||
|
||||
@endpoint(
|
||||
pattern='(?P<q>.+)?$',
|
||||
|
|
|
@ -31,6 +31,9 @@
|
|||
|
||||
import re
|
||||
|
||||
from jsonschema import ValidationError, validators
|
||||
|
||||
from passerelle.utils.jsonresponse import APIError
|
||||
from passerelle.utils.validation import is_number
|
||||
|
||||
FLATTEN_SEPARATOR = '/'
|
||||
|
@ -201,3 +204,30 @@ def datasource_array_schema():
|
|||
|
||||
def datasource_schema():
|
||||
return response_schema(datasource_array_schema())
|
||||
|
||||
|
||||
VALIDATOR_CACHE = {}
|
||||
|
||||
|
||||
def get_validator_for_schema(schema):
|
||||
if id(schema) not in VALIDATOR_CACHE:
|
||||
validator_cls = validators.validator_for(schema)
|
||||
validator_cls.check_schema(schema)
|
||||
validator = validator_cls(schema)
|
||||
VALIDATOR_CACHE[id(schema)] = validator
|
||||
return VALIDATOR_CACHE[id(schema)]
|
||||
|
||||
|
||||
class JSONValidationError(APIError):
|
||||
http_status = 400
|
||||
|
||||
|
||||
def validate_schema(instance, schema):
|
||||
validator = get_validator_for_schema(schema)
|
||||
try:
|
||||
validator.validate(instance)
|
||||
except ValidationError as e:
|
||||
error_msg = e.message
|
||||
if e.path:
|
||||
error_msg = '%s: %s' % ('/'.join(map(str, e.path)), error_msg)
|
||||
raise JSONValidationError(error_msg)
|
||||
|
|
|
@ -52,11 +52,10 @@ from django.views.generic import (
|
|||
)
|
||||
from django.views.generic.detail import SingleObjectMixin
|
||||
from django.views.static import serve
|
||||
from jsonschema import ValidationError, validate, validators
|
||||
|
||||
from passerelle.base.models import BaseResource, ResourceLog
|
||||
from passerelle.utils.conversion import normalize
|
||||
from passerelle.utils.json import unflatten
|
||||
from passerelle.utils.json import unflatten, validate_schema
|
||||
from passerelle.utils.jsonresponse import APIError, JSONEncoder
|
||||
from passerelle.utils.paginator import InfinitePaginator
|
||||
|
||||
|
@ -421,17 +420,8 @@ class GenericEndpointView(GenericConnectorMixin, SingleObjectMixin, View):
|
|||
if pre_process is not None:
|
||||
pre_process(self.endpoint.__self__, data)
|
||||
|
||||
# disable validation on description and title in order to allow lazy translation strings
|
||||
validator = validators.validator_for(json_schema)
|
||||
validator.META_SCHEMA['properties'].pop('description', None)
|
||||
validator.META_SCHEMA['properties'].pop('title', None)
|
||||
try:
|
||||
validate(data, json_schema)
|
||||
except ValidationError as e:
|
||||
error_msg = e.message
|
||||
if e.path:
|
||||
error_msg = '%s: %s' % ('/'.join(map(str, e.path)), error_msg)
|
||||
raise APIError(error_msg, http_status=400)
|
||||
validate_schema(data, json_schema)
|
||||
|
||||
d['post_data'] = data
|
||||
|
||||
return d
|
||||
|
|
|
@ -175,7 +175,7 @@ def test_upload(app, connector, monkeypatch):
|
|||
assert response.json == {
|
||||
'data': None,
|
||||
'err': 1,
|
||||
'err_class': 'passerelle.utils.jsonresponse.APIError',
|
||||
'err_class': 'passerelle.utils.json.JSONValidationError',
|
||||
'err_desc': "file: 'content' is a required property",
|
||||
}
|
||||
|
||||
|
@ -185,7 +185,7 @@ def test_upload(app, connector, monkeypatch):
|
|||
assert response.json == {
|
||||
'data': None,
|
||||
'err': 1,
|
||||
'err_class': 'passerelle.utils.jsonresponse.APIError',
|
||||
'err_class': 'passerelle.utils.json.JSONValidationError',
|
||||
'err_desc': "'file' is a required property",
|
||||
}
|
||||
|
||||
|
|
|
@ -260,8 +260,7 @@ def test_update_appointment_error(app, connector):
|
|||
resp = app.post_json(endpoint + '?id=94PEP4', params=payload, status=400)
|
||||
|
||||
assert resp.json['err']
|
||||
assert resp.json['err_class'] == 'passerelle.utils.jsonresponse.APIError'
|
||||
assert resp.json['err_desc'] == "idSys: 42 is not of type 'string'"
|
||||
assert 'JSONValidationError' in resp.json['err_class']
|
||||
|
||||
|
||||
def test_get_appointment(app, connector):
|
||||
|
|
|
@ -162,7 +162,7 @@ def test_demandes_recues_bad_coordinate(app, connector, geom, err_desc):
|
|||
resp = app.post_json('/litteralis/slug-litteralis/demandes-recues', params=params, status=400)
|
||||
json_resp = resp.json
|
||||
assert json_resp['err'] == 1
|
||||
assert json_resp['err_class'] == 'passerelle.utils.jsonresponse.APIError'
|
||||
assert 'Error' in json_resp['err_class']
|
||||
assert json_resp['err_desc'] == err_desc
|
||||
|
||||
|
||||
|
|
|
@ -118,7 +118,7 @@ def test_pdf_assemble(mocked_check_output, app, pdf):
|
|||
payload = {'filename': 'out.pdf', 'files/0': 42}
|
||||
resp = app.post_json(endpoint, params=payload, status=400)
|
||||
assert resp.json['err'] == 1
|
||||
assert resp.json['err_desc'] == "42 is not of type 'object'"
|
||||
assert resp.json['err_class'] == 'passerelle.utils.json.JSONValidationError'
|
||||
|
||||
resp = app.get(endpoint, status=405)
|
||||
|
||||
|
|
|
@ -363,7 +363,7 @@ def test_schemas(connector, soap):
|
|||
|
||||
|
||||
def test_say_hello_method_validation_error(connector, soap, app):
|
||||
resp = app.get('/soap/test/method/sayHello/')
|
||||
resp = app.get('/soap/test/method/sayHello/', status=400)
|
||||
assert resp.json == {
|
||||
'err': 1,
|
||||
'err_class': 'passerelle.utils.soap.SOAPValidationError',
|
||||
|
|
|
@ -3681,7 +3681,7 @@ def test_update_coordinate_schema_error(con, app):
|
|||
}
|
||||
resp = app.post_json(url + '?NameID=local&rl_id=613878', params=params, status=400)
|
||||
assert resp.json['err'] == 1
|
||||
assert resp.json['err_desc'] == "'true more text' is not of type 'boolean'"
|
||||
assert 'JSONValidationError' in resp.json['err_class']
|
||||
|
||||
|
||||
def test_update_coordinate_wrong_referential_key_error(con, app):
|
||||
|
@ -4348,7 +4348,7 @@ def test_update_child_pai_wrong_payload_type_error(con, app):
|
|||
|
||||
resp = app.post_json(url + '?NameID=local&child_id=613878', params=params, status=400)
|
||||
assert resp.json['err'] == 1
|
||||
assert resp.json['err_desc'] == "42 is not of type 'string'"
|
||||
assert 'JSONValidationError' in resp.json['err_class']
|
||||
|
||||
|
||||
def test_update_child_pai_empty_referential_key_error(con, app):
|
||||
|
@ -4568,8 +4568,7 @@ def test_update_rl_indicator_schema_error(value, con, app):
|
|||
Link.objects.create(resource=con, family_id='1312', name_id='local')
|
||||
resp = app.post_json(url + '?NameID=local&rl_id=613878', params=params, status=400)
|
||||
assert resp.json['err'] == 1
|
||||
assert resp.json['err_class'] == 'passerelle.utils.jsonresponse.APIError'
|
||||
assert "is not of type 'boolean'" in resp.json['err_desc']
|
||||
assert 'JSONValidationError' in resp.json['err_class']
|
||||
|
||||
|
||||
def test_update_rl_indicator_no_indicator_error(con, app):
|
||||
|
@ -8938,7 +8937,7 @@ def test_add_person_basket_subscription_with_indicators_schema_error(con, app):
|
|||
}
|
||||
resp = app.post_json(url + '?family_id=311323', params=params, status=400)
|
||||
assert resp.json['err'] == 1
|
||||
assert resp.json['err_desc'] == "'plop' is not of type 'boolean'"
|
||||
assert 'JSONValidationError' in resp.json['err_class']
|
||||
|
||||
|
||||
def test_add_person_basket_subscription_with_indicators_wrong_referential_key_error(
|
||||
|
|
|
@ -29,11 +29,15 @@
|
|||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import json
|
||||
|
||||
import jsonschema
|
||||
import pytest
|
||||
from django.utils.translation import gettext_lazy
|
||||
|
||||
from passerelle.utils.json import FLATTEN_SEPARATOR as SEP
|
||||
from passerelle.utils.json import flatten, flatten_json_schema, unflatten
|
||||
from passerelle.utils.json import flatten, flatten_json_schema, unflatten, validate_schema
|
||||
from passerelle.utils.jsonresponse import JSONEncoder
|
||||
|
||||
|
||||
def test_unflatten_base():
|
||||
|
@ -198,3 +202,25 @@ def test_flatten_dict_schema():
|
|||
},
|
||||
'additionalProperties': False,
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
'instance,schema',
|
||||
[
|
||||
(
|
||||
{
|
||||
'foo': 'bar',
|
||||
},
|
||||
{
|
||||
'type': 'object',
|
||||
'title': gettext_lazy('test'),
|
||||
'properties': {
|
||||
'foo': {'type': 'string'},
|
||||
},
|
||||
},
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_validate_schema(instance, schema):
|
||||
validate_schema(instance, schema)
|
||||
json.dumps(instance, cls=JSONEncoder)
|
||||
|
|
Loading…
Reference in New Issue