Statistics
| Branch: | Tag: | Revision:

root / snf-pithos-app / pithos / api / test / __init__.py @ 65f480f5

History | View | Annotate | Download (19.2 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.api import settings as pithos_settings
43
from pithos.api.test.util import is_date, get_random_data
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.conf import settings
50
from django.utils.http import urlencode
51

    
52
import django.utils.simplejson as json
53

    
54
import random
55
import threading
56
import functools
57

    
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
TEST_BLOCK_SIZE = 1024
89
TEST_HASH_ALGORITHM = 'sha256'
90

    
91
BACKEND_DB_CONNECTION = None
92

    
93

    
94
def django_to_sqlalchemy():
95
    """Convert the django default database to sqlalchemy connection string"""
96

    
97
    global BACKEND_DB_CONNECTION
98
    if BACKEND_DB_CONNECTION:
99
        return BACKEND_DB_CONNECTION
100

    
101
    # TODO support for more complex configuration
102
    db = settings.DATABASES['default']
103
    name = db.get('TEST_NAME', 'test_%s' % db['NAME'])
104
    if db['ENGINE'] == 'django.db.backends.sqlite3':
105
        BACKEND_DB_CONNECTION = 'sqlite:///%s' % name
106
    else:
107
        d = dict(scheme=django_sqlalchemy_engines.get(db['ENGINE']),
108
                 user=db['USER'],
109
                 pwd=db['PASSWORD'],
110
                 host=db['HOST'].lower(),
111
                 port=int(db['PORT']) if db['PORT'] != '' else '',
112
                 name=name)
113
        BACKEND_DB_CONNECTION = (
114
            '%(scheme)s://%(user)s:%(pwd)s@%(host)s:%(port)s/%(name)s' % d)
115
    return BACKEND_DB_CONNECTION
116

    
117

    
118
class PithosAPITest(TestCase):
119
    def setUp(self):
120
        if (pithos_settings.BACKEND_DB_MODULE ==
121
                'pithos.backends.lib.sqlalchemy'):
122
            pithos_settings.BACKEND_DB_CONNECTION = django_to_sqlalchemy()
123
            pithos_settings.BACKEND_POOL_SIZE = 1
124

    
125
        # Override default block size to spead up tests
126
        pithos_settings.BACKEND_BLOCK_SIZE = TEST_BLOCK_SIZE
127
        pithos_settings.BACKEND_HASH_ALGORITHM = TEST_HASH_ALGORITHM
128

    
129
        self.user = 'user'
130
        self.pithos_path = join_urls(get_service_path(
131
            pithos_settings.pithos_services, 'object-store'))
132

    
133
    def tearDown(self):
134
        #delete additionally created metadata
135
        meta = self.get_account_meta()
136
        self.delete_account_meta(meta)
137

    
138
        #delete additionally created groups
139
        groups = self.get_account_groups()
140
        self.delete_account_groups(groups)
141

    
142
        self._clean_account()
143

    
144
    def _clean_account(self):
145
        for c in self.list_containers():
146
            self.delete_container_content(c['name'])
147
            self.delete_container(c['name'])
148

    
149
    def head(self, url, user='user', *args, **kwargs):
150
        with astakos_user(user):
151
            response = self.client.head(url, *args, **kwargs)
152
        return response
153

    
154
    def get(self, url, user='user', *args, **kwargs):
155
        with astakos_user(user):
156
            response = self.client.get(url, *args, **kwargs)
157
        return response
158

    
159
    def delete(self, url, user='user', *args, **kwargs):
160
        with astakos_user(user):
161
            response = self.client.delete(url, *args, **kwargs)
162
        return response
163

    
164
    def post(self, url, user='user', *args, **kwargs):
165
        with astakos_user(user):
166
            kwargs.setdefault('content_type', 'application/octet-stream')
167
            response = self.client.post(url, *args, **kwargs)
168
        return response
169

    
170
    def put(self, url, user='user', *args, **kwargs):
171
        with astakos_user(user):
172
            kwargs.setdefault('content_type', 'application/octet-stream')
173
            response = self.client.put(url, *args, **kwargs)
174
        return response
175

    
176
    def update_account_meta(self, meta):
177
        kwargs = dict(
178
            ('HTTP_X_ACCOUNT_META_%s' % k, str(v)) for k, v in meta.items())
179
        url = join_urls(self.pithos_path, self.user)
180
        r = self.post('%s?update=' % url, **kwargs)
181
        self.assertEqual(r.status_code, 202)
182
        account_meta = self.get_account_meta()
183
        (self.assertTrue('X-Account-Meta-%s' % k in account_meta) for
184
            k in meta.keys())
185
        (self.assertEqual(account_meta['X-Account-Meta-%s' % k], v) for
186
            k, v in meta.items())
187

    
188
    def reset_account_meta(self, meta):
189
        kwargs = dict(
190
            ('HTTP_X_ACCOUNT_META_%s' % k, str(v)) for k, v in meta.items())
191
        url = join_urls(self.pithos_path, self.user)
192
        r = self.post(url, **kwargs)
193
        self.assertEqual(r.status_code, 202)
194
        account_meta = self.get_account_meta()
195
        (self.assertTrue('X-Account-Meta-%s' % k in account_meta) for
196
            k in meta.keys())
197
        (self.assertEqual(account_meta['X-Account-Meta-%s' % k], v) for
198
            k, v in meta.items())
199

    
200
    def delete_account_meta(self, meta):
201
        transform = lambda k: 'HTTP_%s' % k.replace('-', '_').upper()
202
        kwargs = dict((transform(k), '') for k, v in meta.items())
203
        url = join_urls(self.pithos_path, self.user)
204
        r = self.post('%s?update=' % url, **kwargs)
205
        self.assertEqual(r.status_code, 202)
206
        account_meta = self.get_account_meta()
207
        (self.assertTrue('X-Account-Meta-%s' % k not in account_meta) for
208
            k in meta.keys())
209
        return r
210

    
211
    def delete_account_groups(self, groups):
212
        url = join_urls(self.pithos_path, self.user)
213
        r = self.post('%s?update=' % url, **groups)
214
        self.assertEqual(r.status_code, 202)
215
        return r
216

    
217
    def get_account_info(self, until=None):
218
        url = join_urls(self.pithos_path, self.user)
219
        if until is not None:
220
            parts = list(urlsplit(url))
221
            parts[3] = urlencode({
222
                'until': until
223
            })
224
            url = urlunsplit(parts)
225
        r = self.head(url)
226
        self.assertEqual(r.status_code, 204)
227
        return r
228

    
229
    def get_account_meta(self, until=None):
230
        r = self.get_account_info(until=until)
231
        headers = dict(r._headers.values())
232
        map(headers.pop,
233
            [k for k in headers.keys()
234
                if not k.startswith('X-Account-Meta-')])
235
        return headers
236

    
237
    def get_account_groups(self, until=None):
238
        r = self.get_account_info(until=until)
239
        headers = dict(r._headers.values())
240
        map(headers.pop,
241
            [k for k in headers.keys()
242
                if not k.startswith('X-Account-Group-')])
243
        return headers
244

    
245
    def get_container_info(self, container, until=None):
246
        url = join_urls(self.pithos_path, self.user, container)
247
        if until is not None:
248
            parts = list(urlsplit(url))
249
            parts[3] = urlencode({
250
                'until': until
251
            })
252
            url = urlunsplit(parts)
253
        r = self.head(url)
254
        self.assertEqual(r.status_code, 204)
255
        return r
256

    
257
    def get_container_meta(self, container, until=None):
258
        r = self.get_container_info(container, until=until)
259
        headers = dict(r._headers.values())
260
        map(headers.pop,
261
            [k for k in headers.keys()
262
                if not k.startswith('X-Container-Meta-')])
263
        return headers
264

    
265
    def update_container_meta(self, container, meta):
266
        kwargs = dict(
267
            ('HTTP_X_CONTAINER_META_%s' % k, str(v)) for k, v in meta.items())
268
        url = join_urls(self.pithos_path, self.user, container)
269
        r = self.post('%s?update=' % url, **kwargs)
270
        self.assertEqual(r.status_code, 202)
271
        container_meta = self.get_container_meta(container)
272
        (self.assertTrue('X-Container-Meta-%s' % k in container_meta) for
273
            k in meta.keys())
274
        (self.assertEqual(container_meta['X-Container-Meta-%s' % k], v) for
275
            k, v in meta.items())
276

    
277
    def list_containers(self, format='json', headers={}, **params):
278
        _url = join_urls(self.pithos_path, self.user)
279
        parts = list(urlsplit(_url))
280
        params['format'] = format
281
        parts[3] = urlencode(params)
282
        url = urlunsplit(parts)
283
        _headers = dict(('HTTP_%s' % k.upper(), str(v))
284
                        for k, v in headers.items())
285
        r = self.get(url, **_headers)
286

    
287
        if format is None:
288
            containers = r.content.split('\n')
289
            if '' in containers:
290
                containers.remove('')
291
            return containers
292
        elif format == 'json':
293
            try:
294
                containers = json.loads(r.content)
295
            except:
296
                self.fail('json format expected')
297
            return containers
298
        elif format == 'xml':
299
            return minidom.parseString(r.content)
300

    
301
    def delete_container_content(self, cname):
302
        url = join_urls(self.pithos_path, self.user, cname)
303
        r = self.delete('%s?delimiter=/' % url)
304
        self.assertEqual(r.status_code, 204)
305
        return r
306

    
307
    def delete_container(self, cname):
308
        url = join_urls(self.pithos_path, self.user, cname)
309
        r = self.delete(url)
310
        self.assertEqual(r.status_code, 204)
311
        return r
312

    
313
    def create_container(self, cname):
314
        url = join_urls(self.pithos_path, self.user, cname)
315
        r = self.put(url, data='')
316
        self.assertTrue(r.status_code in (202, 201))
317
        return r
318

    
319
    def upload_object(self, cname, oname=None, length=None, verify=True,
320
                      **meta):
321
        oname = oname or get_random_data(8)
322
        length = length or random.randint(TEST_BLOCK_SIZE, 2 * TEST_BLOCK_SIZE)
323
        data = get_random_data(length=length)
324
        headers = dict(('HTTP_X_OBJECT_META_%s' % k.upper(), v)
325
                       for k, v in meta.iteritems())
326
        url = join_urls(self.pithos_path, self.user, cname, oname)
327
        r = self.put(url, data=data, **headers)
328
        if verify:
329
            self.assertEqual(r.status_code, 201)
330
        return oname, data, r
331

    
332
    def update_object_data(self, cname, oname=None, length=None,
333
                           content_type=None, content_range=None,
334
                           verify=True, **meta):
335
        oname = oname or get_random_data(8)
336
        length = length or random.randint(TEST_BLOCK_SIZE, 2 * TEST_BLOCK_SIZE)
337
        content_type = content_type or 'application/octet-stream'
338
        data = get_random_data(length=length)
339
        headers = dict(('HTTP_X_OBJECT_META_%s' % k.upper(), v)
340
                       for k, v in meta.iteritems())
341
        if content_range:
342
            headers['HTTP_CONTENT_RANGE'] = content_range
343
        url = join_urls(self.pithos_path, self.user, cname, oname)
344
        r = self.post(url, data=data, content_type=content_type, **headers)
345
        if verify:
346
            self.assertEqual(r.status_code, 204)
347
        return oname, data, r
348

    
349
    def append_object_data(self, cname, oname=None, length=None,
350
                           content_type=None):
351
        return self.update_object_data(cname, oname=oname,
352
                                       length=length,
353
                                       content_type=content_type,
354
                                       content_range='bytes */*')
355

    
356
    def create_folder(self, cname, oname=None, **headers):
357
        oname = oname or get_random_data(8)
358
        url = join_urls(self.pithos_path, self.user, cname, oname)
359
        r = self.put(url, data='', content_type='application/directory',
360
                     **headers)
361
        self.assertEqual(r.status_code, 201)
362
        return oname, r
363

    
364
    def list_objects(self, cname, prefix=None):
365
        url = join_urls(self.pithos_path, self.user, cname)
366
        path = '%s?format=json' % url
367
        if prefix is not None:
368
            path = '%s&prefix=%s' % (path, prefix)
369
        r = self.get(path)
370
        self.assertTrue(r.status_code in (200, 204))
371
        try:
372
            objects = json.loads(r.content)
373
        except:
374
            self.fail('json format expected')
375
        return objects
376

    
377
    def get_object_info(self, container, object, version=None, until=None):
378
        url = join_urls(self.pithos_path, self.user, container, object)
379
        if until is not None:
380
            parts = list(urlsplit(url))
381
            parts[3] = urlencode({
382
                'until': until
383
            })
384
            url = urlunsplit(parts)
385
        if version:
386
            url = '%s?version=%s' % (url, version)
387
        r = self.head(url)
388
        self.assertEqual(r.status_code, 200)
389
        return r
390

    
391
    def get_object_meta(self, container, object, version=None, until=None):
392
        r = self.get_object_info(container, object, version, until=until)
393
        headers = dict(r._headers.values())
394
        map(headers.pop,
395
            [k for k in headers.keys()
396
                if not k.startswith('X-Object-Meta-')])
397
        return headers
398

    
399
    def update_object_meta(self, container, object, meta):
400
        kwargs = dict(
401
            ('HTTP_X_OBJECT_META_%s' % k, str(v)) for k, v in meta.items())
402
        url = join_urls(self.pithos_path, self.user, container, object)
403
        r = self.post('%s?update=' % url, content_type='', **kwargs)
404
        self.assertEqual(r.status_code, 202)
405
        object_meta = self.get_object_meta(container, object)
406
        (self.assertTrue('X-Objecr-Meta-%s' % k in object_meta) for
407
            k in meta.keys())
408
        (self.assertEqual(object_meta['X-Object-Meta-%s' % k], v) for
409
            k, v in meta.items())
410

    
411
    def assert_status(self, status, codes):
412
        l = [elem for elem in return_codes]
413
        if isinstance(codes, list):
414
            l.extend(codes)
415
        else:
416
            l.append(codes)
417
        self.assertTrue(status in l)
418

    
419
    def assert_extended(self, data, format, type, size=10000):
420
        if format == 'xml':
421
            self._assert_xml(data, type, size)
422
        elif format == 'json':
423
            self._assert_json(data, type, size)
424

    
425
    def _assert_json(self, data, type, size):
426
        convert = lambda s: s.lower()
427
        info = [convert(elem) for elem in details[type]]
428
        self.assertTrue(len(data) <= size)
429
        for item in info:
430
            for i in data:
431
                if 'subdir' in i.keys():
432
                    continue
433
                self.assertTrue(item in i.keys())
434

    
435
    def _assert_xml(self, data, type, size):
436
        convert = lambda s: s.lower()
437
        info = [convert(elem) for elem in details[type]]
438
        try:
439
            info.remove('content_encoding')
440
        except ValueError:
441
            pass
442
        xml = data
443
        entities = xml.getElementsByTagName(type)
444
        self.assertTrue(len(entities) <= size)
445
        for e in entities:
446
            for item in info:
447
                self.assertTrue(e.getElementsByTagName(item))
448

    
449

    
450
class AssertMappingInvariant(object):
451
    def __init__(self, callable, *args, **kwargs):
452
        self.callable = callable
453
        self.args = args
454
        self.kwargs = kwargs
455

    
456
    def __enter__(self):
457
        self.map = self.callable(*self.args, **self.kwargs)
458
        return self.map
459

    
460
    def __exit__(self, type, value, tb):
461
        map = self.callable(*self.args, **self.kwargs)
462
        for k, v in self.map.items():
463
            if is_date(v):
464
                continue
465

    
466
            assert(k in map), '%s not in map' % k
467
            assert v == map[k]
468

    
469

    
470
class AssertUUidInvariant(object):
471
    def __init__(self, callable, *args, **kwargs):
472
        self.callable = callable
473
        self.args = args
474
        self.kwargs = kwargs
475

    
476
    def __enter__(self):
477
        self.map = self.callable(*self.args, **self.kwargs)
478
        assert('x-object-uuid' in self.map)
479
        self.uuid = self.map['x-object-uuid']
480
        return self.map
481

    
482
    def __exit__(self, type, value, tb):
483
        map = self.callable(*self.args, **self.kwargs)
484
        assert('x-object-uuid' in self.map)
485
        uuid = map['x-object-uuid']
486
        assert(uuid == self.uuid)
487

    
488

    
489
django_sqlalchemy_engines = {
490
    'django.db.backends.postgresql_psycopg2': 'postgresql+psycopg2',
491
    'django.db.backends.postgresql': 'postgresql',
492
    'django.db.backends.mysql': '',
493
    'django.db.backends.sqlite3': 'mssql',
494
    'django.db.backends.oracle': 'oracle'}
495

    
496

    
497
def test_concurrently(times=2):
498
    """
499
    Add this decorator to small pieces of code that you want to test
500
    concurrently to make sure they don't raise exceptions when run at the
501
    same time.  E.g., some Django views that do a SELECT and then a subsequent
502
    INSERT might fail when the INSERT assumes that the data has not changed
503
    since the SELECT.
504
    """
505
    def test_concurrently_decorator(test_func):
506
        def wrapper(*args, **kwargs):
507
            exceptions = []
508

    
509
            def call_test_func():
510
                try:
511
                    test_func(*args, **kwargs)
512
                except Exception, e:
513
                    exceptions.append(e)
514
                    raise
515

    
516
            threads = []
517
            for i in range(times):
518
                threads.append(threading.Thread())
519
            for t in threads:
520
                t.start()
521
            for t in threads:
522
                t.join()
523
            if exceptions:
524
                raise Exception(
525
                    ('test_concurrently intercepted %s',
526
                     'exceptions: %s') % (len(exceptions), exceptions))
527
        return wrapper
528
    return test_concurrently_decorator