root / snf-pithos-app / pithos / api / test / __init__.py @ 369a7b41
History | View | Annotate | Download (15.5 kB)
1 |
#!/usr/bin/env python
|
---|---|
2 |
#coding=utf8
|
3 |
|
4 |
# Copyright 2011-2013 GRNET S.A. All rights reserved.
|
5 |
#
|
6 |
# Redistribution and use in source and binary forms, with or
|
7 |
# without modification, are permitted provided that the following
|
8 |
# conditions are met:
|
9 |
#
|
10 |
# 1. Redistributions of source code must retain the above
|
11 |
# copyright notice, this list of conditions and the following
|
12 |
# disclaimer.
|
13 |
#
|
14 |
# 2. Redistributions in binary form must reproduce the above
|
15 |
# copyright notice, this list of conditions and the following
|
16 |
# disclaimer in the documentation and/or other materials
|
17 |
# provided with the distribution.
|
18 |
#
|
19 |
# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
|
20 |
# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
21 |
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
22 |
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
|
23 |
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
24 |
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
25 |
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
|
26 |
# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
|
27 |
# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
|
28 |
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
|
29 |
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
30 |
# POSSIBILITY OF SUCH DAMAGE.
|
31 |
#
|
32 |
# The views and conclusions contained in the software and
|
33 |
# documentation are those of the authors and should not be
|
34 |
# interpreted as representing official policies, either expressed
|
35 |
# or implied, of GRNET S.A.
|
36 |
|
37 |
from urlparse import urlunsplit, urlsplit |
38 |
from xml.dom import minidom |
39 |
|
40 |
from snf_django.utils.testing import with_settings, astakos_user |
41 |
|
42 |
from pithos.backends.random_word import get_random_word |
43 |
from pithos.api import settings as pithos_settings |
44 |
|
45 |
from synnefo.lib.services import get_service_path |
46 |
from synnefo.lib import join_urls |
47 |
|
48 |
from django.test import TestCase |
49 |
from django.utils.http import urlencode |
50 |
from django.conf import settings |
51 |
|
52 |
import django.utils.simplejson as json |
53 |
|
54 |
import re |
55 |
import random |
56 |
import threading |
57 |
import functools |
58 |
|
59 |
pithos_test_settings = functools.partial(with_settings, pithos_settings) |
60 |
|
61 |
DATE_FORMATS = ["%a %b %d %H:%M:%S %Y",
|
62 |
"%A, %d-%b-%y %H:%M:%S GMT",
|
63 |
"%a, %d %b %Y %H:%M:%S GMT"]
|
64 |
|
65 |
o_names = ['kate.jpg',
|
66 |
'kate_beckinsale.jpg',
|
67 |
'How To Win Friends And Influence People.pdf',
|
68 |
'moms_birthday.jpg',
|
69 |
'poodle_strut.mov',
|
70 |
'Disturbed - Down With The Sickness.mp3',
|
71 |
'army_of_darkness.avi',
|
72 |
'the_mad.avi',
|
73 |
'photos/animals/dogs/poodle.jpg',
|
74 |
'photos/animals/dogs/terrier.jpg',
|
75 |
'photos/animals/cats/persian.jpg',
|
76 |
'photos/animals/cats/siamese.jpg',
|
77 |
'photos/plants/fern.jpg',
|
78 |
'photos/plants/rose.jpg',
|
79 |
'photos/me.jpg']
|
80 |
|
81 |
details = {'container': ('name', 'count', 'bytes', 'last_modified', |
82 |
'x_container_policy'),
|
83 |
'object': ('name', 'hash', 'bytes', 'content_type', |
84 |
'content_encoding', 'last_modified',)} |
85 |
|
86 |
return_codes = (400, 401, 403, 404, 503) |
87 |
|
88 |
|
89 |
class PithosAPITest(TestCase): |
90 |
#TODO unauthorized request
|
91 |
def setUp(self): |
92 |
pithos_settings.BACKEND_DB_MODULE = 'pithos.backends.lib.sqlalchemy'
|
93 |
pithos_settings.BACKEND_DB_CONNECTION = django_to_sqlalchemy() |
94 |
pithos_settings.BACKEND_POOL_SIZE = 1
|
95 |
|
96 |
# Override default block size to spead up tests
|
97 |
pithos_settings.BACKEND_BLOCK_SIZE = 1024
|
98 |
|
99 |
self.user = 'user' |
100 |
self.pithos_path = join_urls(get_service_path(
|
101 |
pithos_settings.pithos_services, 'object-store'))
|
102 |
|
103 |
def tearDown(self): |
104 |
#delete additionally created metadata
|
105 |
meta = self.get_account_meta()
|
106 |
self.delete_account_meta(meta)
|
107 |
|
108 |
#delete additionally created groups
|
109 |
groups = self.get_account_groups()
|
110 |
self.delete_account_groups(groups)
|
111 |
|
112 |
self._clean_account()
|
113 |
|
114 |
def head(self, url, user='user', *args, **kwargs): |
115 |
with astakos_user(user):
|
116 |
response = self.client.head(url, *args, **kwargs)
|
117 |
return response
|
118 |
|
119 |
def get(self, url, user='user', *args, **kwargs): |
120 |
with astakos_user(user):
|
121 |
response = self.client.get(url, *args, **kwargs)
|
122 |
return response
|
123 |
|
124 |
def delete(self, url, user='user', *args, **kwargs): |
125 |
with astakos_user(user):
|
126 |
response = self.client.delete(url, *args, **kwargs)
|
127 |
return response
|
128 |
|
129 |
def post(self, url, user='user', *args, **kwargs): |
130 |
with astakos_user(user):
|
131 |
kwargs.setdefault('content_type', 'application/octet-stream') |
132 |
response = self.client.post(url, *args, **kwargs)
|
133 |
return response
|
134 |
|
135 |
def put(self, url, user='user', *args, **kwargs): |
136 |
with astakos_user(user):
|
137 |
kwargs.setdefault('content_type', 'application/octet-stream') |
138 |
response = self.client.put(url, *args, **kwargs)
|
139 |
return response
|
140 |
|
141 |
def _clean_account(self): |
142 |
for c in self.list_containers(): |
143 |
self.delete_container_content(c['name']) |
144 |
self.delete_container(c['name']) |
145 |
|
146 |
def update_account_meta(self, meta): |
147 |
kwargs = dict(
|
148 |
('HTTP_X_ACCOUNT_META_%s' % k, str(v)) for k, v in meta.items()) |
149 |
url = join_urls(self.pithos_path, self.user) |
150 |
r = self.post('%s?update=' % url, **kwargs) |
151 |
self.assertEqual(r.status_code, 202) |
152 |
account_meta = self.get_account_meta()
|
153 |
(self.assertTrue('X-Account-Meta-%s' % k in account_meta) for |
154 |
k in meta.keys())
|
155 |
(self.assertEqual(account_meta['X-Account-Meta-%s' % k], v) for |
156 |
k, v in meta.items())
|
157 |
|
158 |
def reset_account_meta(self, meta): |
159 |
kwargs = dict(
|
160 |
('HTTP_X_ACCOUNT_META_%s' % k, str(v)) for k, v in meta.items()) |
161 |
url = join_urls(self.pithos_path, self.user) |
162 |
r = self.post(url, **kwargs)
|
163 |
self.assertEqual(r.status_code, 202) |
164 |
account_meta = self.get_account_meta()
|
165 |
(self.assertTrue('X-Account-Meta-%s' % k in account_meta) for |
166 |
k in meta.keys())
|
167 |
(self.assertEqual(account_meta['X-Account-Meta-%s' % k], v) for |
168 |
k, v in meta.items())
|
169 |
|
170 |
def delete_account_meta(self, meta): |
171 |
transform = lambda k: 'HTTP_%s' % k.replace('-', '_').upper() |
172 |
kwargs = dict((transform(k), '') for k, v in meta.items()) |
173 |
url = join_urls(self.pithos_path, self.user) |
174 |
r = self.post('%s?update=' % url, **kwargs) |
175 |
self.assertEqual(r.status_code, 202) |
176 |
account_meta = self.get_account_meta()
|
177 |
(self.assertTrue('X-Account-Meta-%s' % k not in account_meta) for |
178 |
k in meta.keys())
|
179 |
return r
|
180 |
|
181 |
def delete_account_groups(self, groups): |
182 |
url = join_urls(self.pithos_path, self.user) |
183 |
r = self.post('%s?update=' % url, **groups) |
184 |
self.assertEqual(r.status_code, 202) |
185 |
return r
|
186 |
|
187 |
def get_account_info(self, until=None): |
188 |
url = join_urls(self.pithos_path, self.user) |
189 |
if until is not None: |
190 |
parts = list(urlsplit(url))
|
191 |
parts[3] = urlencode({
|
192 |
'until': until
|
193 |
}) |
194 |
url = urlunsplit(parts) |
195 |
r = self.head(url)
|
196 |
self.assertEqual(r.status_code, 204) |
197 |
return r
|
198 |
|
199 |
def get_account_meta(self, until=None): |
200 |
r = self.get_account_info(until=until)
|
201 |
headers = dict(r._headers.values())
|
202 |
map(headers.pop,
|
203 |
[k for k in headers.keys() |
204 |
if not k.startswith('X-Account-Meta-')]) |
205 |
return headers
|
206 |
|
207 |
def get_account_groups(self, until=None): |
208 |
r = self.get_account_info(until=until)
|
209 |
headers = dict(r._headers.values())
|
210 |
map(headers.pop,
|
211 |
[k for k in headers.keys() |
212 |
if not k.startswith('X-Account-Group-')]) |
213 |
return headers
|
214 |
|
215 |
def list_containers(self, format='json', headers={}, **params): |
216 |
_url = join_urls(self.pithos_path, self.user) |
217 |
parts = list(urlsplit(_url))
|
218 |
params['format'] = format
|
219 |
parts[3] = urlencode(params)
|
220 |
url = urlunsplit(parts) |
221 |
_headers = dict(('HTTP_%s' % k.upper(), str(v)) |
222 |
for k, v in headers.items()) |
223 |
r = self.get(url, **_headers)
|
224 |
|
225 |
if format is None: |
226 |
containers = r.content.split('\n')
|
227 |
if '' in containers: |
228 |
containers.remove('')
|
229 |
return containers
|
230 |
elif format == 'json': |
231 |
try:
|
232 |
containers = json.loads(r.content) |
233 |
except:
|
234 |
self.fail('json format expected') |
235 |
return containers
|
236 |
elif format == 'xml': |
237 |
return minidom.parseString(r.content)
|
238 |
|
239 |
def delete_container_content(self, cname): |
240 |
url = join_urls(self.pithos_path, self.user, cname) |
241 |
r = self.delete('%s?delimiter=/' % url) |
242 |
self.assertEqual(r.status_code, 204) |
243 |
return r
|
244 |
|
245 |
def delete_container(self, cname): |
246 |
url = join_urls(self.pithos_path, self.user, cname) |
247 |
r = self.delete(url)
|
248 |
self.assertEqual(r.status_code, 204) |
249 |
return r
|
250 |
|
251 |
def create_container(self, cname): |
252 |
url = join_urls(self.pithos_path, self.user, cname) |
253 |
r = self.put(url, data='') |
254 |
self.assertTrue(r.status_code in (202, 201)) |
255 |
return r
|
256 |
|
257 |
def upload_object(self, cname, oname=None, **meta): |
258 |
oname = oname or get_random_word(8) |
259 |
data = get_random_word(length=random.randint(1, 1024)) |
260 |
headers = dict(('HTTP_X_OBJECT_META_%s' % k.upper(), v) |
261 |
for k, v in meta.iteritems()) |
262 |
url = join_urls(self.pithos_path, self.user, cname, oname) |
263 |
r = self.put(url, data=data, **headers)
|
264 |
self.assertEqual(r.status_code, 201) |
265 |
return oname, data, r
|
266 |
|
267 |
def create_folder(self, cname, oname=get_random_word(8), **headers): |
268 |
url = join_urls(self.pithos_path, self.user, cname, oname) |
269 |
r = self.put(url, data='', content_type='application/directory', |
270 |
**headers) |
271 |
self.assertEqual(r.status_code, 201) |
272 |
return oname, r
|
273 |
|
274 |
def list_objects(self, cname): |
275 |
url = join_urls(self.pithos_path, self.user, cname) |
276 |
r = self.get('%s?format=json' % url) |
277 |
self.assertTrue(r.status_code in (200, 204)) |
278 |
try:
|
279 |
objects = json.loads(r.content) |
280 |
except:
|
281 |
self.fail('json format expected') |
282 |
return objects
|
283 |
|
284 |
def assert_status(self, status, codes): |
285 |
l = [elem for elem in return_codes] |
286 |
if isinstance(codes, list): |
287 |
l.extend(codes) |
288 |
else:
|
289 |
l.append(codes) |
290 |
self.assertTrue(status in l) |
291 |
|
292 |
def assert_extended(self, data, format, type, size=10000): |
293 |
if format == 'xml': |
294 |
self._assert_xml(data, type, size) |
295 |
elif format == 'json': |
296 |
self._assert_json(data, type, size) |
297 |
|
298 |
def _assert_json(self, data, type, size): |
299 |
convert = lambda s: s.lower()
|
300 |
info = [convert(elem) for elem in details[type]] |
301 |
self.assertTrue(len(data) <= size) |
302 |
for item in info: |
303 |
for i in data: |
304 |
if 'subdir' in i.keys(): |
305 |
continue
|
306 |
self.assertTrue(item in i.keys()) |
307 |
|
308 |
def _assert_xml(self, data, type, size): |
309 |
convert = lambda s: s.lower()
|
310 |
info = [convert(elem) for elem in details[type]] |
311 |
try:
|
312 |
info.remove('content_encoding')
|
313 |
except ValueError: |
314 |
pass
|
315 |
xml = data |
316 |
entities = xml.getElementsByTagName(type)
|
317 |
self.assertTrue(len(entities) <= size) |
318 |
for e in entities: |
319 |
for item in info: |
320 |
self.assertTrue(e.getElementsByTagName(item))
|
321 |
|
322 |
|
323 |
class AssertMappingInvariant(object): |
324 |
def __init__(self, callable, *args, **kwargs): |
325 |
self.callable = callable |
326 |
self.args = args
|
327 |
self.kwargs = kwargs
|
328 |
|
329 |
def __enter__(self): |
330 |
self.map = self.callable(*self.args, **self.kwargs) |
331 |
return self.map |
332 |
|
333 |
def __exit__(self, type, value, tb): |
334 |
map = self.callable(*self.args, **self.kwargs) |
335 |
for k, v in self.map.items(): |
336 |
if is_date(v):
|
337 |
continue
|
338 |
|
339 |
assert(k in map), '%s not in map' % k |
340 |
assert v == map[k] |
341 |
|
342 |
django_sqlalchemy_engines = { |
343 |
'django.db.backends.postgresql_psycopg2': 'postgresql+psycopg2', |
344 |
'django.db.backends.postgresql': 'postgresql', |
345 |
'django.db.backends.mysql': '', |
346 |
'django.db.backends.sqlite3': 'mssql', |
347 |
'django.db.backends.oracle': 'oracle'} |
348 |
|
349 |
|
350 |
def django_to_sqlalchemy(): |
351 |
"""Convert the django default database to sqlalchemy connection string"""
|
352 |
# TODO support for more complex configuration
|
353 |
db = settings.DATABASES['default']
|
354 |
name = db.get('TEST_NAME', 'test_%s' % db['NAME']) |
355 |
if db['ENGINE'] == 'django.db.backends.sqlite3': |
356 |
db.get('TEST_NAME', db['NAME']) |
357 |
return 'sqlite:///%s' % name |
358 |
else:
|
359 |
d = dict(scheme=django_sqlalchemy_engines.get(db['ENGINE']), |
360 |
user=db['USER'],
|
361 |
pwd=db['PASSWORD'],
|
362 |
host=db['HOST'].lower(),
|
363 |
port=int(db['PORT']) if db['PORT'] != '' else '', |
364 |
name=name) |
365 |
return '%(scheme)s://%(user)s:%(pwd)s@%(host)s:%(port)s/%(name)s' % d |
366 |
|
367 |
|
368 |
def is_date(date): |
369 |
__D = r'(?P<day>\d{2})'
|
370 |
__D2 = r'(?P<day>[ \d]\d)'
|
371 |
__M = r'(?P<mon>\w{3})'
|
372 |
__Y = r'(?P<year>\d{4})'
|
373 |
__Y2 = r'(?P<year>\d{2})'
|
374 |
__T = r'(?P<hour>\d{2}):(?P<min>\d{2}):(?P<sec>\d{2})'
|
375 |
RFC1123_DATE = re.compile(r'^\w{3}, %s %s %s %s GMT$' % (
|
376 |
__D, __M, __Y, __T)) |
377 |
RFC850_DATE = re.compile(r'^\w{6,9}, %s-%s-%s %s GMT$' % (
|
378 |
__D, __M, __Y2, __T)) |
379 |
ASCTIME_DATE = re.compile(r'^\w{3} %s %s %s %s$' % (
|
380 |
__M, __D2, __T, __Y)) |
381 |
for regex in RFC1123_DATE, RFC850_DATE, ASCTIME_DATE: |
382 |
m = regex.match(date) |
383 |
if m is not None: |
384 |
return True |
385 |
return False |
386 |
|
387 |
|
388 |
def strnextling(prefix): |
389 |
"""Return the first unicode string
|
390 |
greater than but not starting with given prefix.
|
391 |
strnextling('hello') -> 'hellp'
|
392 |
"""
|
393 |
if not prefix: |
394 |
## all strings start with the null string,
|
395 |
## therefore we have to approximate strnextling('')
|
396 |
## with the last unicode character supported by python
|
397 |
## 0x10ffff for wide (32-bit unicode) python builds
|
398 |
## 0x00ffff for narrow (16-bit unicode) python builds
|
399 |
## We will not autodetect. 0xffff is safe enough.
|
400 |
return unichr(0xffff) |
401 |
s = prefix[:-1]
|
402 |
c = ord(prefix[-1]) |
403 |
if c >= 0xffff: |
404 |
raise RuntimeError |
405 |
s += unichr(c + 1) |
406 |
return s
|
407 |
|
408 |
|
409 |
def test_concurrently(times=2): |
410 |
"""
|
411 |
Add this decorator to small pieces of code that you want to test
|
412 |
concurrently to make sure they don't raise exceptions when run at the
|
413 |
same time. E.g., some Django views that do a SELECT and then a subsequent
|
414 |
INSERT might fail when the INSERT assumes that the data has not changed
|
415 |
since the SELECT.
|
416 |
"""
|
417 |
def test_concurrently_decorator(test_func): |
418 |
def wrapper(*args, **kwargs): |
419 |
exceptions = [] |
420 |
|
421 |
def call_test_func(): |
422 |
try:
|
423 |
test_func(*args, **kwargs) |
424 |
except Exception, e: |
425 |
exceptions.append(e) |
426 |
raise
|
427 |
|
428 |
threads = [] |
429 |
for i in range(times): |
430 |
threads.append(threading.Thread()) |
431 |
for t in threads: |
432 |
t.start() |
433 |
for t in threads: |
434 |
t.join() |
435 |
if exceptions:
|
436 |
raise Exception( |
437 |
('test_concurrently intercepted %s',
|
438 |
'exceptions: %s') % (len(exceptions), exceptions)) |
439 |
return wrapper
|
440 |
return test_concurrently_decorator
|