diff --git a/designate/api/v2/controllers/zones/tasks/transfer_requests.py b/designate/api/v2/controllers/zones/tasks/transfer_requests.py index 9308b2af..0522a41a 100644 --- a/designate/api/v2/controllers/zones/tasks/transfer_requests.py +++ b/designate/api/v2/controllers/zones/tasks/transfer_requests.py @@ -13,11 +13,11 @@ # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. -import six import pecan from oslo_log import log as logging from designate import utils +from designate import exceptions from designate.api.v2.controllers import rest from designate.objects import ZoneTransferRequest from designate.objects.adapters import DesignateAdapter @@ -72,12 +72,8 @@ class TransferRequestsController(rest.RestController): context = request.environ['context'] try: body = request.body_dict - except Exception as e: - if six.text_type(e) != 'TODO: Unsupported Content Type': - raise - else: - # Got a blank body - body = dict() + except exceptions.EmptyRequestBody: + body = dict() body['zone_id'] = zone_id diff --git a/designate/api/v2/patches.py b/designate/api/v2/patches.py index 867eb266..cf52710b 100644 --- a/designate/api/v2/patches.py +++ b/designate/api/v2/patches.py @@ -18,12 +18,13 @@ from inspect import getargspec import six from oslo_serialization import jsonutils +from oslo_log import log as logging import pecan.core from designate import exceptions - JSON_TYPES = ('application/json', 'application/json-patch+json') +LOG = logging.getLogger(__name__) class Request(pecan.core.Request): @@ -34,13 +35,16 @@ class Request(pecan.core.Request): Content-Type header. We add this method to ease future XML support, so the main code - is not hardcoded to call pecans "request.json()" method. + is not hardcoded to call pecans "request.json" method. """ if self.content_type in JSON_TYPES: try: return jsonutils.load(self.body_file) except ValueError as valueError: - raise exceptions.InvalidJson(six.text_type(valueError)) + if len(self.body) == 0: + raise exceptions.EmptyRequestBody('Request Body is empty') + else: + raise exceptions.InvalidJson(six.text_type(valueError)) else: raise exceptions.UnsupportedContentType( 'Content-type must be application/json') diff --git a/designate/exceptions.py b/designate/exceptions.py index 381ebd55..98ad1c42 100644 --- a/designate/exceptions.py +++ b/designate/exceptions.py @@ -110,6 +110,11 @@ class BadRequest(Base): expected = True +class EmptyRequestBody(BadRequest): + error_type = 'empty_request_body' + expected = True + + class InvalidUUID(BadRequest): error_type = 'invalid_uuid' diff --git a/functionaltests/api/v2/clients/transfer_accepts_client.py b/functionaltests/api/v2/clients/transfer_accepts_client.py new file mode 100644 index 00000000..ab1d16ef --- /dev/null +++ b/functionaltests/api/v2/clients/transfer_accepts_client.py @@ -0,0 +1,66 @@ +""" +Copyright 2015 Rackspace + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" + +from functionaltests.api.v2.models.transfer_accepts_model import \ + TransferAcceptsModel +from functionaltests.api.v2.models.transfer_accepts_model import \ + TransferAcceptsListModel +from functionaltests.common.client import ClientMixin + + +class TransferAcceptClient(ClientMixin): + + @classmethod + def transfer_accepts_uri(cls, filters=None): + url = "/v2/zones/tasks/transfer_accepts" + if filters: + url = cls.add_filters(url, filters) + return url + + @classmethod + def transfer_accept_uri(cls, transfer_request_id): + return "/v2/zones/tasks/transfer_accepts/{1}".format( + transfer_request_id) + + def list_transfer_accepts(self, zone_id, filters=None, **kwargs): + resp, body = self.client.get( + self.transfer_accepts_uri(filters), **kwargs) + return self.deserialize(resp, body, TransferAcceptsListModel) + + def get_transfer_accept(self, zone_id, transfer_request_id, **kwargs): + resp, body = self.client.get(self.transfer_accept_uri( + transfer_request_id), + **kwargs) + return self.deserialize(resp, body, TransferAcceptsModel) + + def post_transfer_accept(self, transfer_request_model, **kwargs): + resp, body = self.client.post( + self.transfer_accepts_uri(), + body=transfer_request_model.to_json(), + **kwargs) + return self.deserialize(resp, body, TransferAcceptsModel) + + def put_transfer_accept(self, zone_id, transfer_request_id, + transfer_request_model, **kwargs): + resp, body = self.client.put(self.transfer_accept_uri( + transfer_request_id), + body=transfer_request_model.to_json(), **kwargs) + return self.deserialize(resp, body, TransferAcceptsModel) + + def delete_transfer_accept(self, zone_id, transfer_request_id, **kwargs): + resp, body = self.client.delete( + self.transfer_accept_uri(zone_id, transfer_request_id), **kwargs) + return self.deserialize(resp, body, TransferAcceptsModel) diff --git a/functionaltests/api/v2/clients/transfer_requests_client.py b/functionaltests/api/v2/clients/transfer_requests_client.py new file mode 100644 index 00000000..e8e47c95 --- /dev/null +++ b/functionaltests/api/v2/clients/transfer_requests_client.py @@ -0,0 +1,81 @@ +""" +Copyright 2015 Rackspace + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" + +from functionaltests.api.v2.models.transfer_requests_model import \ + TransferRequestsModel +from functionaltests.api.v2.models.transfer_requests_model import \ + TransferRequestsListModel +from functionaltests.common.client import ClientMixin + + +class TransferRequestClient(ClientMixin): + + @classmethod + def create_transfer_requests_uri(cls, zone_id, filters=None): + url = "/v2/zones/{0}/tasks/transfer_requests".format(zone_id) + if filters: + url = cls.add_filters(url, filters) + return url + + @classmethod + def transfer_requests_uri(cls, filters=None): + url = "/v2/zones/tasks/transfer_requests" + if filters: + url = cls.add_filters(url, filters) + return url + + @classmethod + def transfer_request_uri(cls, transfer_request_id): + return "/v2/zones/tasks/transfer_requests/{0}".format( + transfer_request_id) + + def list_transfer_requests(self, filters=None, **kwargs): + resp, body = self.client.get( + self.transfer_requests_uri(filters), **kwargs) + return self.deserialize(resp, body, TransferRequestsListModel) + + def get_transfer_request(self, transfer_request_id, **kwargs): + resp, body = self.client.get(self.transfer_request_uri( + transfer_request_id), + **kwargs) + return self.deserialize(resp, body, TransferRequestsModel) + + def post_transfer_request(self, zone_id, transfer_request_model=None, + **kwargs): + resp, body = self.client.post( + self.create_transfer_requests_uri(zone_id), + body=transfer_request_model.to_json(), + **kwargs) + return self.deserialize(resp, body, TransferRequestsModel) + + def post_transfer_request_empty_body(self, zone_id, **kwargs): + resp, body = self.client.post( + self.create_transfer_requests_uri(zone_id), + body=None, + **kwargs) + return self.deserialize(resp, body, TransferRequestsModel) + + def put_transfer_request(self, transfer_request_id, + transfer_request_model, **kwargs): + resp, body = self.client.put(self.transfer_request_uri( + transfer_request_id), + body=transfer_request_model.to_json(), **kwargs) + return self.deserialize(resp, body, TransferRequestsModel) + + def delete_transfer_request(self, transfer_request_id, **kwargs): + resp, body = self.client.delete( + self.transfer_request_uri(transfer_request_id), **kwargs) + return self.deserialize(resp, body, TransferRequestsModel) diff --git a/functionaltests/api/v2/models/transfer_accepts_model.py b/functionaltests/api/v2/models/transfer_accepts_model.py new file mode 100644 index 00000000..ca5211ac --- /dev/null +++ b/functionaltests/api/v2/models/transfer_accepts_model.py @@ -0,0 +1,33 @@ +# Copyright 2015 Hewlett-Packard Development Company, L.P. +# +# Author: Endre Karlson +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from functionaltests.common.models import BaseModel +from functionaltests.common.models import CollectionModel +from functionaltests.common.models import EntityModel + + +class TransferAcceptsData(BaseModel): + pass + + +class TransferAcceptsModel(EntityModel): + ENTITY_NAME = 'transfer_accept' + MODEL_TYPE = TransferAcceptsData + + +class TransferAcceptsListModel(CollectionModel): + COLLECTION_NAME = 'transfer_accepts' + MODEL_TYPE = TransferAcceptsData diff --git a/functionaltests/api/v2/models/transfer_requests_model.py b/functionaltests/api/v2/models/transfer_requests_model.py new file mode 100644 index 00000000..648654fe --- /dev/null +++ b/functionaltests/api/v2/models/transfer_requests_model.py @@ -0,0 +1,33 @@ +# Copyright 2015 Hewlett-Packard Development Company, L.P. +# +# Author: Endre Karlson +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from functionaltests.common.models import BaseModel +from functionaltests.common.models import CollectionModel +from functionaltests.common.models import EntityModel + + +class TransferRequestsData(BaseModel): + pass + + +class TransferRequestsModel(EntityModel): + ENTITY_NAME = 'transfer_request' + MODEL_TYPE = TransferRequestsData + + +class TransferRequestsListModel(CollectionModel): + COLLECTION_NAME = 'transfer_requests' + MODEL_TYPE = TransferRequestsData diff --git a/functionaltests/api/v2/test_zone_ownership_transfers.py b/functionaltests/api/v2/test_zone_ownership_transfers.py new file mode 100644 index 00000000..f9dffbe2 --- /dev/null +++ b/functionaltests/api/v2/test_zone_ownership_transfers.py @@ -0,0 +1,149 @@ +""" +Copyright 2015 Rackspace + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" +from tempest_lib import exceptions + +from functionaltests.common import datagen +from functionaltests.common import utils +from functionaltests.api.v2.base import DesignateV2Test +from functionaltests.api.v2.clients.transfer_requests_client import \ + TransferRequestClient +from functionaltests.api.v2.clients.transfer_accepts_client import \ + TransferAcceptClient +from functionaltests.api.v2.clients.zone_client import ZoneClient + + +@utils.parameterized_class +class TransferZoneOwnerShipTest(DesignateV2Test): + + def setUp(self): + super(TransferZoneOwnerShipTest, self).setUp() + self.increase_quotas(user='default') + self.increase_quotas(user='alt') + resp, self.zone = ZoneClient.as_user('default').post_zone( + datagen.random_zone_data()) + ZoneClient.as_user('default').wait_for_zone(self.zone.id) + + def test_list_transfer_requests(self): + resp, model = TransferRequestClient.as_user('default') \ + .list_transfer_requests() + self.assertEqual(resp.status, 200) + + def test_create_zone_transfer_request(self): + resp, transfer_request = TransferRequestClient.as_user('default')\ + .post_transfer_request(self.zone.id, + datagen.random_transfer_request_data()) + self.assertEqual(resp.status, 201) + + def test_view_zone_transfer_request(self): + resp, transfer_request = TransferRequestClient.as_user('default')\ + .post_transfer_request(self.zone.id, + datagen.random_transfer_request_data()) + self.assertEqual(resp.status, 201) + + resp, transfer_request = TransferRequestClient.as_user('alt')\ + .get_transfer_request(transfer_request.id) + + self.assertEqual(resp.status, 200) + self.assertEqual(getattr(transfer_request, 'key', None), None) + + def test_create_zone_transfer_request_scoped(self): + + target_project_id = TransferRequestClient.as_user('alt').tenant_id + + resp, transfer_request = TransferRequestClient.as_user('default')\ + .post_transfer_request(self.zone.id, + datagen.random_transfer_request_data( + target_project_id=target_project_id)) + self.assertEqual(resp.status, 201) + self.assertEqual(transfer_request.target_project_id, target_project_id) + + resp, transfer_request = TransferRequestClient.as_user('alt')\ + .get_transfer_request(transfer_request.id) + + self.assertEqual(resp.status, 200) + + def test_view_zone_transfer_request_scoped(self): + target_project_id = TransferRequestClient.as_user('admin').tenant_id + + resp, transfer_request = TransferRequestClient.as_user('default')\ + .post_transfer_request(self.zone.id, + datagen.random_transfer_request_data( + target_project_id=target_project_id)) + self.assertEqual(resp.status, 201) + self.assertEqual(transfer_request.target_project_id, target_project_id) + + self._assert_exception( + exceptions.NotFound, 'zone_transfer_request_not_found', 404, + TransferRequestClient.as_user('alt').get_transfer_request, + self.zone.id) + + resp, transfer_request = TransferRequestClient.as_user('admin')\ + .get_transfer_request(transfer_request.id) + + self.assertEqual(resp.status, 200) + + def test_create_zone_transfer_request_no_body(self): + resp, transfer_request = TransferRequestClient.as_user('default')\ + .post_transfer_request_empty_body(self.zone.id) + self.assertEqual(resp.status, 201) + + def test_do_zone_transfer(self): + resp, transfer_request = TransferRequestClient.as_user('default')\ + .post_transfer_request(self.zone.id, + datagen.random_transfer_request_data()) + self.assertEqual(resp.status, 201) + + resp, transfer_accept = TransferAcceptClient.as_user('alt')\ + .post_transfer_accept( + datagen.random_transfer_accept_data( + key=transfer_request.key, + zone_transfer_request_id=transfer_request.id + )) + self.assertEqual(resp.status, 201) + + def test_do_zone_transfer_scoped(self): + + target_project_id = TransferRequestClient.as_user('alt').tenant_id + + resp, transfer_request = TransferRequestClient.as_user('default')\ + .post_transfer_request(self.zone.id, + datagen.random_transfer_request_data( + target_project_id=target_project_id)) + + self.assertEqual(resp.status, 201) + + resp, retrived_transfer_request = TransferRequestClient.\ + as_user('alt').get_transfer_request(transfer_request.id) + + self.assertEqual(resp.status, 200) + + resp, transfer_accept = TransferAcceptClient.as_user('alt')\ + .post_transfer_accept( + datagen.random_transfer_accept_data( + key=transfer_request.key, + zone_transfer_request_id=transfer_request.id + )) + self.assertEqual(resp.status, 201) + + client = ZoneClient.as_user('default') + + self._assert_exception( + exceptions.NotFound, 'domain_not_found', 404, + client.get_zone, self.zone.id) + + resp, zone = ZoneClient.as_user('alt').get_zone(self.zone.id) + + self.assertEqual(resp.status, 200) diff --git a/functionaltests/common/datagen.py b/functionaltests/common/datagen.py index 9551cadd..9987eb0e 100644 --- a/functionaltests/common/datagen.py +++ b/functionaltests/common/datagen.py @@ -13,11 +13,15 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. """ - +import uuid import random from functionaltests.api.v2.models.blacklist_model import BlacklistModel from functionaltests.api.v2.models.pool_model import PoolModel +from functionaltests.api.v2.models.transfer_requests_model import \ + TransferRequestsModel +from functionaltests.api.v2.models.transfer_accepts_model import \ + TransferAcceptsModel from functionaltests.api.v2.models.recordset_model import RecordsetModel from functionaltests.api.v2.models.zone_model import ZoneModel @@ -33,6 +37,10 @@ def random_ipv6(): return result.replace("0000", "0") +def random_uuid(): + return uuid.uuid4() + + def random_string(prefix='rand', n=8, suffix=''): """Return a string containing random digits @@ -64,6 +72,37 @@ def random_zone_data(name=None, email=None, ttl=None, description=None): 'description': description}) +def random_transfer_request_data(description=None, target_project_id=None): + """Generate random zone data, with optional overrides + + :return: A TransferRequestModel + """ + + data = {} + + if description is None: + data['description'] = random_string(prefix='Description ') + + if target_project_id: + data['target_project_id'] = target_project_id + + return TransferRequestsModel.from_dict(data) + + +def random_transfer_accept_data(key=None, zone_transfer_request_id=None): + """Generate random zone data, with optional overrides + + :return: A TransferRequestModel + """ + if key is None: + key = random_string() + if zone_transfer_request_id is None: + zone_transfer_request_id = random_uuid() + return TransferAcceptsModel.from_dict({ + 'key': key, + 'zone_transfer_request_id': zone_transfer_request_id}) + + def random_recordset_data(record_type, zone_name, name=None, records=None, ttl=None): """Generate random recordset data, with optional overrides