Add Swift REST API

Adding the REST API needed to support the new angular Swift UI.

Co-Author: Neill Cox <neill@ingenious.com.au>
Change-Id: Ife1073cf6aa481bdbd89f09805ac76fe7106d5df
Partially-Implements: blueprint angularize-swift
This commit is contained in:
Richard Jones 2015-12-11 10:39:26 +11:00
parent dea5b2878e
commit 672b6ae003
12 changed files with 1014 additions and 18 deletions

View File

@ -31,3 +31,4 @@ from . import network # noqa
from . import neutron # noqa
from . import nova # noqa
from . import policy # noqa
from . import swift # noqa

View File

@ -0,0 +1,224 @@
# Copyright 2015, Rackspace, US, Inc.
#
# 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.
"""API for the swift service.
"""
from django import forms
from django.views.decorators.csrf import csrf_exempt
from django.views import generic
from openstack_dashboard import api
from openstack_dashboard.api.rest import urls
from openstack_dashboard.api.rest import utils as rest_utils
@urls.register
class Info(generic.View):
"""API for information about the Swift installation.
"""
url_regex = r'swift/info/$'
@rest_utils.ajax()
def get(self, request):
"""Get information about the Swift installation.
"""
capabilities = api.swift.swift_get_capabilities(request)
return {'info': capabilities}
@urls.register
class Containers(generic.View):
"""API for swift container listing for an account
"""
url_regex = r'swift/containers/$'
@rest_utils.ajax()
def get(self, request):
"""Get the list of containers for this account
TODO(neillc): Add pagination
"""
containers, has_more = api.swift.swift_get_containers(request)
containers = [container.to_dict() for container in containers]
return {'items': containers, 'has_more': has_more}
@urls.register
class Container(generic.View):
"""API for swift container level information
"""
url_regex = r'swift/containers/(?P<container>[^/]+)/metadata/$'
@rest_utils.ajax()
def get(self, request, container):
"""Get the container details
"""
return api.swift.swift_get_container(request, container).to_dict()
@rest_utils.ajax()
def post(self, request, container):
metadata = {}
if 'is_public' in request.DATA:
metadata['is_public'] = request.DATA['is_public']
api.swift.swift_create_container(request, container, metadata=metadata)
return rest_utils.CreatedResponse(
u'/api/swift/containers/%s' % container,
)
@rest_utils.ajax()
def delete(self, request, container):
api.swift.swift_delete_container(request, container)
@rest_utils.ajax(data_required=True)
def put(self, request, container):
metadata = {'is_public': request.DATA['is_public']}
api.swift.swift_update_container(request, container, metadata=metadata)
@urls.register
class Objects(generic.View):
"""API for a list of swift objects
"""
url_regex = r'swift/containers/(?P<container>[^/]+)/objects/$'
@rest_utils.ajax()
def get(self, request, container):
"""Get object information.
:param request:
:param container:
:return:
"""
path = request.GET.get('path')
objects = api.swift.swift_get_objects(
request,
container,
prefix=path
)
# filter out the folder from the listing if we're filtering for
# contents of a (pseudo) folder
contents = [{
'path': o.name,
'name': o.name.split('/')[-1],
'bytes': o.bytes,
'is_subdir': o.content_type == 'application/pseudo-folder',
'is_object': o.content_type != 'application/pseudo-folder',
'content_type': o.content_type
} for o in objects[0] if o.name != path]
return {'items': contents}
class UploadObjectForm(forms.Form):
file = forms.FileField(required=False)
@urls.register
class Object(generic.View):
"""API for a single swift object or pseudo-folder
"""
url_regex = r'swift/containers/(?P<container>[^/]+)/object/' \
'(?P<object_name>.+)$'
# note: not an AJAX request - the body will be raw file content
@csrf_exempt
def post(self, request, container, object_name):
"""Create a new object or pseudo-folder
:param request:
:param container:
:param object_name:
If the object_name (ie. POST path) ends in a '/' then a folder is
created, rather than an object. Any file content passed along with
the request will be ignored in that case.
POST parameter:
:param file: the file data for the upload.
:return:
"""
form = UploadObjectForm(request.POST, request.FILES)
if not form.is_valid():
raise rest_utils.AjaxError(500, 'Invalid request')
data = form.clean()
if object_name[-1] == '/':
result = api.swift.swift_create_pseudo_folder(
request,
container,
object_name
)
else:
result = api.swift.swift_upload_object(
request,
container,
object_name,
data['file']
)
return rest_utils.CreatedResponse(
u'/api/swift/containers/%s/object/%s' % (container, result.name)
)
@rest_utils.ajax()
def delete(self, request, container, object_name):
api.swift.swift_delete_object(request, container, object_name)
@urls.register
class ObjectMetadata(generic.View):
"""API for a single swift object
"""
url_regex = r'swift/containers/(?P<container>[^/]+)/metadata/' \
'(?P<object_name>.+)$'
@rest_utils.ajax()
def get(self, request, container, object_name):
return api.swift.swift_get_object(
request,
container_name=container,
object_name=object_name,
with_data=False
).to_dict()
@urls.register
class ObjectCopy(generic.View):
"""API to copy a swift object
"""
url_regex = r'swift/containers/(?P<container>[^/]+)/copy/' \
'(?P<object_name>.+)$'
@rest_utils.ajax()
def post(self, request, container, object_name):
dest_container = request.DATA['dest_container']
dest_name = request.DATA['dest_name']
result = api.swift.swift_copy_object(
request,
container,
object_name,
dest_container,
dest_name
)
return rest_utils.CreatedResponse(
u'/api/swift/containers/%s/object/%s' % (dest_container,
result.name)
)

View File

@ -131,10 +131,10 @@ def ajax(authenticated=True, data_required=False,
return JSONResponse(data, json_encoder=json_encoder)
except http_errors as e:
# exception was raised with a specific HTTP status
if hasattr(e, 'http_status'):
http_status = e.http_status
elif hasattr(e, 'code'):
http_status = e.code
for attr in ['http_status', 'code', 'status_code']:
if hasattr(e, attr):
http_status = getattr(e, attr)
break
else:
log.exception('HTTP exception with no status/code')
return JSONResponse(str(e), 500)

View File

@ -356,3 +356,7 @@ def swift_get_object(request, container_name, object_name, with_data=True,
container_name,
orig_name=orig_name,
data=data)
def swift_get_capabilities(request):
return swift_api(request).get_capabilities()

View File

@ -358,11 +358,19 @@ class SwiftTests(test.TestCase):
self.assertNotContains(res, INVALID_CONTAINER_NAME_1)
self.assertNotContains(res, INVALID_CONTAINER_NAME_2)
# Check that the returned Content-Disposition filename is well
# surrounded by double quotes and with commas removed
content = res.get('Content-Disposition')
expected_name = '"%s"' % obj.name.replace(',', '')
# Check that the returned Content-Disposition filename is
# correct - some have commas which must be removed
expected_name = obj.name.replace(',', '')
# some have a path which must be removed
if '/' in expected_name:
expected_name = expected_name.split('/')[-1]
# There will also be surrounding double quotes
expected_name = '"' + expected_name + '"'
expected = 'attachment; filename=%s' % expected_name
content = res.get('Content-Disposition')
if six.PY3:
header = email.header.decode_header(content)
@ -417,7 +425,7 @@ class SwiftTests(test.TestCase):
@test.create_stubs({api.swift: ('swift_get_containers',
'swift_copy_object')})
def test_copy_get(self):
original_name = u"test.txt"
original_name = u"test folder%\u6346/test.txt"
copy_name = u"test.copy.txt"
container = self.containers.first()
obj = self.objects.get(name=original_name)

View File

@ -70,7 +70,9 @@
// Checks to ensure we call the api service with the appropriate
// parameters.
if (angular.isDefined(config.data)) {
if (angular.isDefined(config.call_args)) {
expect(apiService[config.method].calls.mostRecent().args).toEqual(config.call_args);
} else if (angular.isDefined(config.data)) {
expect(apiService[config.method]).toHaveBeenCalledWith(config.path, config.data);
} else {
expect(apiService[config.method]).toHaveBeenCalledWith(config.path);

View File

@ -0,0 +1,283 @@
/**
* Copyright 2015, Rackspace, US, Inc.
*
* 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.
*/
(function () {
'use strict';
angular
.module('horizon.app.core.openstack-service-api')
.factory('horizon.app.core.openstack-service-api.swift', swiftAPI);
swiftAPI.$inject = [
'horizon.framework.util.http.service',
'horizon.framework.widgets.toast.service'
];
/**
* @ngdoc service
* @name horizon.app.core.openstack-service-api.swift
* @description Provides direct pass through to Swift with NO abstraction.
*/
function swiftAPI(apiService, toastService) {
var service = {
copyObject: copyObject,
createContainer: createContainer,
createFolder: createFolder,
deleteContainer: deleteContainer,
deleteObject: deleteObject,
formData: formData,
getContainer: getContainer,
getContainers: getContainers,
getInfo: getInfo,
getContainerURL: getContainerURL,
getObjectDetails:getObjectDetails,
getObjects: getObjects,
getObjectURL: getObjectURL,
setContainerAccess: setContainerAccess,
uploadObject: uploadObject
};
return service;
// this exists solely so that we can mock FormData
function formData() {
return new FormData();
}
// internal use only
function getContainerURL(container) {
return '/api/swift/containers/' + encodeURIComponent(container);
}
/**
* @name horizon.app.core.openstack-service-api.swift.getObjectURL
* @description
* Calculate the download URL for an object.
*
*/
function getObjectURL(container, object, type) {
var urlType = type || 'object';
var objectUrl = encodeURIComponent(object).replace('%2F', '/');
return getContainerURL(container) + '/' + urlType + '/' + objectUrl;
}
/**
* @name horizon.app.core.openstack-service-api.swift.getInfo
* @description
* Lists the activated capabilities for this version of the OpenStack
* Object Storage API.
*
* The result is an object passed through from the Swift /info/ call.
*
*/
function getInfo() {
return apiService.get('/api/swift/info/')
.error(function () {
toastService.add('error', gettext('Unable to get the Swift service info.'));
});
}
/**
* @name horizon.app.core.openstack-service-api.swift.getContainers
* @description
* Get the list of containers for this account
*
* The result is an object with 'items' and 'has_more' flag.
*
*/
function getContainers() {
return apiService.get('/api/swift/containers/')
.error(function() {
toastService.add('error', gettext('Unable to get the Swift container listing.'));
});
}
/**
* @name horizon.app.core.openstack-service-api.swift.getContainer
* @description
* Get the container's detailed metadata
*
* The result is an object with the metadata fields.
*
*/
function getContainer(container) {
return apiService.get(service.getContainerURL(container) + '/metadata/')
.error(function() {
toastService.add('error', gettext('Unable to get the container details.'));
});
}
/**
* @name horizon.app.core.openstack-service-api.swift.createContainer
* @description
* Creates the named container with the is_public flag set to isPublic.
*
*/
function createContainer(container, isPublic) {
var data = {is_public: false};
if (isPublic) {
data.is_public = true;
}
return apiService.post(service.getContainerURL(container) + '/metadata/', data)
.error(function () {
toastService.add('error', gettext('Unable to create the container.'));
});
}
/**
* @name horizon.app.core.openstack-service-api.swift.deleteContainer
* @description
* Delete the named container.
*
*/
function deleteContainer(container) {
return apiService.delete(service.getContainerURL(container) + '/metadata/')
.error(function () {
toastService.add('error', gettext('Unable to delete the container.'));
});
}
/**
* @name horizon.app.core.openstack-service-api.swift.setContainerAccess
* @description
* Set the container's is_public flag.
*
*/
function setContainerAccess(container, isPublic) {
var data = {is_public: isPublic};
return apiService.put(service.getContainerURL(container) + '/metadata/', data)
.error(function () {
toastService.add('error', gettext('Unable to change the container access.'));
});
}
/**
* @name horizon.app.core.openstack-service-api.swift.getObjects
* @description
* Get a listing of the objects in the container, optionally
* limited to a specific folder.
*
* Use the params value "path" to specify a folder prefix to limit
* the fetch to a pseudo-folder.
*
*/
function getObjects(container, params) {
var options = {};
if (params) {
options.params = params;
}
return apiService.get(service.getContainerURL(container) + '/objects/', options)
.error(function () {
toastService.add('error', gettext('Unable to get the objects in container.'));
});
}
/**
* @name horizon.app.core.openstack-service-api.swift.uploadObject
* @description
* Add a file to the specified container with the given objectName (which
* may include pseudo-folder path), the mimetype and raw file data.
*
*/
function uploadObject(container, objectName, file) {
var fd = service.formData();
fd.append("file", file);
return apiService.post(
service.getObjectURL(container, objectName),
fd,
{
headers: {
'Content-Type': undefined
}
}
)
.error(function () {
toastService.add('error', gettext('Unable to upload the object.'));
});
}
/**
* @name horizon.app.core.openstack-service-api.swift.deleteObject
* @description
* Delete an object (or pseudo-folder).
*
*/
function deleteObject(container, objectName) {
return apiService.delete(
service.getObjectURL(container, objectName)
)
.error(function (response, status) {
if (status === 409) {
toastService.add('error', gettext(
'Unable to delete the folder because it is not empty.'
));
} else {
toastService.add('error', gettext('Unable to delete the object.'));
}
});
}
/**
* @name horizon.app.core.openstack-service-api.swift.getObjectDetails
* @description
* Get the metadata for an object.
*
*/
function getObjectDetails(container, objectName) {
return apiService.get(
service.getObjectURL(container, objectName, 'metadata')
)
.error(function () {
toastService.add('error', gettext('Unable to get details of the object.'));
});
}
/**
* @name horizon.app.core.openstack-service-api.swift.createFolder
* @description
* Create a pseudo-folder.
*
*/
function createFolder(container, folderName) {
return apiService.post(
service.getObjectURL(container, folderName) + '/',
{}
)
.error(function () {
toastService.add('error', gettext('Unable to create the folder.'));
});
}
/**
* @name horizon.app.core.openstack-service-api.swift.copyObject
* @description
* Copy an object.
*
*/
function copyObject(container, objectName, destContainer, destName) {
return apiService.post(
service.getObjectURL(container, objectName, 'copy'),
{dest_container: destContainer, dest_name: destName}
)
.error(function () {
toastService.add('error', gettext('Unable to copy the object.'));
});
}
}
}());

View File

@ -0,0 +1,220 @@
/*
* (c) Copyright 2015 Copyright 2015, Rackspace, US, Inc.
*
* 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.
*/
(function() {
'use strict';
describe('Swift API', function() {
var testCall, service;
var apiService = {};
var toastService = {};
var fakeFormData = {
append: angular.noop
};
beforeEach(
module('horizon.mock.openstack-service-api',
function($provide, initServices) {
testCall = initServices($provide, apiService, toastService);
})
);
beforeEach(module('horizon.app.core.openstack-service-api'));
beforeEach(inject(['horizon.app.core.openstack-service-api.swift', function(swiftAPI) {
service = swiftAPI;
spyOn(service, 'formData').and.returnValue(fakeFormData);
}]));
it('defines the service', function() {
expect(service).toBeDefined();
});
var tests = [
{
func: "getInfo",
method: "get",
path: "/api/swift/info/",
error: "Unable to get the Swift service info."
},
{
func: 'getContainers',
method: 'get',
path: '/api/swift/containers/',
error: 'Unable to get the Swift container listing.'
},
{
func: 'getContainer',
method: 'get',
path: '/api/swift/containers/spam/metadata/',
error: 'Unable to get the container details.',
testInput: [ 'spam' ]
},
{
func: 'createContainer',
method: 'post',
path: '/api/swift/containers/new-spam/metadata/',
data: {is_public: false},
error: 'Unable to create the container.',
testInput: [ 'new-spam' ]
},
{
func: 'createContainer',
method: 'post',
path: '/api/swift/containers/new-spam/metadata/',
data: {is_public: false},
error: 'Unable to create the container.',
testInput: [ 'new-spam', false ]
},
{
func: 'createContainer',
method: 'post',
path: '/api/swift/containers/new-spam/metadata/',
data: {is_public: true},
error: 'Unable to create the container.',
testInput: [ 'new-spam', true ]
},
{
func: 'deleteContainer',
method: 'delete',
path: '/api/swift/containers/spam/metadata/',
error: 'Unable to delete the container.',
testInput: [ 'spam' ]
},
{
func: 'setContainerAccess',
method: 'put',
data: {is_public: false},
path: '/api/swift/containers/spam/metadata/',
error: 'Unable to change the container access.',
testInput: [ 'spam', false ]
},
{
func: 'getObjects',
method: 'get',
data: {},
path: '/api/swift/containers/spam/objects/',
error: 'Unable to get the objects in container.',
testInput: [ 'spam' ]
},
{
func: 'getObjects',
method: 'get',
data: {params: {path: '/foo/bar'}},
path: '/api/swift/containers/spam/objects/',
error: 'Unable to get the objects in container.',
testInput: [ 'spam', {path: '/foo/bar'} ]
},
{
func: 'uploadObject',
method: 'post',
call_args: [
'/api/swift/containers/spam/object/ham',
fakeFormData,
{headers: {'Content-Type': undefined}}
],
error: 'Unable to upload the object.',
testInput: [ 'spam', 'ham', 'some junk' ]
},
{
func: 'deleteObject',
method: 'delete',
path: '/api/swift/containers/spam/object/ham',
error: 'Unable to delete the object.',
testInput: [ 'spam', 'ham' ]
},
{
func: 'getObjectDetails',
method: 'get',
path: '/api/swift/containers/spam/metadata/ham',
error: 'Unable to get details of the object.',
testInput: [ 'spam', 'ham' ]
},
{
func: 'createFolder',
method: 'post',
call_args: ['/api/swift/containers/spam/object/ham/', {}],
error: 'Unable to create the folder.',
testInput: [ 'spam', 'ham' ]
},
{
func: 'copyObject',
method: 'post',
call_args: [
'/api/swift/containers/spam/copy/ham',
{dest_container: 'eggs', dest_name: 'bacon'}
],
error: 'Unable to copy the object.',
testInput: [ 'spam', 'ham', 'eggs', 'bacon' ]
}
];
// Iterate through the defined tests and apply as Jasmine specs.
angular.forEach(tests, function(params) {
it('defines the ' + params.func + ' call properly', function test() {
var callParams = [apiService, service, toastService, params];
testCall.apply(this, callParams);
});
});
it('returns a better error message when delete is prevented', function test() {
var promise = {error: angular.noop};
spyOn(apiService, 'delete').and.returnValue(promise);
spyOn(promise, 'error');
service.deleteObject('spam', 'ham');
expect(apiService.delete).toHaveBeenCalledWith('/api/swift/containers/spam/object/ham');
var innerFunc = promise.error.calls.argsFor(0)[0];
expect(innerFunc).toBeDefined();
spyOn(toastService, 'add');
innerFunc('whatever', 409);
expect(toastService.add).toHaveBeenCalledWith(
'error',
'Unable to delete the folder because it is not empty.'
);
});
it('constructs container URLs', function test() {
expect(service.getContainerURL('spam')).toEqual('/api/swift/containers/spam');
});
it('constructs container URLs with reserved characters', function test() {
expect(service.getContainerURL('sp#m')).toEqual(
'/api/swift/containers/sp%23m'
);
});
it('constructs object URLs', function test() {
expect(service.getObjectURL('spam', 'ham')).toEqual(
'/api/swift/containers/spam/object/ham'
);
});
it('constructs object URLs for different functions', function test() {
expect(service.getObjectURL('spam', 'ham', 'blah')).toEqual(
'/api/swift/containers/spam/blah/ham'
);
});
it('constructs object URLs with reserved characters', function test() {
expect(service.getObjectURL('sp#m', 'ham/f#o')).toEqual(
'/api/swift/containers/sp%23m/object/ham/f%23o'
);
});
});
})();

View File

@ -27,7 +27,7 @@ TEST = TestData(neutron_data.data)
class NeutronNetworksTestCase(test.TestCase):
def setUp(self):
super(NeutronNetworksTestCase, self).setUp()
self._networks = [mock_factory(n)
self._networks = [test.mock_factory(n)
for n in TEST.api_networks.list()]
@mock.patch.object(neutron.api, 'neutron')
@ -109,9 +109,9 @@ class NeutronNetworksTestCase(test.TestCase):
class NeutronSubnetsTestCase(test.TestCase):
def setUp(self):
super(NeutronSubnetsTestCase, self).setUp()
self._networks = [mock_factory(n)
self._networks = [test.mock_factory(n)
for n in TEST.api_networks.list()]
self._subnets = [mock_factory(n)
self._subnets = [test.mock_factory(n)
for n in TEST.api_subnets.list()]
@mock.patch.object(neutron.api, 'neutron')
@ -142,9 +142,9 @@ class NeutronSubnetsTestCase(test.TestCase):
class NeutronPortsTestCase(test.TestCase):
def setUp(self):
super(NeutronPortsTestCase, self).setUp()
self._networks = [mock_factory(n)
self._networks = [test.mock_factory(n)
for n in TEST.api_networks.list()]
self._ports = [mock_factory(n)
self._ports = [test.mock_factory(n)
for n in TEST.api_ports.list()]
@mock.patch.object(neutron.api, 'neutron')

View File

@ -0,0 +1,237 @@
# Copyright 2016, Rackspace, US, Inc.
#
# 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.
import mock
from openstack_dashboard.api.rest import swift
from openstack_dashboard.test import helpers as test
from openstack_dashboard.test.test_data import swift_data
from openstack_dashboard.test.test_data.utils import TestData # noqa
TEST = TestData(swift_data.data)
class SwiftRestTestCase(test.TestCase):
def setUp(self):
super(SwiftRestTestCase, self).setUp()
self._containers = TEST.containers.list()
self._objects = TEST.objects.list()
self._folder = TEST.folder.list()
self._subfolder = TEST.subfolder.list()
#
# Version
#
@mock.patch.object(swift.api, 'swift')
def test_version_get(self, nc):
request = self.mock_rest_request()
nc.swift_get_capabilities.return_value = {'swift': {'version': '1.0'}}
response = swift.Info().get(request)
self.assertStatusCode(response, 200)
self.assertEqual(response.json, {
'info': {'swift': {'version': '1.0'}}
})
nc.swift_get_capabilities.assert_called_once_with(request)
#
# Containers
#
@mock.patch.object(swift.api, 'swift')
def test_containers_get(self, nc):
request = self.mock_rest_request()
nc.swift_get_containers.return_value = (self._containers, False)
response = swift.Containers().get(request)
self.assertStatusCode(response, 200)
self.assertEqual(response.json['items'][0]['name'],
u'container one%\u6346')
self.assertEqual(response.json['has_more'], False)
nc.swift_get_containers.assert_called_once_with(request)
#
# Container
#
@mock.patch.object(swift.api, 'swift')
def test_container_get(self, nc):
request = self.mock_rest_request()
nc.swift_get_container.return_value = self._containers[0]
response = swift.Container().get(request, u'container one%\u6346')
self.assertStatusCode(response, 200)
self.assertEqual(response.json, self._containers[0].to_dict())
nc.swift_get_container.assert_called_once_with(request,
u'container one%\u6346')
@mock.patch.object(swift.api, 'swift')
def test_container_create(self, nc):
request = self.mock_rest_request(body='{}')
response = swift.Container().post(request, 'spam')
self.assertStatusCode(response, 201)
self.assertEqual(response['location'],
u'/api/swift/containers/spam')
nc.swift_create_container.assert_called_once_with(
request, 'spam', metadata={}
)
@mock.patch.object(swift.api, 'swift')
def test_container_create_is_public(self, nc):
request = self.mock_rest_request(body='{"is_public": false}')
response = swift.Container().post(request, 'spam')
self.assertStatusCode(response, 201)
self.assertEqual(response['location'],
u'/api/swift/containers/spam')
nc.swift_create_container.assert_called_once_with(
request, 'spam', metadata={'is_public': False}
)
@mock.patch.object(swift.api, 'swift')
def test_container_delete(self, nc):
request = self.mock_rest_request()
response = swift.Container().delete(request, u'container one%\u6346')
self.assertStatusCode(response, 204)
nc.swift_delete_container.assert_called_once_with(
request, u'container one%\u6346'
)
@mock.patch.object(swift.api, 'swift')
def test_container_update(self, nc):
request = self.mock_rest_request(body='{"is_public": false}')
response = swift.Container().put(request, 'spam')
self.assertStatusCode(response, 204)
nc.swift_update_container.assert_called_once_with(
request, 'spam', metadata={'is_public': False}
)
#
# Objects
#
@mock.patch.object(swift.api, 'swift')
def test_objects_get(self, nc):
request = self.mock_rest_request(GET={})
nc.swift_get_objects.return_value = (self._objects, False)
response = swift.Objects().get(request, u'container one%\u6346')
self.assertStatusCode(response, 200)
self.assertEqual(len(response.json['items']), 4)
self.assertEqual(response.json['items'][3]['path'],
u'test folder%\u6346/test.txt')
self.assertEqual(response.json['items'][3]['name'], 'test.txt')
self.assertEqual(response.json['items'][3]['is_object'], True)
self.assertEqual(response.json['items'][3]['is_subdir'], False)
nc.swift_get_objects.assert_called_once_with(request,
u'container one%\u6346',
prefix=None)
@mock.patch.object(swift.api, 'swift')
def test_container_get_path_folder(self, nc):
request = self.mock_rest_request(GET={'path': u'test folder%\u6346/'})
nc.swift_get_objects.return_value = (self._subfolder, False)
response = swift.Objects().get(request, u'container one%\u6346')
self.assertStatusCode(response, 200)
self.assertEqual(len(response.json['items']), 1)
self.assertEqual(response.json['items'][0]['is_object'], True)
self.assertEqual(response.json['items'][0]['is_subdir'], False)
nc.swift_get_objects.assert_called_once_with(
request,
u'container one%\u6346', prefix=u'test folder%\u6346/'
)
#
# Object
#
@mock.patch.object(swift.api, 'swift')
def test_object_get(self, nc):
request = self.mock_rest_request()
nc.swift_get_object.return_value = self._objects[0]
response = swift.ObjectMetadata().get(request, 'container', 'test.txt')
self.assertStatusCode(response, 200)
self.assertEqual(response.json, self._objects[0].to_dict())
nc.swift_get_object.assert_called_once_with(
request,
container_name='container',
object_name='test.txt',
with_data=False
)
@mock.patch.object(swift.api, 'swift')
def test_object_delete(self, nc):
request = self.mock_rest_request()
nc.swift_delete_object.return_value = True
response = swift.Object().delete(request, 'container', 'test.txt')
self.assertStatusCode(response, 204)
nc.swift_delete_object.assert_called_once_with(request,
'container',
'test.txt')
@mock.patch.object(swift, 'UploadObjectForm')
@mock.patch.object(swift.api, 'swift')
def test_object_create(self, nc, uf):
uf.return_value.is_valid.return_value = True
# note file name not used, path name is
file = mock.Mock(name=u'NOT object%\u6346')
uf.return_value.clean.return_value = {'file': file}
request = self.mock_rest_request()
real_name = u'test_object%\u6346'
nc.swift_upload_object.return_value = self._objects[0]
response = swift.Object().post(request, 'spam', real_name)
self.assertStatusCode(response, 201)
self.assertEqual(
response['location'],
'=?utf-8?q?/api/swift/containers/spam/object/test_object'
'=25=E6=8D=86?='
)
self.assertTrue(nc.swift_upload_object.called)
call = nc.swift_upload_object.call_args[0]
self.assertEqual(call[0:3], (request, 'spam', u'test_object%\u6346'))
self.assertEqual(call[3], file)
@mock.patch.object(swift, 'UploadObjectForm')
@mock.patch.object(swift.api, 'swift')
def test_folder_create(self, nc, uf):
uf.return_value.is_valid.return_value = True
uf.return_value.clean.return_value = {}
request = self.mock_rest_request()
nc.swift_create_pseudo_folder.return_value = self._folder[0]
response = swift.Object().post(request, 'spam', u'test_folder%\u6346/')
self.assertStatusCode(response, 201)
self.assertEqual(
response['location'],
'=?utf-8?q?/api/swift/containers/spam/object/test_folder'
'=25=E6=8D=86/?='
)
self.assertTrue(nc.swift_create_pseudo_folder.called)
call = nc.swift_create_pseudo_folder.call_args[0]
self.assertEqual(call[0:3], (request, 'spam', u'test_folder%\u6346/'))
@mock.patch.object(swift.api, 'swift')
def test_object_copy(self, nc):
request = self.mock_rest_request(
body='{"dest_container":"eggs", "dest_name":"bacon"}',
)
nc.swift_copy_object.return_value = self._objects[0]
response = swift.ObjectCopy().post(request,
'spam',
u'test object%\u6346')
self.assertStatusCode(response, 201)
self.assertEqual(
response['location'],
'=?utf-8?q?/api/swift/containers/eggs/object/test_object'
'=25=E6=8D=86?='
)
self.assertTrue(nc.swift_copy_object.called)
call = nc.swift_copy_object.call_args[0]
self.assertEqual(call[0:5], (request,
'spam',
u'test object%\u6346',
'eggs',
'bacon'))
self.assertStatusCode(response, 201)

View File

@ -632,3 +632,14 @@ class update_settings(django_test_utils.override_settings):
copied.update(new_value)
kwargs[key] = copied
super(update_settings, self).__init__(**kwargs)
def mock_obj_to_dict(r):
return mock.Mock(**{'to_dict.return_value': r})
def mock_factory(r):
"""mocks all the attributes as well as the to_dict """
mocked = mock_obj_to_dict(r)
mocked.configure_mock(**r)
return mocked

View File

@ -23,6 +23,7 @@ def data(TEST):
TEST.containers = utils.TestDataContainer()
TEST.objects = utils.TestDataContainer()
TEST.folder = utils.TestDataContainer()
TEST.subfolder = utils.TestDataContainer()
# '%' can break URL if not properly url-quoted
# ' ' (space) can break 'Content-Disposition' if not properly
@ -73,7 +74,7 @@ def data(TEST):
"timestamp": timeutils.utcnow().isoformat(),
"last_modified": None,
"hash": u"object_hash"}
object_dict_4 = {"name": u"test.txt",
object_dict_4 = {"name": u"test folder%\u6346/test.txt",
"content_type": u"text/plain",
"bytes": 128,
"timestamp": timeutils.utcnow().isoformat(),
@ -88,8 +89,8 @@ def data(TEST):
data=obj_data)
TEST.objects.add(swift_object)
folder_dict = {"name": u"test folder%\u6346",
"content_type": u"text/plain",
folder_dict = {"name": u"test folder%\u6346/",
"content_type": u"application/pseudo-folder",
"bytes": 128,
"timestamp": timeutils.utcnow().isoformat(),
"_table_data_type": u"subfolders",
@ -97,3 +98,8 @@ def data(TEST):
"hash": u"object_hash"}
TEST.folder.add(swift.PseudoFolder(folder_dict, container_1.name))
# just the objects matching the folder prefix
TEST.subfolder.add(swift.StorageObject(object_dict_4, container_1.name,
data=object_dict_4))
TEST.subfolder.add(swift.PseudoFolder(folder_dict, container_1.name))