Commit 5341faa9 authored by Romain Courteaud's avatar Romain Courteaud

erp5_json_rpc_api: validate output schema

parent 3c2a77c0
...@@ -335,9 +335,9 @@ class JsonRpcAPIService(OpenAPIService): ...@@ -335,9 +335,9 @@ class JsonRpcAPIService(OpenAPIService):
operation = self.getMatchingOperation(request) operation = self.getMatchingOperation(request)
if operation is None: if operation is None:
raise NotFound() raise NotFound()
method = getattr(self, operation)#self.getMethodForOperation(operation) json_form = getattr(self, operation)#self.getMethodForOperation(operation)
if method.getPortalType() != 'JSON Form': if json_form.getPortalType() != 'JSON Form':
raise ValueError('%s is not a JSON Form' % operation) raise ValueError('%s is not a JSON Form' % operation)
# parameters = self.extractParametersFromRequest(operation, request) # parameters = self.extractParametersFromRequest(operation, request)
try: try:
...@@ -348,16 +348,32 @@ class JsonRpcAPIService(OpenAPIService): ...@@ -348,16 +348,32 @@ class JsonRpcAPIService(OpenAPIService):
raise JsonRpcAPINotJsonDictContent("Did not received a JSON Object") raise JsonRpcAPINotJsonDictContent("Did not received a JSON Object")
try: try:
result = method(json_data=json_data, list_error=False)#**parameters) result = json_form(json_data=json_data, list_error=False)#**parameters)
except jsonschema.exceptions.ValidationError as e: except jsonschema.exceptions.ValidationError as e:
raise JsonRpcAPIInvalidJsonDictContent(str(e)) raise JsonRpcAPIInvalidJsonDictContent(str(e))
response = request.RESPONSE response = request.RESPONSE
output_schema = json_form.getOutputSchema()
# XXX Hardcoded JSONForm behaviour # XXX Hardcoded JSONForm behaviour
if (result == "Nothing to do") or (not result): if (result == "Nothing to do") or (not result):
# If there is no output, ensure no output schema is defined
if output_schema:
raise ValueError('%s has an output schema but response is empty' % operation)
result = { result = {
'status': 200, 'status': 200,
'type': 'success-type', 'type': 'success-type',
'title': 'query completed' 'title': 'query completed'
} }
else:
if not output_schema:
raise ValueError('%s does not have an output schema but response is not empty' % operation)
# Ensure the response matches the output schema
try:
jsonschema.validate(
result,
json.loads(output_schema),
format_checker=jsonschema.FormatChecker()
)
except jsonschema.exceptions.ValidationError as e:
raise ValueError(e.message)
response.setHeader("Content-Type", "application/json") response.setHeader("Content-Type", "application/json")
return json.dumps(result).encode() return json.dumps(result).encode()
...@@ -35,12 +35,13 @@ class JsonRpcAPITestCase(ERP5TypeTestCase): ...@@ -35,12 +35,13 @@ class JsonRpcAPITestCase(ERP5TypeTestCase):
_action_list_text = '' _action_list_text = ''
def addJSONForm(self, script_id, input_json_schema=None, def addJSONForm(self, script_id, input_json_schema=None,
after_method_id=None): after_method_id=None, output_json_schema=None):
self.portal.portal_callables.newContent( self.portal.portal_callables.newContent(
portal_type='JSON Form', portal_type='JSON Form',
id=script_id, id=script_id,
text_content=input_json_schema, text_content=input_json_schema,
after_method_id=after_method_id after_method_id=after_method_id,
output_schema=output_json_schema
) )
self.tic() self.tic()
self._json_form_id_to_cleanup.append(script_id) self._json_form_id_to_cleanup.append(script_id)
...@@ -410,6 +411,126 @@ error.handling.callable | JsonRpcService_testExample''' ...@@ -410,6 +411,126 @@ error.handling.callable | JsonRpcService_testExample'''
}) })
self.assertNotEqual(self.portal.getTitle(), "ooops") self.assertNotEqual(self.portal.getTitle(), "ooops")
def test_requestErrorHandling_noOutputWithOutputSchema(self):
self.addPythonScript(
'JsonRpcService_returnNothing',
'data_dict, json_form',
'context.getPortalObject().setTitle("ooops")\n'
)
self.addJSONForm(
'JsonRpcService_testExample',
'{}',
output_json_schema='''{"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {"foo": {"type": "string"}}}''',
after_method_id='JsonRpcService_returnNothing',
)
response = self.publish(
self.connector.getPath() + '/error.handling.callable',
user='ERP5TypeTestCase',
request_method='POST',
stdin=io.BytesIO(
'{}'.encode()),
env={'CONTENT_TYPE': 'application/json'})
self.assertEqual(response.getStatus(), 500)
self.assertEqual(response.getHeader('content-type'), 'application/json')
self.assertEqual(
json.loads(response.getBody()), {
"type": "unknown-error",
"title": "ValueError: JsonRpcService_testExample has an output schema but response is empty"
})
self.assertNotEqual(self.portal.getTitle(), "ooops")
def test_requestErrorHandling_emptyOutputWithOutputSchema(self):
self.addPythonScript(
'JsonRpcService_returnNothing',
'data_dict, json_form',
'context.getPortalObject().setTitle("ooops")\n'
'return {}'
)
self.addJSONForm(
'JsonRpcService_testExample',
'{}',
output_json_schema='''{"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {"foo": {"type": "string"}}}''',
after_method_id='JsonRpcService_returnNothing',
)
response = self.publish(
self.connector.getPath() + '/error.handling.callable',
user='ERP5TypeTestCase',
request_method='POST',
stdin=io.BytesIO(
'{}'.encode()),
env={'CONTENT_TYPE': 'application/json'})
self.assertEqual(response.getStatus(), 500)
self.assertEqual(response.getHeader('content-type'), 'application/json')
self.assertEqual(
json.loads(response.getBody()), {
"type": "unknown-error",
"title": "ValueError: JsonRpcService_testExample has an output schema but response is empty"
})
self.assertNotEqual(self.portal.getTitle(), "ooops")
def test_requestErrorHandling_invalidOutputWithOutputSchema(self):
self.addPythonScript(
'JsonRpcService_returnNothing',
'data_dict, json_form',
'context.getPortalObject().setTitle("ooops")\n'
'return {"foo": 2}'
)
self.addJSONForm(
'JsonRpcService_testExample',
'{}',
output_json_schema='''{"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {"foo": {"type": "string"}}}''',
after_method_id='JsonRpcService_returnNothing',
)
response = self.publish(
self.connector.getPath() + '/error.handling.callable',
user='ERP5TypeTestCase',
request_method='POST',
stdin=io.BytesIO(
'{}'.encode()),
env={'CONTENT_TYPE': 'application/json'})
self.assertEqual(response.getStatus(), 500)
self.assertEqual(response.getHeader('content-type'), 'application/json')
self.assertEqual(
json.loads(response.getBody()), {
"type": "unknown-error",
"title": "ValueError: 2 is not of type u'string'"
})
self.assertNotEqual(self.portal.getTitle(), "ooops")
def test_requestErrorHandling_outputWithoutOutputSchema(self):
self.addPythonScript(
'JsonRpcService_returnNothing',
'data_dict, json_form',
'context.getPortalObject().setTitle("ooops")\n'
'return {"foo": 2}'
)
self.addJSONForm(
'JsonRpcService_testExample',
'{}',
after_method_id='JsonRpcService_returnNothing',
)
response = self.publish(
self.connector.getPath() + '/error.handling.callable',
user='ERP5TypeTestCase',
request_method='POST',
stdin=io.BytesIO(
'{}'.encode()),
env={'CONTENT_TYPE': 'application/json'})
self.assertEqual(response.getStatus(), 500)
self.assertEqual(response.getHeader('content-type'), 'application/json')
self.assertEqual(
json.loads(response.getBody()), {
"type": "unknown-error",
"title": "ValueError: JsonRpcService_testExample does not have an output schema but response is not empty"
})
self.assertNotEqual(self.portal.getTitle(), "ooops")
class TestJsonRpcAPICustomErrorHandling(JsonRpcAPITestCase): class TestJsonRpcAPICustomErrorHandling(JsonRpcAPITestCase):
_action_list_text = '''error.handling.missing.callable | JsonRpcService_doesNotExist _action_list_text = '''error.handling.missing.callable | JsonRpcService_doesNotExist
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment