diff --git a/passerelle/apps/cmis/__init__.py b/passerelle/apps/cmis/__init__.py index 5b4cbaca..e69de29b 100644 --- a/passerelle/apps/cmis/__init__.py +++ b/passerelle/apps/cmis/__init__.py @@ -1,17 +0,0 @@ -# passerelle - uniform access to multiple data sources and services -# Copyright (C) 2021 Entr'ouvert -# -# This program is free software: you can redistribute it and/or modify it -# under the terms of the GNU Affero General Public License as published -# by the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. -# -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see . - -default_app_config = 'passerelle.apps.cmis.apps.CmisAppConfig' diff --git a/passerelle/apps/cmis/apps.py b/passerelle/apps/cmis/apps.py deleted file mode 100644 index a9cd1de8..00000000 --- a/passerelle/apps/cmis/apps.py +++ /dev/null @@ -1,62 +0,0 @@ -# passerelle - uniform access to multiple data sources and services -# Copyright (C) 2021 Entr'ouvert -# -# This program is free software: you can redistribute it and/or modify it -# under the terms of the GNU Affero General Public License as published -# by the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. -# -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see . - -from django.apps import AppConfig - - -def add_logging_to_cmislib(): - '''Monkeypatch cmislib request module to log requests and responses.''' - - from cmislib.atompub import binding - from cmislib.net import RESTService as CMISRESTService - - class RESTService(CMISRESTService): - def get(self, url, *args, **kwargs): - logger = kwargs.pop('passerelle_logger') - logger.debug('cmislib GET request to %s', url) - resp, content = super().get(url, *args, **kwargs) - logger.debug('cmislib GET response (%s)', resp['status'], extra={'response': content.decode()}) - return resp, content - - def delete(self, url, *args, **kwargs): - logger = kwargs.pop('passerelle_logger') - logger.debug('cmislib DELETE request to %s', url) - resp, content = super().delete(url, *args, **kwargs) - logger.debug('cmislib DELETE response (%s)', resp['status'], extra={'response': content.decode()}) - return resp, content - - def post(self, url, payload, *args, **kwargs): - logger = kwargs.pop('passerelle_logger') - logger.debug('cmislib POST request to %s', url, extra={'payload': payload.decode()}) - resp, content = super().post(url, payload, *args, **kwargs) - logger.debug('cmislib POST response (%s)', resp['status'], extra={'response': content.decode()}) - return resp, content - - def put(self, url, payload, *args, **kwargs): - logger = kwargs.pop('passerelle_logger') - logger.debug('cmislib PUT request to %s', url, payload, extra={'payload': payload.decode()}) - resp, content = super().put(url, *args, **kwargs) - logger.debug('cmislib PUT response (%s)', resp['status'], extra={'response': content.decode()}) - return resp, content - - binding.Rest = RESTService - - -class CmisAppConfig(AppConfig): - name = 'passerelle.apps.cmis' - - def ready(self): - add_logging_to_cmislib() diff --git a/passerelle/apps/cmis/models.py b/passerelle/apps/cmis/models.py index d4b61b27..c0a1282e 100644 --- a/passerelle/apps/cmis/models.py +++ b/passerelle/apps/cmis/models.py @@ -34,6 +34,7 @@ from cmislib.exceptions import ( from django.db import models from django.http import HttpResponse from django.utils.functional import cached_property +from django.utils.http import urlencode from django.utils.translation import gettext_lazy as _ from passerelle.base.models import BaseResource @@ -147,7 +148,14 @@ class CmisConnector(BaseResource): @contextmanager def get_cmis_gateway(self): with ignore_loggers('cmislib', 'cmislib.atompub.binding'): - yield CMISGateway(self.cmis_endpoint, self.username, self.password, self.logger) + import cmislib.atompub.binding as atompub_binding + + old_Rest = atompub_binding.Rest + atompub_binding.Rest = lambda: RESTService(self) + try: + yield CMISGateway(self.cmis_endpoint, self.username, self.password, self.logger) + finally: + atompub_binding.Rest = old_Rest def _validate_inputs(self, data): """process dict @@ -236,7 +244,7 @@ def wrap_cmis_error(f): class CMISGateway: def __init__(self, cmis_endpoint, username, password, logger): - self._cmis_client = CmisClient(cmis_endpoint, username, password, passerelle_logger=logger) + self._cmis_client = CmisClient(cmis_endpoint, username, password) self._logger = logger @cached_property @@ -284,3 +292,41 @@ class CMISGateway: @wrap_cmis_error def get_object(self, object_id): return self.repo.getObject(object_id) + + +# Mock API from cmilib.net.RESTService +class RESTService: + def __init__(self, resource): + self.resource = resource + + def request(self, method, url, username, password, body=None, content_type=None, **kwargs): + if username or password: + auth = (username, password) + else: + auth = None + + headers = kwargs.pop('headers', {}) + + if kwargs: + url = url + ('&' if '?' in url else '?') + urlencode(kwargs) + + if content_type: + headers['Content-Type'] = content_type + + response = self.resource.requests.request( + method=method, url=url, auth=auth, headers=headers, data=body + ) + + return {'status': str(response.status_code)}, response.content + + def get(self, url, username=None, password=None, **kwargs): + return self.request('GET', url, username, password, **kwargs) + + def delete(self, url, username=None, password=None, **kwargs): + return self.request('DELETE', url, username, password, **kwargs) + + def put(self, url, payload, contentType, username=None, password=None, **kwargs): + return self.request('PUT', url, username, password, body=payload, content_type=contentType, **kwargs) + + def post(self, url, payload, contentType, username=None, password=None, **kwargs): + return self.request('POST', url, username, password, body=payload, content_type=contentType, **kwargs) diff --git a/tests/test_cmis.py b/tests/test_cmis.py index 03420e64..d42dc575 100644 --- a/tests/test_cmis.py +++ b/tests/test_cmis.py @@ -1,14 +1,28 @@ +# passerelle - uniform access to multiple data sources and services +# Copyright (C) 2023 Entr'ouvert +# +# This program is free software: you can redistribute it and/or modify it +# under the terms of the GNU Affero General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + import base64 import os import re -import xml.etree.ElementTree as ET from unittest import mock from unittest.mock import Mock, call -from urllib import error as urllib2 -import httplib2 import py import pytest +import responses from cmislib import CmisClient from cmislib.exceptions import ( CmisException, @@ -22,7 +36,7 @@ from django.urls import reverse from django.utils.encoding import force_bytes, force_str from passerelle.apps.cmis.models import CmisConnector -from passerelle.base.models import AccessRight, ApiUser, ResourceLog +from passerelle.base.models import AccessRight, ApiUser from tests.test_manager import login @@ -392,9 +406,6 @@ def test_create_doc(): @pytest.mark.parametrize( "cmis_exc,err_msg", [ - (httplib2.HttpLib2Error, "connection error"), - # FIXME used for cmslib 0.5 compat - (urllib2.URLError, "connection error"), (PermissionDeniedException, "permission denied"), (UpdateConflictException, "update conflict"), (InvalidArgumentException, "invalid property"), @@ -509,8 +520,8 @@ def test_cmis_types_view(setup, app, admin_user, monkeypatch): @pytest.mark.parametrize('debug', (False, True)) -@mock.patch('httplib2.Http.request') -def test_raw_uploadfile(mocked_request, app, setup, debug, caplog): +@responses.activate +def test_raw_uploadfile(app, setup, debug, caplog): """ Simulate the bellow bash query : $ http https://passerelle.dev.publik.love/cmis/ged/uploadfile \ file:='{"filename": "test2", "content": "c2FsdXQK"}' path=/test-eo @@ -525,46 +536,21 @@ def test_raw_uploadfile(mocked_request, app, setup, debug, caplog): if debug: setup.set_log_level('DEBUG') - def cmis_mocked_request(uri, method="GET", body=None, **kwargs): - """simulate the 3 (ordered) HTTP queries involved""" - response = {'status': '200'} - if method == 'GET' and uri == 'http://example.com/cmisatom': - with open('%s/tests/data/cmis/cmis1.out.xml' % os.getcwd(), 'rb') as fd: - content = fd.read() - elif method == 'GET' and uri == ( - 'http://example.com/cmisatom/test/path?path=/test-eo&filter=&includeAllowableActions=false&includeACL=false&' - 'includePolicyIds=false&includeRelationships=&renditionFilter=' - ): - with open('%s/tests/data/cmis/cmis2.out.xml' % os.getcwd(), 'rb') as fd: - content = fd.read() - elif method == 'POST' and uri == 'http://example.com/cmisatom/test/children?id=L3Rlc3QtZW8%3D': - with open('%s/tests/data/cmis/cmis3.in.xml' % os.getcwd()) as fd: - expected_input = fd.read() - expected_input = expected_input.replace('\n', '') - expected_input = re.sub('> *<', '><', expected_input) - input1 = ET.tostring(ET.XML(expected_input)) + with open('%s/tests/data/cmis/cmis1.out.xml' % os.getcwd(), 'rb') as fd: + cmis1_body = fd.read() - # reorder properties - input2 = ET.XML(body) - objects = input2.find('{http://docs.oasis-open.org/ns/cmis/restatom/200908/}object') - properties = objects.find('{http://docs.oasis-open.org/ns/cmis/core/200908/}properties') - data = [] - for elem in properties: - key = elem.tag - data.append((key, elem)) - data.sort() - properties[:] = [item[-1] for item in data] - input2 = ET.tostring(input2) + with open('%s/tests/data/cmis/cmis2.out.xml' % os.getcwd(), 'rb') as fd: + cmis2_body = fd.read() - if input1 != input2: - raise Exception('expect [[%s]] but get [[%s]]' % (body, expected_input)) - with open('%s/tests/data/cmis/cmis3.out.xml' % os.getcwd(), 'rb') as fd: - content = fd.read() - else: - raise Exception('my fault error, url is not yet mocked: %s' % uri) - return (response, content) + with open('%s/tests/data/cmis/cmis3.out.xml' % os.getcwd(), 'rb') as fd: + cmis3_body = fd.read() + + responses.add(responses.GET, 'http://example.com/cmisatom', body=cmis1_body, status=200) + + responses.add(responses.GET, 'http://example.com/cmisatom/test/path', body=cmis2_body, status=200) + + responses.add(responses.POST, 'http://example.com/cmisatom/test/children', body=cmis3_body, status=200) - mocked_request.side_effect = cmis_mocked_request params = { "path": path, "file": {"filename": file_name, "content": b64encode(file_content), "content_type": "image/jpeg"}, @@ -575,22 +561,6 @@ def test_raw_uploadfile(mocked_request, app, setup, debug, caplog): assert json_result['data']['properties']['cmis:objectTypeId'] == "cmis:document" assert json_result['data']['properties']['cmis:name'] == file_name - if not debug: - assert ResourceLog.objects.count() == 2 - else: - assert ResourceLog.objects.count() == 11 - logs = list(ResourceLog.objects.all()) - assert logs[3].message == 'cmislib GET request to http://example.com/cmisatom' - assert logs[4].message == 'cmislib GET response (200)' - assert logs[4].extra['response'].startswith('