diff --git a/setup.py b/setup.py index 2eb5620..412ec6f 100644 --- a/setup.py +++ b/setup.py @@ -15,7 +15,7 @@ REQUIREMENTS = [ 'waitress', 'cliquet[postgresql,monitoring]', - 'fxsync-client>=0.0.1' + 'syncclient' ] if PY2: @@ -30,11 +30,6 @@ 'main = syncto:main', ]} -DEPENDENCY_LINKS = [ - "https://github.com/mozilla-services/syncclient/tarball/master" - "#egg=fxsync-client-0.0.1" -] - setup(name='syncto', version='1.0.0.dev0', description='Read Firefox Sync server using Kinto API.', @@ -54,5 +49,4 @@ include_package_data=True, zip_safe=False, install_requires=REQUIREMENTS, - entry_points=ENTRY_POINTS, - dependency_links=DEPENDENCY_LINKS) + entry_points=ENTRY_POINTS) diff --git a/syncto/authentication.py b/syncto/authentication.py index 674502b..70ba0d9 100644 --- a/syncto/authentication.py +++ b/syncto/authentication.py @@ -1,10 +1,8 @@ from pyramid import httpexceptions from pyramid.security import forget -from requests.exceptions import HTTPError from cliquet.errors import http_error, ERRORS -from cliquet.views.errors import service_unavailable -from sync.client import SyncClient +from syncclient.client import SyncClient from syncto import AUTHORIZATION_HEADER, CLIENT_STATE_HEADER @@ -40,19 +38,5 @@ def build_sync_client(request): bid_assertion = authorization_header.split(" ", 1)[1] client_state = request.headers[CLIENT_STATE_HEADER] - try: - sync_client = SyncClient(bid_assertion, client_state) - except HTTPError as e: - if e.response.status_code in (400, 401): - message = '%s %s: %s' % (e.response.status_code, - e.response.reason, - e.response.text) - response = http_error(httpexceptions.HTTPUnauthorized(), - errno=ERRORS.INVALID_AUTH_TOKEN, - message=message) - # Forget the current user credentials. - response.headers.extend(forget(request)) - raise response - else: - raise service_unavailable(e, request) + sync_client = SyncClient(bid_assertion, client_state) return sync_client diff --git a/syncto/tests/test_functional.py b/syncto/tests/test_functional.py index fae7fb5..7a6e4ea 100644 --- a/syncto/tests/test_functional.py +++ b/syncto/tests/test_functional.py @@ -184,3 +184,35 @@ def setUp(self): def test_record_handle_cors_headers(self): resp = self.app.get(RECORD_URL, headers=self.headers, status=200) self.assertIn('Access-Control-Allow-Origin', resp.headers) + + def test_can_delete_record(self): + self.sync_client.return_value.delete_record.return_value = None + self.app.delete(RECORD_URL, headers=self.headers, status=204) + + def test_delete_return_a_503_in_case_of_unknown_error(self): + response = mock.MagicMock() + response.status_code = 500 + self.sync_client.return_value.delete_record.side_effect = HTTPError( + response=response) + self.app.delete(RECORD_URL, headers=self.headers, status=503) + + def test_delete_return_a_400_in_case_of_bad_request(self): + response = mock.MagicMock() + response.status_code = 400 + self.sync_client.return_value.delete_record.side_effect = HTTPError( + response=response) + self.app.delete(RECORD_URL, headers=self.headers, status=400) + + def test_delete_return_a_403_in_case_of_forbidden_resource(self): + response = mock.MagicMock() + response.status_code = 403 + self.sync_client.return_value.delete_record.side_effect = HTTPError( + response=response) + self.app.delete(RECORD_URL, headers=self.headers, status=403) + + def test_delete_return_a_404_in_case_of_unknown_resource(self): + response = mock.MagicMock() + response.status_code = 404 + self.sync_client.return_value.delete_record.side_effect = HTTPError( + response=response) + self.app.delete(RECORD_URL, headers=self.headers, status=404) diff --git a/syncto/views/errors.py b/syncto/views/errors.py new file mode 100644 index 0000000..4b88ce2 --- /dev/null +++ b/syncto/views/errors.py @@ -0,0 +1,41 @@ +from pyramid import httpexceptions +from pyramid.security import NO_PERMISSION_REQUIRED, forget +from pyramid.view import view_config +from requests.exceptions import HTTPError + +from cliquet import logger +from cliquet.errors import http_error, ERRORS +from cliquet.utils import reapply_cors +from cliquet.views.errors import service_unavailable + + +@view_config(context=HTTPError, permission=NO_PERMISSION_REQUIRED) +def error(context, request): + """Catch server errors and trace them.""" + logger.error(context, exc_info=True) + + message = '%s %s: %s' % (context.response.status_code, + context.response.reason, + context.response.text) + if context.response.status_code == 400: + response = http_error(httpexceptions.HTTPBadRequest(), + errno=ERRORS.INVALID_PARAMETERS, + message=message) + elif context.response.status_code == 401: + response = http_error(httpexceptions.HTTPUnauthorized(), + errno=ERRORS.INVALID_AUTH_TOKEN, + message=message) + # Forget the current user credentials. + response.headers.extend(forget(request)) + elif context.response.status_code == 403: + response = http_error(httpexceptions.HTTPForbidden(), + errno=ERRORS.FORBIDDEN, + message=message) + elif context.response.status_code == 404: + response = http_error(httpexceptions.HTTPNotFound(), + errno=ERRORS.INVALID_RESOURCE_ID, + message=message) + else: + response = service_unavailable(context, request) + + return reapply_cors(request, response) diff --git a/syncto/views/record.py b/syncto/views/record.py index d603a52..15caef5 100644 --- a/syncto/views/record.py +++ b/syncto/views/record.py @@ -30,3 +30,17 @@ def record_get(request): convert_headers(sync_client.raw_resp, request.response) return {'data': record} + + +@record.delete(permission=NO_PERMISSION_REQUIRED) +def record_delete(request): + collection_name = request.matchdict['collection_name'] + record_id = request.matchdict['record_id'] + sync_id = uuid4_to_base64(record_id) + + sync_client = build_sync_client(request) + sync_client.delete_record(collection_name, sync_id) + + request.response.status_code = 204 + del request.response.headers['Content-Type'] + return request.response