From 035b401faa7ff0c62ef0947a60f5f22fa34991cd Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Laur=C3=A9line=20Gu=C3=A9rin?= <zebuline@entrouvert.com>
Date: Tue, 14 Jan 2020 10:37:20 +0100
Subject: [PATCH] jsonschema: pre_process the payload, before schema validation
 (#38933)

---
 passerelle/views.py            |   3 +
 tests/test_generic_endpoint.py | 126 ++++++++++++++++++++++++++++++++-
 2 files changed, 128 insertions(+), 1 deletion(-)

diff --git a/passerelle/views.py b/passerelle/views.py
index a82f219e..09ef94a9 100644
--- a/passerelle/views.py
+++ b/passerelle/views.py
@@ -348,6 +348,7 @@ class GenericEndpointView(GenericConnectorMixin, SingleObjectMixin, View):
                 json_schema = request_body['schema']['application/json']
                 must_unflatten = hasattr(json_schema, 'items') and json_schema.get('unflatten', False)
                 merge_extra = hasattr(json_schema, 'items') and json_schema.get('merge_extra', False)
+                pre_process = hasattr(json_schema, 'items') and json_schema.get('pre_process')
                 try:
                     data = json.loads(request.body)
                 except ValueError as e:
@@ -356,6 +357,8 @@ class GenericEndpointView(GenericConnectorMixin, SingleObjectMixin, View):
                     data = unflatten(data)
                 if merge_extra and hasattr(data, 'items'):
                     data.update(data.pop('extra', {}))
+                if pre_process is not None:
+                    pre_process(self.endpoint.im_self, data)
                 try:
                     validate(data, json_schema)
                 except ValidationError as e:
diff --git a/tests/test_generic_endpoint.py b/tests/test_generic_endpoint.py
index 8a97df75..5f18ac3a 100644
--- a/tests/test_generic_endpoint.py
+++ b/tests/test_generic_endpoint.py
@@ -17,6 +17,7 @@
 
 from __future__ import unicode_literals
 
+import copy
 import os
 import json
 import random
@@ -27,10 +28,11 @@ import pytest
 
 import utils
 
+from django.contrib.contenttypes.models import ContentType
 from django.core.urlresolvers import reverse
 
 from passerelle.apps.arcgis.models import ArcGIS
-from passerelle.base.models import ResourceLog, ProxyLogger, BaseResource, HTTPResource
+from passerelle.base.models import ResourceLog, ProxyLogger, BaseResource, HTTPResource, LoggingParameters
 from passerelle.base.models import ResourceStatus
 from passerelle.apps.mdel.models import MDEL
 from passerelle.contrib.stub_invoices.models import StubInvoicesConnector
@@ -267,6 +269,128 @@ def test_endpoint_decorator():
     connector.foo9.endpoint_info.http_method = 'post'
     assert connector.foo9.endpoint_info.long_description == 'foo9 post'
 
+
+class FakeJSONConnector(object):
+    slug = 'connector-json'
+    log_level = 'DEBUG'
+
+    FOO_SCHEMA = {
+        'properties': {
+            'foo': {
+                'type': 'array',
+                'items': {
+                    'properties': {
+                        'id': {'type': 'integer'},
+                        'bar': {'type': 'boolean'}
+                    },
+                    'required': ['id', 'bar']
+                }
+            }
+        }
+    }
+    BAR_SCHEMA = copy.deepcopy(FOO_SCHEMA)
+
+    def __init__(self, *args, **kwargs):
+        super(FakeJSONConnector, self).__init__(*args, **kwargs)
+        self.logger = ProxyLogger(connector=self)
+
+    @property
+    def logging_parameters(self):
+        return LoggingParameters()
+
+    def get_connector_slug(self):
+        return 'connector-json'
+
+    def down(self):
+        return False
+
+    def pre_process(self, post_data):
+        foo_objects = copy.deepcopy(post_data.get('foo', []))
+        foo_objects.reverse()
+        foo_len = len(foo_objects)
+        for i, foo in enumerate(foo_objects):
+            if not foo.get('id'):
+                # id is empty, remove foo object
+                post_data['foo'].pop(foo_len - i - 1)
+        return post_data
+
+    BAR_SCHEMA['pre_process'] = pre_process
+
+    @endpoint(
+        post={
+            'request_body': {
+                'schema': {
+                    'application/json': FOO_SCHEMA
+                }
+            }
+        })
+    def foo(self, request, post_data):
+        return {'data': post_data}
+
+    @endpoint(
+        post={
+            'request_body': {
+                'schema': {
+                    'application/json': BAR_SCHEMA
+                }
+            }
+        })
+    def bar(self, request, post_data):
+        return {'data': post_data}
+
+
+def test_endpoint_decorator_pre_process(db, app):
+    connector = FakeJSONConnector()
+
+    patch_init = mock.patch('passerelle.views.GenericConnectorMixin.init_stuff')
+    patch_object = mock.patch('passerelle.views.GenericEndpointView.get_object',
+                              return_value=connector)
+
+    url_foo = reverse('generic-endpoint', kwargs={
+        'connector': 'connector-json',
+        'slug': 'connector-json',
+        'endpoint': 'foo',
+    })
+    url_bar = reverse('generic-endpoint', kwargs={
+        'connector': 'connector-json',
+        'slug': 'connector-json',
+        'endpoint': 'bar',
+    })
+
+    payload = {
+        'foo': [
+            {'id': 42, 'bar': True}
+        ]
+    }
+    with patch_init, patch_object:
+        resp = app.post_json(url_foo, params=payload)
+    assert resp.json['err'] == 0
+    assert resp.json['data'] == payload
+    with patch_init, patch_object:
+        resp = app.post_json(url_bar, params=payload)
+    assert resp.json['err'] == 0
+    assert resp.json['data'] == payload
+
+    payload = {
+        'foo': [
+            {'id': 42, 'bar': True},
+            {'id': None, 'bar': False}  # invalid object
+        ]
+    }
+    with patch_init, patch_object:
+        resp = app.post_json(url_foo, params=payload, status=400)
+    assert resp.json['err'] == 1
+    assert resp.json['err_desc'] == "None is not of type u'integer'"
+    with patch_init, patch_object:
+        resp = app.post_json(url_bar, params=payload)
+    assert resp.json['err'] == 0
+    assert resp.json['data'] == {
+        'foo': [
+            {'id': 42, 'bar': True}
+        ]
+    }
+
+
 def test_endpoint_description_in_template(app, db):
     StubInvoicesConnector(slug='fake').save()
     resp = app.get('/stub-invoices/fake/')
-- 
2.20.1

