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:
parent
dea5b2878e
commit
672b6ae003
@ -31,3 +31,4 @@ from . import network # noqa
|
||||
from . import neutron # noqa
|
||||
from . import nova # noqa
|
||||
from . import policy # noqa
|
||||
from . import swift # noqa
|
||||
|
224
openstack_dashboard/api/rest/swift.py
Normal file
224
openstack_dashboard/api/rest/swift.py
Normal 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)
|
||||
)
|
@ -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)
|
||||
|
@ -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()
|
||||
|
@ -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)
|
||||
|
@ -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);
|
||||
|
@ -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.'));
|
||||
});
|
||||
}
|
||||
}
|
||||
}());
|
@ -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'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
})();
|
@ -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')
|
||||
|
237
openstack_dashboard/test/api_tests/swift_rest_tests.py
Normal file
237
openstack_dashboard/test/api_tests/swift_rest_tests.py
Normal 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)
|
@ -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
|
||||
|
@ -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))
|
||||
|
Loading…
x
Reference in New Issue
Block a user